卡頓優化

前言

無論是啟動,記憶體,布局等等這些優化,最終的目的就是為了應用不卡頓。應用的體驗性好壞,最直觀的表現就是應用的流暢程度,用戶不知道什麼啟動優化,記憶體不足,等等,應用卡頓,那麼這個應用就不行,被卸載的概率非常大。所以說為了保證用戶留存率,卡頓優化是非常非常的重要。在這篇文章,咱們不討論是什麼原因造成卡頓,其實在前面寫的性能優化文章中,都是造成卡頓的原因,需要需要做好卡頓優化,最好從頭開始一步一步來處理。今天我們主要是介紹一些針對卡頓檢測的一些工具使用。

檢測卡頓常用工具

Systrace

Systrace這個工具在《布局優化》一章節中已經介紹過了,這裡就不在贅述。地址://www.cnblogs.com/huangjialin/p/13353541.html

StrictMode的使用

StrictMode,嚴苛模式。StrictMode是在Android開發過程中不可缺少的性能檢測工具,它能夠檢測出在開發過程中不合理的程式碼塊,非常方便。

策略分類

StrictMode分為執行緒策略(ThreadPolicy)和虛擬機策略(VmPolicy)

使用方式
 //開啟Thread策略模式
        StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                .detectNetwork()//監測主執行緒使用網路io
//                .detectCustomSlowCalls()//監測自定義運行緩慢函數
//                .detectDiskReads() // 檢測在UI執行緒讀磁碟操作
//                .detectDiskWrites() // 檢測在UI執行緒寫磁碟操作
                .detectAll()
                .penaltyLog() //寫入日誌
                .penaltyDialog()//監測到上述狀況時彈出對話框
                .build());

        //開啟VM策略模式
        StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
//                .detectLeakedSqlLiteObjects()//監測sqlite泄露
//                .detectLeakedClosableObjects()//監測沒有關閉IO對象
//                .setClassInstanceLimit(MainActivity.class, 1) // 設置某個類的同時處於記憶體中的實例上限,可以協助檢查記憶體泄露
//                .detectActivityLeaks()
                .detectAll()
                .penaltyLog()//寫入日誌
                .build());

上面基本都注釋好了,這裡就不在一一說明了。如果我們在開發過程中,能夠通過StrictMode這個工具類來規避掉這些問題,那麼將會大大的減少很多性能相關的問題。

BlockCanary使用

我們先看看怎麼使用,然後在看BlockCanary
依賴

debugImplementation 'com.github.bzcoder:blockcanarycompat-android:0.0.4'

在application中

BlockCanary.install(mContext, appBlockCanaryContext).start();

appBlockCanaryContext類

/**
 * BlockCanary配置的各種資訊
 */
public class AppBlockCanaryContext extends BlockCanaryContext {

    /**
     * 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 500;
    }

    /**
     * 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) {
        Log.i("lz","blockInfo "+blockInfo.toString());
    }
}

就是這麼簡單,一旦發生卡頓,那麼將會以通知的形式在通知欄中顯示出對應的堆棧資訊(和leakcanary類似,但是兩者沒有任何關係)。或者在
SD卡中的blockcanary文件夾中生成對應的log日誌

那麼,問題來了BlockCanary是怎麼檢測卡頓的呢?大家注意AppBlockCanaryContext類中有一個provideBlockThreshold()方法,return了一個500ms。我們知道Android中消息的分發處理都是通過handler來的,handler中有一個looper對象,looper對象中有一個loop方法,看一下源碼

...

 for (;;) {
            Message msg = queue.next(); // might block
           ...//省略若干
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }

           ...//省略若干
            
            try {
                msg.target.dispatchMessage(msg);
                dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0;
            } finally {
                if (traceTag != 0) {
                    Trace.traceEnd(traceTag);
                }
            }
            ...//省略若干

            if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }

           ...//省略若干
        }


...

應用中的所有事件都是通過dispatchMessage這個方法來進行處理,那麼是不是說dispatchMessage執行的時間就是某個事件執行的時間。如果這個時間大於我們在provideBlockThreshold()定義的時間,我們就認為發送了卡頓了,需要優化,就會獲取到對應的堆棧資訊保存起來並提示。

ANR-WatchDog介紹

發生卡頓,嚴重時很容易導致anr,我們知道發生anr的情況基本上就是以下幾種情況
1、觸摸事件在5s沒有得到響應。
2、Bradcast 是10s,後台廣播是60s
3、service:前台service 20s 後台service 200s

用戶在使用應用時,出現ANR,那麼體驗將會是非常非常差,現在很多手機廠商為了用戶體驗,去掉了這個ANR彈框,但是還是會一直卡在這裡。
ANR-WatchDog 使用
添加依賴

implementation 'com.github.anrwatchdog:anrwatchdog:1.3.0'

在application中的onCreate()方法

new ANRWatchDog().setIgnoreDebugger(true).start();

setIgnoreDebugger 是指忽略點斷點的情況
當然如果這樣設置的話,一旦出現ANR,那麼ANR-Watchdog是會將應用殺死的,如果不想殺死應用那麼需要設置一個監聽

new ANRWatchDog().setANRListener(new ANRWatchDog.ANRListener() {
    @Override
    public void onAppNotResponding(ANRError error) {
        // Handle the error. For example, log it to HockeyApp:
       
    }
}).start();

ANR-WatchDog 原理
我用自己的話來總結一下原理:前面我們說了所有的消息都是通過handler中的dispatchMessage方法進行分發的,而ANR-WatchDog 原理就是通過dispatchMessage的時間長度來判斷是否出現anr的,過程大概是這樣的:ANR-WatchDog 開啟一個監控執行緒,並向主執行緒發一個消息,然後自己休眠5s,5s後看主執行緒有沒有處理這個消息,如果處理了,那麼繼續發送,進入一下次檢查。如果沒有處理,說明主執行緒發生了阻塞,收集對應的anr資訊。

Lancet的介紹

前面寫的幾篇性能優化文章基本都介紹有對應的hook框架,這裡再介紹一個AOP框架—Lancet,這幾個框架的比對,後面會再開一篇。這裡先看看怎麼使用。Lancet是一個輕量級Android AOP框架,特點:
1、編譯速度快,並且支援增量編譯
2、簡潔的API,幾行程式碼完成注入需求
3、沒有任何多餘程式碼插入apk
4、支援用於SDK,可以在SDK編寫注入程式碼來修改依賴SDK的APP

使用方式
在根目錄的build.gradle
dependencies{
classpath ‘me.ele:lancet-plugin:1.0.5’
}

在APP目錄的build.gradle
apply plugin: ‘me.ele.lancet’

dependencies {
provided ‘me.ele:lancet-base:1.0.5’
}

開一個類aaaa.java

@Proxy(“i”)
@TargetClass(“android.util.Log”)
public static int anyName(String tag, String msg){
msg = msg + “lancet”;
return (int) Origin.call();
}

這樣,就完成了,當我們通過Log.i(“lancet”,”你好「);來列印日誌的時候,輸出的文本先被Lancet出來後,在輸出。”你好,lancet”

常見卡頓問題解決方案

1、記憶體抖動的問題,GC過於頻繁
2、方法太耗時了(CPU佔用)
3、布局過於複雜,渲染速度慢
….