深刻理解 Activty 加載速度優化

如何定義activity加載速度?

我的理解,進入一個activity開始 一直到首屏頁面被渲染出來也就是用戶可見的狀態。這個時間固然是越短越好。這個時間越長, activity的白屏時間就越長,這對於不少低端的手機用戶來講是不可忍受的,用戶體驗極差。java

如何獲得activity繪製完ui界面的時間?

方案A:onCreate和onResume的時間差

答:先說結論,此測量activity首屏渲染時間的方法爲錯誤。 下面從多個維度來證實這個方案的錯誤。android

首先咱們看onResume的函數註釋:git

注意看這個地方紅線標註的單詞是指的「交互」這個意思,也就是說,執行到onresume這個方法的時候是指的用戶能夠交互了, 並無說能夠看到東西了,也沒說ui繪製完畢了。有人問 「能夠交互「」難道不是在「能夠看見「」以後麼,你沒看到怎麼交互呢? 其實這個說法是錯誤的,若是你的代碼寫的很爛,手機又不好的話,其實activity在白屏的時候 頁面還沒渲染出來你就能夠點返回 鍵進行返回了。這個返回的動做 就是能夠交互的狀態,可是白屏表明着界面還沒繪製完畢。這一點你用MONKEY跑自動化測試的時候 能夠明顯看到。github

方案B:命令行查看activity的啓動時間

能夠看到用命令行啓動一個acitivty的時候 下面也是有時間輸出的。這個時間通常都會認爲至關接近咱們想要的activity的啓動時間了。咱們注意看一下 一樣的一條命令, 咱們第一次啓動這個activity遠遠比後面幾回時間要長。緣由就是第一次加載一個activity的時候 不少圖片類的資源 文字資源 xml等等信息都是第一次load到內存裏,因此比較耗時,後面由於加載過一次因此內存有一些緩存之類的東西因此後面幾回時間會比較快(要知道io操做是至關耗時的,直接從內存加載固然快不少)。shell

咱們在源碼裏搜索一下這段輸出的日誌關鍵字,最終定位到這段日誌是在activityrecord這個類的這個方法裏輸出的。數據庫

你們能夠看一下,這個totalTime 的定義,當前時間 減去 開始運行的時間。能夠得出一個結論這個時間已經很是接近 咱們想要的時間了。咱們的界面繪製時間必定是小於這個總時間的。 有興趣的同窗能夠跟蹤一下這個mLaunStartTime 究竟是在哪裏被誰賦值。我這裏篇幅所限就不過多論述。緩存

能夠給點提示activitystack的這個方法被調用的時候賦值的。bash

有沒有更好的方案C?

方案B的時間雖然能夠接近咱們想要的結果,可是畢竟這是命令行才能使用,還得有root權限,非root權限的手機你是沒法 執行這個命令的,這讓咱們想統計activity的啓動時間帶來了困難。必定要找到一個能夠從代碼層面輸出界面繪製時間的方法。微信

都知道activity的管理者真正是activitythread,因此咱們直接找這個類的源碼看看。這個方法過長了,咱們先放主要的片斷網絡

首先咱們看第一張圖,這裏明顯的調用了,resume這個方法的回調,可是下面第二張圖能夠看到裏面有個decorView 而且這個decorView 正在被vm add進去,都知道decorView的子view 有個xml佈局裏面有個framelayout是咱們acitivity的rootview,就是那個id爲content的layout。能夠看出來 這裏onResume方法調用就在這個addview 前面了,因此再次證實方案a是多麼不靠譜,你acitivity的界面都沒add進去呢 怎麼可能繪製結束?

這裏可能有些繞,可是隻要記住activity的層級關係便可:

一個Activity包含了一個Window,這個Window實際上是一個PhoneWindow,在PhoneWindow中包含了DecorView,變量名稱爲mDecor,mDecor有一個子View,這個子View的佈局方式根據設定的主題來肯定,在這個子View的xml佈局中包含了一個FrameLayout元素,這個FrameLayout元素的id爲content,這個content對應於PhoneWindow中的mContentParent變量,用戶自定義的佈局做爲mContentParent的子View存在,通常狀況下mContentParnet只有一個子View,若是在Activity調用addView方式其實是給PhoneWindow中的mContentParent添加子View,因爲mContentParent是一個FrameLayout,所以新的子view會覆蓋經過setContentView添加的子view。

繼續跟:

一直跟,跟到這裏:

這裏咱們new 出了ViewRootImpl對象, 咱們知道這個對象就是android view的根對象了,負責view繪製的measure, layout, draw的巨長的方法 performTraversals就是這個類的,咱們繼續看setView方法 這裏面最重要的就是調用了requestLayout 這個方法

@Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }
    
    //這個方法其實不難理解,看名字本身翻譯下就知道就是遍歷作一些事情的意思(至因而什麼事固然是ui繪製啊)
    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            //Choreographer 負責幀率刷新的一個類,之後會講到他。暫時理解成相似於往ui線程post了一個消息就能夠了
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }
    
    //mTraversalRunnable 就是這個類的對象
    final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }
    final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
    
    
    void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

            if (mProfile) {
                Debug.startMethodTracing("ViewAncestor");
            }
            //這個方法應該很敏感,頗有名的一個方法 就不分析他了 太長了,超出篇幅。
            performTraversals();

            if (mProfile) {
                Debug.stopMethodTracing();
                mProfile = false;
            }
        }
    }
複製代碼

分析到這裏,應該能夠稍微理一理activity繪製的一個大概流程:

1.activitythread 調用handleresumeactivity方法 也就是 先回調onresume方法 2.scheduleTraversals post了一個TraversalRunnable消息。 3.post的這個消息作了一件事 調用了繪製ui的核心方法performTraversals。

這個流程也再次驗證了方案a 利用oncreate和onresume時間差的不靠譜

方案C:IdleHandler

方案C 是一個接近靠譜的方法。在闡述這個方法以前,咱們先用一張圖迴歸一下Handler Looper和MessageQueue這個東西。

簡單來講一下這三者之間的關係: Handler經過sendMessage將消息投遞給MessageQueue,Looper經過消息循環(loop)不斷的從MessageQueue中取出消息,而後消息被Handler的dispatchMessage分發到handleMessage方法消費掉。

而後咱們看一個特殊的源碼,來自於MessageQueue:

注意看他的註釋:

其實意思就是說,若是咱們looper裏的消息都處理完了,那麼就會回調這個接口,若是這個方法返回false,那麼回調這一次之後就會把這個idleHandler給幹掉,若是返回true,那麼消息處理完畢就繼續調用這個iderHandler接口的queueidle方法。

so:咱們的正確方案C 就呼之欲出了:

t1 就是oncreate方法的時間戳。 第一個標註紅線的 顯然是被證實過錯誤的作法。 而第二個標註紅線的 顯然是正確的作法。 前面已經分析過,activity的繪製正是從往ui線程的handler裏post的 一個消息開始,那麼這個消息對應的動做所有處理結束之後, 顯然就回回調咱們這個idleHandler的了。因此這個方法是目前爲止最通用最準確 獲取activity啓動之後到顯示東西到屏幕這一段時間 最準確的方法。

知道activity啓動時間了之後能作什麼?

簡單來講,在大部分低端手機中,咱們老是但願用戶進入一個新頁面的時候能儘快看到這個頁面想要展現的內容,尤爲在弱網環境 或者大量數據須要從網絡中獲取時,咱們老是但願界面能先展現一些固定的結構,甚至基本要素。而後等對應的接口回來之後再進去 填充數據,不然頁面白白的區域顯示時間過長,體驗不佳(這點頭條新浪微博微信等作的尤爲出色)

如何加快activity的啓動時間?

cpu的時間片老是固定的,硬件所限,爲了讓ui線程儘快的處理完畢,咱們老是但願這一段時間內儘量的只有ui線程在跑, 這樣ui線程獲取的時間片更多,執行速度起來就會很快,若是你一開始就在oncreate方法裏作了太多的諸如網絡操做, io操做,數據庫操做,那必然的是ui線程獲取cpu時間變少,速度變慢。

肯定咱們的延遲加載方案

咱們來看這樣一段程序:

TextView textView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        textView = (TextView) findViewById(R.id.tv);
        Log.v("wuyue", "textView height==" + textView.getWidth());
    }

    @Override
    protected void onResume() {
        super.onResume();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() {
                @Override
                public boolean queueIdle() {
                    Log.v("wuyue", "textView height2==" + textView.getWidth());
                    return false;
                }
            });
        }
    }
複製代碼

很顯然,第一種在oncreate方法裏獲取tv的高度確定獲取不到由於這會還沒繪製結束呢。 第二種就能夠拿到了,緣由前面已經說過了。很少講。

日誌也反應了咱們的正確性。

那麼有沒有更好的方法來證實這個是正確的呢?

能夠用android studio的 method trace來看方法的執行軌跡,ddms的 method profiling也能夠。這2個工具在這裏很少介紹了。 是查卡頓的很重要的方法,各位自行百度谷歌使用方法便可。

除了啓動優化之外,咱們還能夠作些什麼?

前面講述的是activity的啓動優化,實際上,咱們更但願實時的知道咱們app運行的具體狀況,好比滑動的時候到底有沒有卡頓? 若是有卡頓發生,怎麼知道大概在哪裏出現了問題以便咱們迅速定位到問題代碼?

adb shell dumpsys gfxinfo

這個命令你們都很熟悉,可獲取最新128幀的繪製信息,詳細包括每一幀繪製的Draw,Process,Execute三個過程的耗時,若是這三個時間總和超過16.6ms即認爲是發生了卡頓。 可是咱們不可能每次到一個頁面都去手動執行如下這個命令,太麻煩了,並且 不一樣的手機還要屢次打這個命令,線上實際生產版本也沒辦法讓用戶來打這個命令獲取結果,因此實際上這個方法並不使用。 仍是須要在代碼層面下功夫

Looper代碼揭祕

ui線程綁定的looper的loop方法 無限循環跑這段代碼,執行dispatch方法,注意這個方法的先後都有logging的輸出。 那麼這2個logging輸出的時間差 是否是就能夠認爲這是咱們執行ui線程的時間嗎?這個時間長不就表明了ui線程有卡頓現象麼?

同時咱們到 這個me.mLogging還能夠經過public的set方法來設置。

肯定思路設計抓取卡頓信息的方案。

經過setMessageLogging方法來設置咱們自定義的printer。

自定義的printer 要重寫 println 方法,判斷若是是dispatch方法先後的日誌格式輸出,那麼就要計算時間戳。

超過這個時間戳就認爲卡頓了,輸出線程上下文堆棧信息 看看是哪裏,哪一個方法出現了卡頓。

重要代碼

  • 自定義printer
package com.suning.mobile.ebuy;

import android.os.Looper;
import android.util.Printer;


public class CustomPrinterForGetBlockInfo {
    public static void start() {
        Looper.getMainLooper().setMessageLogging(new Printer() {
            //日誌輸出有不少種格式,咱們這裏只捕獲ui線程中dispatch上下文的日誌信息
            //因此這裏定義了2個key值,注意不一樣的手機這2個key值可能不同,有須要的話這裏要作機型適配,
            //不然部分手機這裏可能抓取不到日誌信息
            private static final String START = ">>>>> Dispatching";
            private static final String END = "<<<<< Finished";
            @Override
            public void println(String x) {
                //這裏的思路就是若是發如今打印dispatch方法的 start信息,
                //那麼咱們就在 「時間戳」 以後 post一個runnable
                if (x.startsWith(START)) {
                    LogMonitor.getInstance().startMonitor();
                }
                //由於咱們start 不是當即start runnable 而是在「時間戳」 以後 那麼若是在這個時間戳以內
                //dispacth方法執行完畢之後的END到來,那麼就會remove掉這個runnable
                //因此 這裏就知道 若是dispatch方法執行時間在時間戳以內 那麼咱們就認爲這個ui沒卡頓,不輸出任何卡頓信息
                //不然就輸出卡頓信息 這裏卡頓信息主要用StackTraceElement 來輸出
                if (x.startsWith(END)) {
                    LogMonitor.getInstance().removeMonitor();
                }
            }
        });
    }
}

複製代碼
  • 看看咱們的LogMoniter
package com.suning.mobile.ebuy;

import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.util.Log;


public class LogMonitor {
    private static LogMonitor sInstance = new LogMonitor();
    //HandlerThread 這個其實就是一個thread,只不過相對於普通的thread 他對外暴露了一個looper而已。方便
    //咱們和handler配合使用
    private HandlerThread mLogThread = new HandlerThread("BLOCKINFO");
    private Handler mIoHandler;
    //這個時間戳的值,一般設置成不超過1000,你能夠調低這個數值來優化你的代碼。數值越低 暴露的信息就越多
    private static final long TIME_BLOCK = 1000L;

    private LogMonitor() {
        mLogThread.start();
        mIoHandler = new Handler(mLogThread.getLooper());
    }

    private static Runnable mLogRunnable = new Runnable() {
        @Override
        public void run() {
            StringBuilder sb = new StringBuilder();
            //把ui線程的block的堆棧信息都打印出來 方便咱們定位問題
            StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
            for (StackTraceElement s : stackTrace) {
                sb.append(s.toString() + "\n");
            }
            Log.e("BLOCK", sb.toString());
        }
    };

    public static LogMonitor getInstance() {
        return sInstance;
    }

    public void startMonitor() {
        //在time以後 再啓動這個runnable 若是在這個time以前調用了removeMonitor 方法,那這個runnable確定就沒法執行了
        mIoHandler.postDelayed(mLogRunnable, TIME_BLOCK);
    }

    public void removeMonitor() {
        mIoHandler.removeCallbacks(mLogRunnable);
    }
}

複製代碼
  • 最後再application中的oncreate方法啓動咱們的統計函數

基本上就能夠了。能夠知足咱們的卡頓統計需求。

額外奉送,統計幀率的方法。

前面咱們分析actiivty頁面繪製的時候提到過Choreographer這個類。其實這個類網上資料超多,你們能夠自行搜索一下, 這個類的 Choreographer.getInstance().postFrameCallback(this); 是能夠統計到幀率的。實時的,很方便。 經過這個咱們也能夠檢測到卡頓現象,和上面的方法其實效果差很少,惟一要注意的,大多數blog的isMonitor 其實都不可用,緣由是

注意看這個函數是個hide函數,壓根沒辦法給咱們app使用到的。編譯是不可能編譯經過的。 這裏給出正確的寫法,其他代碼我就很少複述了其實都差很少。搜搜均可以搜到。

public boolean isMonitor() {
        //網上流傳的方法多數是這個,可是這個是錯的,由於hasCallbacks 是一個hide函數 你壓根調用不了的,只能反射調用
        //return mIoHandler.hasCallbacks(mLogRunnable);
        try {
            //經過詳細地類名獲取到指定的類
            Class<?> handlerClass = Class.forName("android.os.Handler");
            //經過方法名,傳入參數獲取指定方法
            java.lang.reflect.Method method = handlerClass.getMethod("hasCallbacks", Runnable.class);
            Boolean ret = (Boolean) method.invoke(mIoHandler, mLogRunnable);
            return ret;
        } catch (Exception e) {
        }
        return false;
    }
複製代碼

總結

說了這麼多,其實本篇文章核心思想就2點,統計activity啓動時間,儘量縮小頁面白屏的時間。 統計卡頓的上下文環境,方便咱們定位代碼問題便於優化。大致的分析問題和解決問題的思路都在這裏了。 有興趣的同窗能夠自行拓展思路,寫出一個個庫方便使用。可是核心思想應該就是上述內容。 固然不想重複造輪子的同窗也可使用開源庫。在這裏我推薦2個我的認爲比較好的:

比較小巧精緻的庫功能很少

這個庫就大而全了。全面檢測android app性能的工具

相關文章
相關標籤/搜索