ANR產生的原因及定位分析

ANR全稱是Application Not Responding,意思是應用程序無響應。相信從事Android開發的肯定遇到過。ANR的直觀體驗是用戶在操作App的過程中,感覺界面卡頓,當界面卡頓超過一定時間(一般5秒),就會出現ANR對話框。ANR對於一個應用來說是不能承受之痛,其影響並不比應用發生Crash小。

ANR產生的原因

只有當應用程序的UI線程響應超時纔會引起ANR,超時產生原因一般有兩種。

  • 當前的事件沒有機會得到處理,例如UI線程正在響應另一個事件,當前事件由於某種原因被阻塞了。
  • 當前的事件正在處理,但是由於耗時太長沒能及時完成。

根據ANR產生的原因不同,超時事件也不盡相同,從本質上將,產生ANR的原因有三種,大致可以對應到Android中四大組件中的三個(Activity/View,BroadcastReceiver和Service)。

KeyDispatchTimeout

最常見的一種類型,原因就是View的點擊事件或者觸摸事件在特定的時間(5s)內無法得到響應。

BroadcastTimeout

原因是BroadcastReceiver的onReceive()函數運行在主線程中,在特定的時間(10s)內無法完成處理。

ServiceTimeout

比較少出現的一種類型,原因是Service的各個生命週期函數在特定時間(20s)內無法完成處理。

典型的ANR問題場景

  • 應用程序UI線程存在耗時操作。例如在UI線程中進行聯網請求,數據庫操作或者文件操作等。
  • 應用程序的UI線程等待子線程釋放某個鎖,從而無法處理用戶的輸入。
  • 耗時的動畫需要大量的計算工作,可能導致CPU負載過重。

ANR的定位和分析

當發生ANR時,可以通過結合Logcat日誌和生成的位於手機內部存儲的/data/anr/traces.tex文件進行分析和定位。

Cmd line: com.anly.githubapp  // 最新的ANR發生的進程(包名)

...

DALVIK THREADS (41):
"main" prio=5 tid=1 Sleeping
  | group="main" sCount=1 dsCount=0 obj=0x73467fa8 self=0x7fbf66c95000
  | sysTid=2976 nice=0 cgrp=default sched=0/0 handle=0x7fbf6a8953e0
  | state=S schedstat=( 0 0 0 ) utm=60 stm=37 core=1 HZ=100
  | stack=0x7ffff4ffd000-0x7ffff4fff000 stackSize=8MB
  | held mutexes=
  at java.lang.Thread.sleep!(Native method)
  - sleeping on <0x35fc9e33> (a java.lang.Object)
  at java.lang.Thread.sleep(Thread.java:1031)
  - locked <0x35fc9e33> (a java.lang.Object)
  at java.lang.Thread.sleep(Thread.java:985) // 主線程中sleep過長時間, 阻塞導致無響應.
  at com.tencent.bugly.crashreport.crash.c.l(BUGLY:258)
  - locked <@addr=0x12dadc70> (a com.tencent.bugly.crashreport.crash.c)
  at com.tencent.bugly.crashreport.CrashReport.testANRCrash(BUGLY:166)  // 產生ANR的那個函數調用
  - locked <@addr=0x12d1e840> (a java.lang.Class<com.tencent.bugly.crashreport.CrashReport>)
  at com.anly.githubapp.common.wrapper.CrashHelper.testAnr(CrashHelper.java:23)
  at com.anly.githubapp.ui.module.main.MineFragment.onClick(MineFragment.java:80) // ANR的起點
  at com.anly.githubapp.ui.module.main.MineFragment_ViewBinding$2.doClick(MineFragment_ViewBinding.java:47)
  at butterknife.internal.DebouncingOnClickListener.onClick(DebouncingOnClickListener.java:22)
  at android.view.View.performClick(View.java:4780)
  at android.view.View$PerformClick.run(View.java:19866)
  at android.os.Handler.handleCallback(Handler.java:739)
  at android.os.Handler.dispatchMessage(Handler.java:95)
  at android.os.Looper.loop(Looper.java:135)
  at android.app.ActivityThread.main(ActivityThread.java:5254)
  at java.lang.reflect.Method.invoke!(Native method)
  at java.lang.reflect.Method.invoke(Method.java:372)
  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:903)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:698)

現在找到了ANR產生的原因和位置了,就可以對產生ANR的代碼進行修復。這麼一個一個的找肯定不現實,那我們就來說說如何避免和檢測ANR。

ANR的避免和檢測

爲了避免在開發中引入可能導致應用發生ANR的問題,除了切記不要在主線程中作耗時操作,我們也可以藉助於一些工具來進行檢測,從而更有效的避免ANR的引入。

StrictMode

嚴格模式StrictMode是Android SDK提供的一個用來檢測代碼中是否存在違規操作的工具類,StrictMode主要檢測兩大類問題。

  • 線程策略 ThreadPolicy
    • detectCustomSlowCalls:檢測自定義耗時操作
    • detectDiskReads:檢測是否存在磁盤讀取操作
    • detectDiskWrites:檢測是否存在磁盤寫入操作
    • detectNetWork:檢測是否存在網絡操作
  • 虛擬機策略VmPolicy
    • detectActivityLeaks:檢測是否存在Activity泄露
    • detectLeakedClosableObjects:檢測是否存在未關閉的Closeable對象泄露
    • detectLeakedSqlLiteObjects:檢測是否存在Sqlite對象泄露
    • setClassInstanceLimit:檢測類實例個數是否超過限制

可以看到,ThreadPolicy可以用來檢測可能催在的主線程耗時操作,需要注意的是我們只能在Debug版本中使用它,發佈到市場上的版本要關閉掉。StrictMode的使用很簡單,我們只需要在應用初始化的地方例如Application或者MainActivity類的onCreate方法中執行如下代碼:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);

        // 開啓線程模式
        StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                .detectAll()
                .penaltyLog()
                .penaltyDialog() ////打印logcat,當然也可以定位到dropbox,通過文件保存相應的log
                .build());
        // 開啓虛擬機模式
        StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                .detectAll()
                .penaltyLog()
                .build());
    }
}

上面的初始化代碼調用penaltyLog表示在Logcat中打印日誌,調用detectAll方法表示啓動所有的檢測策略,我們也可以根據應用的具體要求只開啓某些策略,語句如下:

         StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                 .detectDiskReads()
                 .detectDiskWrites()
                 .detectNetwork()  
                 .penaltyLog()
                 .build());

         StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                 .detectLeakedSqlLiteObjects()
                 .detectLeakedClosableObjects()
                 .detectActivityLeaks()
                 .penaltyLog()
                 .build());

BlockCanary

BlockCanary是一個非侵入式式的性能監控函數庫,它的用法和leakCanary類似,只不過後者監控應用的內存泄露,而BlockCanary主要用來監控應用主線程的卡頓。它的基本原理是利用主線程的消息隊列處理機制,通過對比消息分發開始和結束的時間點來判斷是否超過設定的時間,如果是,則判斷爲主線程卡頓。它的集成很簡單,首先在build.gradle中添加依賴

一般選取以下其中一個 case 引入即可

dependencies {
    compile 'com.github.markzhai:blockcanary-android:1.5.0'

    // 僅在debug包啓用BlockCanary進行卡頓監控和提示的話,可以這麼用
    debugCompile 'com.github.markzhai:blockcanary-android:1.5.0'
    releaseCompile 'com.github.markzhai:blockcanary-no-op:1.5.0'
}

然後在Application類中進行配置和初始化即可

public class AnrDemoApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        // 在主進程初始化調用哈
        BlockCanary.install(this, new AppBlockCanaryContext()).start();

    }
}

實現自己監控的上下文


public class AppBlockCanaryContext extends BlockCanaryContext {
    // 實現各種上下文,包括應用標示符,用戶uid,網絡類型,卡慢判斷闕值,Log保存位置等

    /**
     * Implement in your project.
     *
     * @return Qualifier which can specify this installation, like version + flavor.
     */
    public String provideQualifier() {
        return "unknown";
    }


    /**
     * Implement in your project.
     *
     * @return user id
     */
    public String provideUid() {
        return "uid";
    }

    /**
     * Network type
     *
     * @return {@link String} like 2G, 3G, 4G, wifi, etc.
     */
    public String provideNetworkType() {
        return "unknown";
    }

    /**
     * Config monitor duration, after this time BlockCanary will stop, use
     * with {@code BlockCanary}'s isMonitorDurationEnd
     *
     * @return monitor last duration (in hour)
     */
    public int provideMonitorDuration() {
        return -1;
    }

    /**
     * Config block threshold (in millis), dispatch over this duration is regarded as a BLOCK. You may set it
     * from performance of device.
     *
     * @return threshold in mills
     */
    public int provideBlockThreshold() {
        return 1000;
    }

    /**
     * Thread stack dump interval, use when block happens, BlockCanary will dump on main thread
     * stack according to current sample cycle.
     * <p>
     * Because the implementation mechanism of Looper, real dump interval would be longer than
     * the period specified here (especially when cpu is busier).
     * </p>
     *
     * @return dump interval (in millis)
     */
    public int provideDumpInterval() {
        return provideBlockThreshold();
    }

    /**
     * Path to save log, like "/blockcanary/", will save to sdcard if can.
     *
     * @return path of log files
     */
    public String providePath() {
        return "/blockcanary/";
    }

    /**
     * If need notification to notice block.
     *
     * @return true if need, else if not need.
     */
    public boolean displayNotification() {
        return true;
    }

    /**
     * Implement in your project, bundle files into a zip file.
     *
     * @param src  files before compress
     * @param dest files compressed
     * @return true if compression is successful
     */
    public boolean zip(File[] src, File dest) {
        return false;
    }

    /**
     * Implement in your project, bundled log files.
     *
     * @param zippedFile zipped file
     */
    public void upload(File zippedFile) {
        throw new UnsupportedOperationException();
    }


    /**
     * Packages that developer concern, by default it uses process name,
     * put high priority one in pre-order.
     *
     * @return null if simply concern only package with process name.
     */
    public List<String> concernPackages() {
        return null;
    }

    /**
     * Filter stack without any in concern package, used with @{code concernPackages}.
     *
     * @return true if filter, false it not.
     */
    public boolean filterNonConcernStack() {
        return false;
    }

    /**
     * Provide white list, entry in white list will not be shown in ui list.
     *
     * @return return null if you don't need white-list filter.
     */
    public List<String> provideWhiteList() {
        LinkedList<String> whiteList = new LinkedList<>();
        whiteList.add("org.chromium");
        return whiteList;
    }

    /**
     * Whether to delete files whose stack is in white list, used with white-list.
     *
     * @return true if delete, false it not.
     */
    public boolean deleteFilesInWhiteList() {
        return true;
    }

    /**
     * Block interceptor, developer may provide their own actions.
     */
    public void onBlock(Context context, BlockInfo blockInfo) {

    }
}

在AndroidManifest.xml文件中聲明Application,一定不要忘記

這裏寫圖片描述

現在就已經將BlockCanary集成到應用裏面了,接下來,編譯安裝到手機上,點擊測試按鈕,將產生一個ANR,效果如圖:

這裏寫圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章