电脑知识|欧美黑人一区二区三区|软件|欧美黑人一级爽快片淫片高清|系统|欧美黑人狂野猛交老妇|数据库|服务器|编程开发|网络运营|知识问答|技术教程文章 - 好吧啦网

您的位置:首頁技術文章
文章詳情頁

Android教你如何發現APP卡頓的實現

瀏覽:38日期:2022-09-21 18:26:29

最近部門打算優化下 APP 在低端機上的卡頓情況,既然想優化,就必須獲取卡頓情況,那么如何獲取卡頓情況就是本文目的。

一般主線程過多的 UI 繪制、大量的 IO 操作或是大量的計算操作占用 CPU,導致 App 界面卡頓。只要我們能在發生卡頓的時候,捕捉到主線程的堆棧信息和系統的資源使用信息,即可準確分析卡頓發生在什么函數,資源占用情況如何。那么問題就是如何有效檢測 Android 主線程的卡頓發生?

用 adb 系統工具觀察 App 的卡頓數據情況,試圖重現場景來定位問題。

常用的方式是使用 adb SurfaceFlinger 服務和 adb gfxinfo 功能,在自動化操作 app 的過程中,使用 adb 獲取數據來監控 app 的流暢情況,發現出現出現卡頓的時間段,尋找出現卡頓的場景和操作。

方式1:adb shell dumpsysSurfaceFlinger

使用 ‘adb shell dumpsysSurfaceFlinger’ 命令即可獲取最近 127 幀的數據,通過定期執行 adb 命令,獲取幀數來計算出幀率 FPS。

方式2:adb shell dumpsys gfxinfo

使用 ‘adb shell dumpsys gfxinfo’ 命令即可獲取最新 128 幀的繪制信息,詳細包括每一幀繪制的 Draw,Process,Execute 三個過程的耗時,如果這三個時間總和超過 16.6ms 即認為是發生了卡頓。

已有的兩種方案比較適合衡量回歸卡頓問題的修復效果和判斷某些特定場景下是否有卡頓情況,然而,這樣的方式有幾個明顯的不足:

一般很難構造實際用戶卡頓的環境來重現; 這種方式操作起來比較麻煩,需編寫自動化用例,無法覆蓋大量的可疑場景,測試重現耗時耗人力; 無法衡量靜態頁面的卡頓情況; 出現卡頓的時候app無法及時獲取運行狀態和信息,開發定位困難。

隨著對Android 源碼的深入研究,也有了其他兩種比較方便的方式,并且這兩種方式侵入性小,占用內存低,能夠更好的用在實際場景中:

利用UI線程的Looper打印的日志匹配; 使用Choreographer.FrameCallback

利用 UI 線程的 Looper 打印的日志匹配

Android 主線程更新 UI。如果界面1秒鐘刷新少于 60 次,即 FPS 小于 60,用戶就會產生卡頓感覺。簡單來說,Android 使用消息機制進行 UI 更新,UI 線程有個 Looper,在其 loop方法中會不斷取出 message,調用其綁定的 Handler 在 UI 線程執行。如果在 handler 的 dispatchMesaage 方法里有耗時操作,就會發生卡頓。

下面來看下 Looper.loop( ) 的源碼

public static void loop() { final Looper me = myLooper(); if (me == null) { throw new RuntimeException('No Looper; Looper.prepare() wasn’t called on this thread.'); } final MessageQueue queue = me.mQueue; // Make sure the identity of this thread is that of the local process, // and keep track of what that identity token actually is. Binder.clearCallingIdentity(); final long ident = Binder.clearCallingIdentity(); // Allow overriding a threshold with a system prop. e.g. // adb shell ’setprop log.looper.1000.main.slow 1 && stop && start’ final int thresholdOverride =SystemProperties.getInt('log.looper.' + Process.myUid() + '.' + Thread.currentThread().getName() + '.slow', 0); boolean slowDeliveryDetected = false; for (;;) { Message msg = queue.next(); // might block if (msg == null) {// No message indicates that the message queue is quitting.return; } // This must be in a local variable, in case a UI event sets the logger final Printer logging = me.mLogging; if (logging != null) {logging.println('>>>>> Dispatching to ' + msg.target + ' ' + msg.callback + ': ' + msg.what); } // Make sure the observer won’t change while processing a transaction. final Observer observer = sObserver; final long traceTag = me.mTraceTag; long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs; long slowDeliveryThresholdMs = me.mSlowDeliveryThresholdMs; if (thresholdOverride > 0) {slowDispatchThresholdMs = thresholdOverride;slowDeliveryThresholdMs = thresholdOverride; } final boolean logSlowDelivery = (slowDeliveryThresholdMs > 0) && (msg.when > 0); final boolean logSlowDispatch = (slowDispatchThresholdMs > 0); final boolean needStartTime = logSlowDelivery || logSlowDispatch; final boolean needEndTime = logSlowDispatch; if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {Trace.traceBegin(traceTag, msg.target.getTraceName(msg)); } final long dispatchStart = needStartTime ? SystemClock.uptimeMillis() : 0; final long dispatchEnd; Object token = null; if (observer != null) {token = observer.messageDispatchStarting(); } long origWorkSource = ThreadLocalWorkSource.setUid(msg.workSourceUid); try {msg.target.dispatchMessage(msg);if (observer != null) { observer.messageDispatched(token, msg);}dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0; } catch (Exception exception) {if (observer != null) { observer.dispatchingThrewException(token, msg, exception);}throw exception; } finally {ThreadLocalWorkSource.restore(origWorkSource);if (traceTag != 0) { Trace.traceEnd(traceTag);} } if (logSlowDelivery) {if (slowDeliveryDetected) { if ((dispatchStart - msg.when) <= 10) { Slog.w(TAG, 'Drained'); slowDeliveryDetected = false; }} else { if (showSlowLog(slowDeliveryThresholdMs, msg.when, dispatchStart, 'delivery', msg)) { // Once we write a slow delivery log, suppress until the queue drains. slowDeliveryDetected = true; }} } if (logSlowDispatch) {showSlowLog(slowDispatchThresholdMs, dispatchStart, dispatchEnd, 'dispatch', msg); } if (logging != null) {logging.println('<<<<< Finished to ' + msg.target + ' ' + msg.callback); } // Make sure that during the course of dispatching the // identity of the thread wasn’t corrupted. final long newIdent = Binder.clearCallingIdentity(); if (ident != newIdent) {Log.wtf(TAG, 'Thread identity changed from 0x' + Long.toHexString(ident) + ' to 0x' + Long.toHexString(newIdent) + ' while dispatching to ' + msg.target.getClass().getName() + ' ' + msg.callback + ' what=' + msg.what); } msg.recycleUnchecked(); } }

代碼中兩處標紅的地方,就是 msg.target.dispatchMessage(msg) 的執行前后索打印的 log。通過測量處理時間就能檢測到部分UI線程是否有耗時的操作。注意到這行執行代碼的前后,有兩個 logging.println 函數,如果設置了logging,會分別打印出 ”>>>>> Dispatching to “ 和 ”<<<<< Finished to “ 這樣的日志,這樣我們就可以通過兩次log的時間差值,來計算 dispatchMessage 的執行時間,從而設置閾值判斷是否發生了卡頓。

那么如何設置 logging 呢?

我們看下面的代碼:

/** * Control logging of messages as they are processed by this Looper. If * enabled, a log message will be written to <var>printer</var> * at the beginning and ending of each message dispatch, identifying the * target Handler and message contents. * * @param printer A Printer object that will receive log messages, or * null to disable message logging. */public final class Looper { private Printer mLogging; public void setMessageLogging(@Nullable Printer printer) { mLogging = printer; } } public interface Printer { void println(String x); }

Looper 的 mLogging 是私有的,并且提供了 setMessageLogging(@Nullable Printer printer) 方法,所以我們可以自己實現一個 Printer,在通過 setMessageLogging() 方法傳入即可,代碼如下:

public class BlockDetectByPrinter { public static void start() { Looper.getMainLooper().setMessageLogging(new Printer() { private static final String START = '>>>>> Dispatching'; private static final String END = '<<<<< Finished'; @Override public void println(String x) {if (x.startsWith(START)) { LogMonitor.getInstance().startMonitor();}if (x.startsWith(END)) { LogMonitor.getInstance().removeMonitor();} } }); }}

設置了logging后,loop方法會回調 logging.println 打印出每次消息執行的時間日志:”>>>>> Dispatching to “和”<<<<< Finished to “。BlockDetectByPrinter 的使用則在Application 的 onCreate 方法中調用 BlockDetectByPrinter.start() 即可。

我們可以簡單實現一個 LogMonitor 來記錄卡頓時候主線程的堆棧信息。當匹配到 >>>>> Dispatching 時,執行 startMonitor,會在 200ms(設定的卡頓閾值)后執行任務,這個任務負責在子線程(非UI線程)打印UI線程的堆棧信息。如果消息低于 200ms 內執行完成,就可以匹配到 <<<<< Finished 日志,那么在打印堆棧任務啟動前執行 removeMonitor 取消了這個任務,則認為沒有卡頓的發生;如果消息超過 200ms 才執行完畢,此時認為發生了卡頓,并打印 UI 線程的堆棧信息。

LogMonitor如何實現?

public class LogMonitor { private static final String TAG = 'LogMonitor'; private static LogMonitor sInstance = new LogMonitor(); private HandlerThread mLogThread = new HandlerThread('log'); private Handler mIoHandler; private static final long TIME_BLOCK = 200L; private LogMonitor() { mLogThread.start(); mIoHandler = new Handler(mLogThread.getLooper()); } private static Runnable mLogRunnable = new Runnable() { @Override public void run() { StringBuilder sb = new StringBuilder(); StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace(); for (StackTraceElement s : stackTrace) {sb.append(s.toString() + 'n'); } Log.e(TAG, sb.toString()); } }; public static LogMonitor getInstance() { return sInstance; } public boolean isMonitor() { return mIoHandler.hasCallbacks(mLogRunnable); } public void startMonitor() { mIoHandler.postDelayed(mLogRunnable, TIME_BLOCK); } public void removeMonitor() { mIoHandler.removeCallbacks(mLogRunnable); }}

這里我們使用 HandlerThread 來構造一個 Handler,HandlerThread 繼承自 Thread,實際上就一個 Thread,只不過比普通的 Thread 多了一個 Looper,對外提供自己這個 Looper 對象的 getLooper 方法,然后創建 Handler 時將 HandlerThread 中的 looper 對象傳入。這樣我們的 mIoHandler 對象就是與 HandlerThread 這個非 UI 線程綁定的了,它處理耗時操作將不會阻塞UI。如果UI線程阻塞超過 200ms,就會在子線程中執行 mLogRunnable,打印出 UI 線程當前的堆棧信息,如果處理消息沒有超過 1000ms,則會實時的 remove 掉這個mLogRunnable 任務。

發生卡頓時打印出堆棧信息的大致內容如下,開發可以通過 log 定位耗時的地方。

2020-10-30 14:26:13.823 30359-30415/com.example.myproxyplugin E/LogMonitor: java.lang.Thread.sleep(Native Method) java.lang.Thread.sleep(Thread.java:443) java.lang.Thread.sleep(Thread.java:359) com.example.myproxyplugin.MainActivity$1.run(MainActivity.java:22) android.os.Handler.handleCallback(Handler.java:900) android.os.Handler.dispatchMessage(Handler.java:103) android.os.Looper.loop(Looper.java:219) android.app.ActivityThread.main(ActivityThread.java:8347) java.lang.reflect.Method.invoke(Native Method) com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513) com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1055)

優點:用戶使用 app 或者測試過程中都能從app層面來監控卡頓情況,一旦出現卡頓能記錄 app 狀態和信息, 只要dispatchMesaage執行耗時過大都會記錄下來,不再有前面兩種adb方式面臨的問題與不足。

缺點:需另開子線程獲取堆棧信息,會消耗少量系統資源。

在實際實現中,不同手機不同 Android 系統甚至是不同的 ROM 版本,Loop 函數不一定都能打印出 ”>>>>> Dispatching to “ 和 ”<<<<< Finished to “ 這樣的日志,導致該方式無法進行。

優化的策略:我們知道 Loop 函數開始和結束必會執行 println 打印日志,所以優化版本將卡頓的判斷改為,Loop輸出第一句 log 時當作 startMonitor,輸出下一句log時當作end時刻來解決這個問題。

其實 Looper 中有個 Observer 接口可以很好的完成這個任務,只是因為被標記為 hide 了,所以我們不能使用,不過可以知道下。

Observer 接口提供了三個方法,分別是監聽任務開始,結束,發生錯誤的回調。

/** {@hide} */ public interface Observer { /** * Called right before a message is dispatched. * * <p> The token type is not specified to allow the implementation to specify its own type. * * @return a token used for collecting telemetry when dispatching a single message. * The token token must be passed back exactly once to either * {@link Observer#messageDispatched} or {@link Observer#dispatchingThrewException} * and must not be reused again. * */ Object messageDispatchStarting(); /** * Called when a message was processed by a Handler. * * @param token Token obtained by previously calling * {@link Observer#messageDispatchStarting} on the same Observer instance. * @param msg The message that was dispatched. */ void messageDispatched(Object token, Message msg); /** * Called when an exception was thrown while processing a message. * * @param token Token obtained by previously calling * {@link Observer#messageDispatchStarting} on the same Observer instance. * @param msg The message that was dispatched and caused an exception. * @param exception The exception that was thrown. */ void dispatchingThrewException(Object token, Message msg, Exception exception); }

利用Choreographer.FrameCallback監控卡頓

Choreographer.FrameCallback 官方文檔鏈接(https://developer.android.com/reference/android/view/Choreographer.FrameCallback.html)

我們知道, Android 系統每隔 16ms 發出 VSYNC 信號,來通知界面進行重繪、渲染,每一次同步的周期為16.6ms,代表一幀的刷新頻率。SDK 中包含了一個相關類,以及相關回調。理論上來說兩次回調的時間周期應該在 16ms,如果超過了 16ms 我們則認為發生了卡頓,利用兩次回調間的時間周期來判斷是否發生卡頓(這個方案是 Android 4.1 API 16 以上才支持)。

這個方案的原理主要是通過 Choreographer 類設置它的 FrameCallback 函數,當每一幀被渲染時會觸發回調 FrameCallback, FrameCallback 回調 void doFrame (long frameTimeNanos) 函數。一次界面渲染會回調 doFrame 方法,如果兩次 doFrame 之間的間隔大于 16.6ms 說明發生了卡頓。

public class FPSFrameCallback implements Choreographer.FrameCallback { private static final String TAG = 'FPS_TEST'; private long mLastFrameTimeNanos = 0; private long mFrameIntervalNanos; public FPSFrameCallback(long lastFrameTimeNanos) { mLastFrameTimeNanos = lastFrameTimeNanos; // 1s 60 幀 mFrameIntervalNanos = (long) (1000000000 / 60.0); } @Override public void doFrame(long frameTimeNanos) { //初始化時間 if (mLastFrameTimeNanos == 0) { mLastFrameTimeNanos = frameTimeNanos; } final long jitterNanos = frameTimeNanos - mLastFrameTimeNanos; if (jitterNanos >= mFrameIntervalNanos) { final long skippedFrames = jitterNanos / mFrameIntervalNanos; if (skippedFrames > 30) {Log.i(TAG, 'Skipped ' + skippedFrames + ' frames! ' + 'The application may be doing too much work on its main thread.'); } } mLastFrameTimeNanos = frameTimeNanos; //注冊下一幀回調 Choreographer.getInstance().postFrameCallback(this); }}

本質和 log 沒太多區別,但是這個更加通用些,不會因為機型系統原因出現不可用的問題。

示例

下面進入實戰,看看代碼層面是如何實現的。

MainActivity 代碼如下:

public class MainActivity extends AppCompatActivity { Handler handler = new Handler(Looper.getMainLooper()); private final Runnable runnable = new Runnable() { @Override public void run() { try {Thread.sleep(600);handler.postDelayed(runnable, 500); } catch (InterruptedException e) {e.printStackTrace(); } } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Choreographer.getInstance().postFrameCallback(new FPSFrameCallback(System.nanoTime())); BlockDetectByPrinter.start(); } @Override protected void onResume() { super.onResume(); handler.postDelayed(runnable, 500); }}

收集到的堆棧信息如下:

2020-10-30 14:26:13.823 30359-30415/com.example.myproxyplugin E/LogMonitor: java.lang.Thread.sleep(Native Method) java.lang.Thread.sleep(Thread.java:443) java.lang.Thread.sleep(Thread.java:359) com.example.myproxyplugin.MainActivity$1.run(MainActivity.java:22) android.os.Handler.handleCallback(Handler.java:900) android.os.Handler.dispatchMessage(Handler.java:103) android.os.Looper.loop(Looper.java:219) android.app.ActivityThread.main(ActivityThread.java:8347) java.lang.reflect.Method.invoke(Native Method) com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513) com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1055)

對于 FPS log 可以看到如下信息:

I/Choreographer: Skipped 64 frames! The application may be doing too much work on its main thread. I/FPS_TEST: Skipped 65 frames! The application may be doing too much work on its main thread.

如果你要把上面的方法用到自己的APP 中,那么還需要很多操作,具體可以閱讀參考文獻的內容。

參考文章

廣研Android卡頓監控系統

到此這篇關于Android教你如何發現APP卡頓的實現的文章就介紹到這了,更多相關Android APP卡頓內容請搜索好吧啦網以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持好吧啦網!

標簽: Android
相關文章:
主站蜘蛛池模板: 上海皓越真空设备有限公司官网-真空炉-真空热压烧结炉-sps放电等离子烧结炉 | 定坤静电科技静电消除器厂家-除静电设备| 带压开孔_带压堵漏_带压封堵-菏泽金升管道工程有限公司 | 超声波清洗机_超声波清洗机设备_超声波清洗机厂家_鼎泰恒胜 | 找果网 | 苹果手机找回方法,苹果iPhone手机丢了找回,认准找果网! | 学习安徽网| 科研ELISA试剂盒,酶联免疫检测试剂盒,昆虫_植物ELISA酶免试剂盒-上海仁捷生物科技有限公司 | 自清洗过滤器_全自动过滤器_全自动反冲洗过滤器_量子过滤器-滑漮滴 | 耐高温风管_耐高温软管_食品级软管_吸尘管_钢丝软管_卫生级软管_塑料波纹管-东莞市鑫翔宇软管有限公司 | 福建省教师资格证-福建教师资格证考试网 | 杭州月嫂技术培训服务公司-催乳师培训中心报名费用-产后康复师培训机构-杭州优贝姆健康管理有限公司 | 穿线管|波纹穿线管|包塑金属软管|蛇皮管?闵彬专注弱电工程? | 电动卫生级调节阀,电动防爆球阀,电动软密封蝶阀,气动高压球阀,气动对夹蝶阀,气动V型调节球阀-上海川沪阀门有限公司 | 苏州伊诺尔拆除公司_专业酒店厂房拆除_商场学校拆除_办公楼房屋拆除_家工装拆除拆旧 | 微型气泵-真空-蠕动-水泵-厂家-深圳市品亚科技有限公司 | 上海单片机培训|重庆曙海培训分支机构—CortexM3+uC/OS培训班,北京linux培训,Windows驱动开发培训|上海IC版图设计,西安linux培训,北京汽车电子EMC培训,ARM培训,MTK培训,Android培训 | 神马影院-实时更新秒播| 广州食堂承包_广州团餐配送_广州堂食餐饮服务公司 - 旺记餐饮 | 代办建筑资质升级-建筑资质延期就找上海国信启航 | 成都热收缩包装机_袖口式膜包机_高速塑封机价格_全自动封切机器_大型套膜机厂家 | 艾默生变频器,艾默生ct,变频器,ct驱动器,广州艾默生变频器,供水专用变频器,风机变频器,电梯变频器,艾默生变频器代理-广州市盟雄贸易有限公司官方网站-艾默生变频器应用解决方案服务商 | 产业规划_产业园区规划-产业投资选址及规划招商托管一体化服务商-中机院产业园区规划网 | 河南卓美创业科技有限公司-河南卓美防雷公司-防雷接地-防雷工程-重庆避雷针-避雷器-防雷检测-避雷带-避雷针-避雷塔、机房防雷、古建筑防雷等-山西防雷公司 | 高压绝缘垫-红色配电房绝缘垫-绿色高压绝缘地毯-上海苏海电气 | 鄂泉泵业官网|(杭州、上海、全国畅销)大流量防汛排涝泵-LW立式排污泵 | 干式变压器厂_干式变压器厂家_scb11/scb13/scb10/scb14/scb18干式变压器生产厂家-山东科锐变压器有限公司 | 超声波成孔成槽质量检测仪-压浆机-桥梁预应力智能张拉设备-上海硕冠检测设备有限公司 | 小程序开发公司_APP开发多少钱_软件开发定制_微信小程序制作_客户销售管理软件-济南小溪畅流网络科技有限公司 | 岩石钻裂机-液压凿岩机-劈裂机-挖改钻_湖南烈岩科技有限公司 | 在线钠离子分析仪-硅酸根离子浓度测定仪-油液水分测定仪价格-北京时代新维测控设备有限公司 | 皮带式输送机械|链板式输送机|不锈钢输送机|网带输送机械设备——青岛鸿儒机械有限公司 | 申江储气罐厂家,储气罐批发价格,储气罐规格-上海申江压力容器有限公司(厂) | TPE_TPE热塑性弹性体_TPE原料价格_TPE材料厂家-惠州市中塑王塑胶制品公司- 中塑王塑胶制品有限公司 | ◆大型吹塑加工|吹塑加工|吹塑代加工|吹塑加工厂|吹塑设备|滚塑加工|滚塑代加工-莱力奇塑业有限公司 | 上海小程序开发-小程序制作-上海小程序定制开发公司-微信商城小程序-上海咏熠 | 大_小鼠elisa试剂盒-植物_人Elisa试剂盒-PCR荧光定量试剂盒-上海一研生物科技有限公司 | 上海新光明泵业制造有限公司-电动隔膜泵,气动隔膜泵,卧式|立式离心泵厂家 | 帽子厂家_帽子工厂_帽子定做_义乌帽厂_帽厂_制帽厂_帽子厂_浙江高普制帽厂 | 橡胶接头_橡胶软接头_套管伸缩器_管道伸缩器厂家-巩义市远大供水材料有限公司 | 微水泥_硅藻泥_艺术涂料_艺术漆_艺术漆加盟-青岛泥之韵环保壁材 武汉EPS线条_EPS装饰线条_EPS构件_湖北博欧EPS线条厂家 | 矿用履带式平板车|探水钻机|气动架柱式钻机|架柱式液压回转钻机|履带式钻机-启睿探水钻机厂家 |