深刻探索Android啓動速度優化

前言

成爲一名優秀的Android開發,須要一份完備的知識體系,在這裏,讓咱們一塊兒成長爲本身所想的那樣~。

在性能優化的整個知識體系中,最重要的就是穩定性優化,在上一篇文章 《深刻探索Android穩定性優化》 中咱們已經深刻探索了Android穩定性優化的疆域。那麼,除了穩定性之外,對於性能緯度來講,哪一個方面的性能是最重要的呢?毫無疑問,就是應用的啓動速度。下面,就讓咱們揚起航帆,一塊兒來逐步深刻探索Android啓動速度優化的奧祕html

思惟導圖大綱

目錄

  • 1、啓動優化的意義
  • 2、應用啓動流程
    • 1 、應用啓動的類型
    • 二、冷啓動分析及其優化方向
  • 3、啓動耗時檢測
    • 一、查看Logcat
    • 二、adb shell
    • 三、代碼打點(函數插樁)
    • 四、AOP(Aspect Oriented Programming) 打點
    • 五、啓動速度分析工具 — TraceView
    • 六、啓動速度分析工具 — Systrace
    • 七、啓動監控
  • 4、啓動優化常規方案
    • 一、主題切換
    • 二、第三方庫懶加載
    • 三、異步初始化預備知識-線程優化
    • 四、異步初始化
    • 五、延遲初始化
    • 六、Multidex預加載優化
    • 七、類預加載優化
    • 八、WebView啓動優化
    • 九、頁面數據預加載
    • 十、啓動階段不啓動子進程
    • 十一、閃屏頁與主頁的繪製優化
  • 5、啓動優化黑科技
    • 一、啓動階段抑制GC
    • 二、CPU鎖頻
    • 三、IO優化
    • 四、數據重排
    • 五、類加載優化(Dalvik)
    • 六、保活
  • 6、啓動優化的常見問題
    • 一、啓動優化是怎麼作的?
    • 二、是怎麼異步的,異步遇到問題沒有?
    • 三、啓動優化有哪些容易忽略的注意點?
    • 四、版本迭代致使的啓動變慢有好的解決方式嗎?
  • 7、總結
    • 一、優化總方針
    • 二、注意事項

1、啓動優化的意義

若是咱們去一家餐廳吃飯,在點餐的時候等了半天都沒有服務人員過來,可能就沒有耐心等待直接走了。java

對於App來講,也是一樣如此,若是用戶點擊App後,App半天都打不開,用戶就可能失去耐心卸載應用python

啓動速度是用戶對咱們App的第一體驗,打開應用後才能去使用其中提供的強大功能,就算咱們應用的內部界面設計的再精美,功能再強大,若是啓動速度過慢,用戶第一印象就會不好linux

所以,拯救App的啓動速度,迫在眉睫。android

2、應用啓動流程

1 、應用啓動的類型

應用啓動的類型總共分爲以下三種:git

  • 冷啓動
  • 熱啓動
  • 溫啓動

下面,咱們來詳細分析下各個啓動類型的特色及流程。github

冷啓動

從點擊應用圖標到UI界面徹底顯示且用戶可操做的所有過程。web

特色

耗時最多,衡量標準算法

啓動流程

Click Event -> IPC -> Process.start -> ActivityThread -> bindApplication -> LifeCycle -> ViewRootImplshell

首先,用戶進行了一個點擊操做,這個點擊事件它會觸發一個IPC的操做,以後便會執行到Process的start方法中,這個方法是用於進程建立的,接着,便會執行到ActivityThread的main方法,這個方法能夠看作是咱們單個App進程的入口,至關於Java進程的main方法,在其中會執行消息循環的建立與主線程Handler的建立,建立完成以後,就會執行到 bindApplication 方法,在這裏使用了反射去建立 Application以及調用了 Application相關的生命週期,Application結束以後,便會執行Activity的生命週期,在Activity生命週期結束以後,最後,就會執行到 ViewRootImpl,這時纔會進行真正的一個頁面的繪製

熱啓動

直接從後臺切換到前臺。

特色

啓動速度最快。

溫啓動

只會重走Activity的生命週期,而不會重走進程的建立,Application的建立與生命週期等。

特色

較快,介於冷啓動和熱啓動之間的一個速度。

啓動流程

LifeCycle -> ViewRootImpl

ViewRootImpl是什麼?

它是GUI管理系統與GUI呈現系統之間的橋樑。每個ViewRootImpl關聯一個Window, ViewRootImpl 最終會經過它的setView方法綁定Window所對應的View,並經過其performTraversals方法對View進行佈局、測量和繪製

二、冷啓動分析及其優化方向

冷啓動涉及的相關任務

冷啓動以前

  • 首先,會啓動App
  • 而後,加載空白Window
  • 最後,建立進程

須要注意的是,這些都是系統的行爲,通常狀況下咱們是沒法直接干預的。

隨後任務

  • 首先,建立Application
  • 啓動主線程
  • 建立MainActivity
  • 加載佈局
  • 佈置屏幕
  • 首幀繪製

一般到了界面首幀繪製完成後,咱們就能夠認爲啓動已經結束了。

優化方向

咱們的優化方向就是 Application和Activity的生命週期 這個階段,由於這個階段的時機對於咱們來講是可控的

3、啓動耗時檢測

一、查看Logcat

在Android Studio Logcat中過濾關鍵字「Displayed」,能夠看到對應的冷啓動耗時日誌。

二、adb shell

使用adb shell獲取應用的啓動時間

// 其中的AppstartActivity全路徑能夠省略前面的packageName
adb shell am start -W [packageName]/[AppstartActivity全路徑]
複製代碼

執行後會獲得三個時間:ThisTime、TotalTime和WaitTime,詳情以下:

ThisTime

表示最後一個Activity啓動耗時。

TotalTime

表示全部Activity啓動耗時。

WaitTime

表示AMS啓動Activity的總耗時。

通常來講,只需查看獲得的TotalTime,即應用的啓動時間,其包括 建立進程 + Application初始化 + Activity初始化到界面顯示 的過程。

特色:

  • 一、線下使用方便,不能帶到線上
  • 二、非嚴謹、精確時間

三、代碼打點(函數插樁)

能夠寫一個統計耗時的工具類來記錄整個過程的耗時狀況。其中須要注意的有:

  • 在上傳數據到服務器時建議根據用戶ID的尾號來抽樣上報
  • 在項目中核心基類的關鍵回調函數和核心方法中加入打點。

其代碼以下所示:

/**
* 耗時監視器對象,記錄整個過程的耗時狀況,能夠用在不少須要統計的地方,好比Activity的啓動耗時和Fragment的啓動耗時。
*/
public class TimeMonitor {

    private final String TAG = TimeMonitor.class.getSimpleName();
    private int mMonitord = -1;
    
    // 保存一個耗時統計模塊的各類耗時,tag對應某一個階段的時間
    private HashMap<String, Long> mTimeTag = new HashMap<>();
    private long mStartTime = 0;

    public TimeMonitor(int mMonitorId) {
        Log.d(TAG, "init TimeMonitor id: " + mMonitorId);
        this.mMonitorId = mMonitorId;
    }

    public int getMonitorId() {
        return mMonitorId;
    }

    public void startMonitor() {
        // 每次從新啓動都把前面的數據清除,避免統計錯誤的數據
        if (mTimeTag.size() > 0) {
        mTimeTag.clear();
        }
        mStartTime = System.currentTimeMillis();
    }

    /**
    * 每打一次點,記錄某個tag的耗時
    */
    public void recordingTimeTag(String tag) {
        // 若保存過相同的tag,先清除
        if (mTimeTag.get(tag) != null) {
            mTimeTag.remove(tag);
        }
        long time = System.currentTimeMillis() - mStartTime;
        Log.d(TAG, tag + ": " + time);
        mTimeTag.put(tag, time);
    }

    public void end(String tag, boolean writeLog) {
        recordingTimeTag(tag);
        end(writeLog);
    }

    public void end(boolean writeLog) {
        if (writeLog) {
            //寫入到本地文件
        }
    }

    public HashMap<String, Long> getTimeTags() {
        return mTimeTag;
    }
}
複製代碼

爲了使代碼更好管理,咱們須要定義一個打點配置類,以下所示:

/**
* 打點配置類,用於統計各階段的耗時,便於代碼的維護和管理。
*/
public final class TimeMonitorConfig {

    // 應用啓動耗時
    public static final int TIME_MONITOR_ID_APPLICATION_START = 1;
}
複製代碼

此外,耗時統計可能會在多個模塊和類中須要打點,因此須要一個單例類來管理各個耗時統計的數據

/**
* 採用單例管理各個耗時統計的數據。
*/
public class TimeMonitorManager {

    private static TimeMonitorManager mTimeMonitorManager = null;
private HashMap<Integer, TimeMonitor> mTimeMonitorMap = null;

    public synchronized static TimeMonitorManager getInstance() {
        if (mTimeMonitorManager == null) {
            mTimeMonitorManager = new TimeMonitorManager();
        }
        return mTimeMonitorManager;
    }

    public TimeMonitorManager() {
        this.mTimeMonitorMap = new HashMap<Integer, TimeMonitor>();
    }

    /**
     * 初始化打點模塊
    */
    public void resetTimeMonitor(int id) {
        if (mTimeMonitorMap.get(id) != null) {
            mTimeMonitorMap.remove(id);
        }
        getTimeMonitor(id).startMonitor();
    }

    /**
    * 獲取打點器
    */
    public TimeMonitor getTimeMonitor(int id) {
        TimeMonitor monitor = mTimeMonitorMap.get(id);
        if (monitor == null) {
            monitor = new TimeMonitor(id);
            mTimeMonitorMap.put(id, monitor);
        }
        return monitor;
    }
}
複製代碼

主要在如下幾個方面須要打點:

  • 應用程序的生命週期節點
  • 啓動時須要初始化的重要方法,例如數據庫初始化,讀取本地的一些數據。
  • 其餘耗時的一些算法

例如,啓動時在Application和第一個Activity加入打點統計:

Application 打點

@Override
protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    TimeMonitorManager.getInstance().resetTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START);
}

@Override
public void onCreate() {
    super.onCreate();
    SoLoader.init(this, /* native exopackage */ false);
    TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START).recordingTimeTag("Application-onCreate");
}
複製代碼

第一個Activity打點

@Override
protected void onCreate(Bundle savedInstanceState) {
    TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START).recordingTimeTag("SplashActivity-onCreate");
    super.onCreate(savedInstanceState);
    
    initData();
    
    TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START).recordingTimeTag("SplashActivity-onCreate-Over");
}

@Override
protected void onStart() {
    super.onStart();
    TimeMonitorManager.getInstance().getTimeMonitor(TimeMonitorConfig.TIME_MONITOR_ID_APPLICATION_START).end("SplashActivity-onStart", false);
}
複製代碼

特色

精確,可帶到線上,可是代碼有侵入性,修改爲本高

注意事項

  • 一、在上傳數據到服務器時建議根據用戶ID的尾號來抽樣上報

  • 二、onWindowFocusChanged只是首幀時間,App啓動完成的結束點應該是真實數據展現出來的時候(一般來講都是首幀數據),如列表第一條數據展現,記得使用getViewTreeObserver().addOnPreDrawListener()(在API 16以上能夠使用addOnDrawListener),它會把任務延遲到列表顯示後再執行,例如,在Awesome-WanAndroid項目的主頁就有一個RecyclerView實現的列表,啓動結束的時間就是列表的首幀時間,也即列表第一條數據展現的時候。這裏,咱們直接在RecyclerView的適配器ArticleListAdapter的convert(onBindViewHolder)方法中加上以下代碼便可:

    if (helper.getLayoutPosition() == 1 && !mHasRecorded) {
          mHasRecorded = true;
          helper.getView(R.id.item_search_pager_group).getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
              @Override
              public boolean onPreDraw() {
                  helper.getView(R.id.item_search_pager_group).getViewTreeObserver().removeOnPreDrawListener(this);
                  LogHelper.i("FeedShow");
                  return true;
              }
          });
      }
    複製代碼

具體的實例代碼可在 這裏查看

爲何不使用onWindowFocusChanged這個方法做爲啓動結束點?

由於用戶看到真實的界面是須要有網絡請求返回真實數據的,可是onWindowFocusChanged只是界面繪製的首幀時機,可是列表中的數據是須要從網絡中下載獲得的,因此應該以列表的首幀數據做爲啓動結束點。

四、AOP(Aspect Oriented Programming) 打點

面向切面編程,經過預編譯和運行期動態代理實現程序功能統一維護的一種技術。

一、做用

利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合性下降提升程序的可重用性,同時大大提升了開發效率

二、AOP核心概念

一、橫切關注點

對哪些方法進行攔截,攔截後怎麼處理。

二、切面(Aspect)

類是對物體特徵的抽象,切面就是對橫切關注點的抽象

三、鏈接點(JoinPoint)

被攔截到的點(方法、字段、構造器)。

四、切入點(PointCut)

對JoinPoint進行攔截的定義。

五、通知(Advice)

攔截到JoinPoint後要執行的代碼,分爲前置、後置、環繞三種類型。

三、準備:接入AspectJx進行切面編碼

首先,爲了在Android使用AOP埋點須要引入AspectJ,在項目根目錄的build.gradle下加入:

classpath 'com.hujiang.aspectjx:gradle-android-plugin- aspectjx:2.0.0'
複製代碼

而後,在app目錄下的build.gradle下加入:

apply plugin: 'android-aspectjx'
implement 'org.aspectj:aspectjrt:1.8.+'
複製代碼

四、AOP埋點實戰

JoinPoint通常定位在以下位置

  • 一、函數調用
  • 二、獲取、設置變量
  • 三、類初始化

使用PointCut對咱們指定的鏈接點進行攔截,經過Advice,就能夠攔截到JoinPoint後要執行的代碼。Advice一般有如下幾種類型:

  • 一、Before:PointCut以前執行
  • 二、After:PointCut以後執行
  • 三、Around:PointCut以前、以後分別執行

首先,咱們舉一個小栗子:

@Before("execution(* android.app.Activity.on**(..))")
public void onActivityCalled(JoinPoint joinPoint) throws Throwable {
...
}
複製代碼

在 execution 中的是一個匹配規則,第一個 * 表明匹配任意的方法返回值,後面的語法代碼匹配全部Activity中on開頭的方法

其中execution是處理Join Point的類型,在AspectJx中共有兩種類型,以下所示:

  • 一、call:插入在函數體裏面
  • 二、execution:插入在函數體外面

如何統計Application中的全部方法耗時?

@Aspect
public class ApplicationAop {

    @Around("call (* com.json.chao.application.BaseApplication.**(..))")
    public void getTime(ProceedingJoinPoint joinPoint) {
    Signature signature = joinPoint.getSignature();
    String name = signature.toShortString();
    long time = System.currentTimeMillis();
    try {
        joinPoint.proceed();
    } catch (Throwable throwable) {
        throwable.printStackTrace();
    }
    Log.i(TAG, name + " cost" +     (System.currentTimeMillis() - time));
    }
}
複製代碼

在上述代碼中,咱們須要注意 不一樣的Action類型其對應的方法入參是不一樣的,具體的差別以下所示:

  • 當Action爲Before、After時,方法入參爲JoinPoint
  • 當Action爲Around時,方法入參爲ProceedingPoint
Around和Before、After的最大區別:

ProceedingPoint不一樣於JoinPoint,其提供了proceed方法執行目標方法

五、總結AOP特性

  • 一、無侵入性
  • 二、修改方便,建議使用

四、啓動速度分析工具 — TraceView

一、使用方式

  • 一、代碼中添加:Debug.startMethodTracing()、檢測方法、Debug.stopMethodTracing()。(須要使用adb pull將生成的.trace文件導出到電腦,而後使用Android Studio的Profiler進行加載)
  • 二、打開 Profiler -> CPU -> 點擊 Record -> 點擊 Stop -> 查看Profiler下方Top Down/Bottom Up 區域,以找出耗時的熱點方法

二、Profile CPU

使用 Profile 的 CPU 模塊能夠幫咱們快速找到耗時的熱點方法,下面,咱們來詳細來分析一下這個模塊。

一、Trace types

Trace types 有四種,以下所示。

一、Trace Java Methods

會記錄每一個方法的時間、CPU信息。對運行時性能影響較大。

二、Sample Java Methods

相比於Trace Java Methods會記錄每一個方法的時間、CPU信息,它會在應用的Java代碼執行期間頻繁捕獲應用的調用堆棧,對運行時性能的影響比較小,可以記錄更大的數據區域

三、Sample C/C++ Functions

需部署到Android 8.0及以上設備,內部使用simpleperf跟蹤應用的native代碼,也能夠命令行使用simpleperf。

四、Trace System Calls

  • 檢查應用與系統資源的交互狀況
  • 查看全部核心的CPU瓶頸
  • 內部採用systrace,也能夠使用systrace命令。

二、Event timeline

用於顯示應用程序在其生命週期中轉換不一樣狀態的活動,如用戶交互、屏幕旋轉事件等。

三、CPU timeline

用於顯示應用程序 實時CPU使用率、其它進程實時CPU使用率、應用程序使用的線程總數

四、Thread activity timeline

列出應用程序進程中的每一個線程,並使用了不一樣的顏色在其時間軸上指示其活動

  • 綠色:線程處於活動狀態準備好使用CPU
  • 黃色:線程正等待IO操做。(重要)
  • 灰色:線程正在睡眠不消耗CPU時間

五、檢查跟蹤數據窗口

Profile提供的檢查跟蹤數據窗口有四種,以下所示:

一、Call Chart

提供函數跟蹤數據的圖形表示形式。

  • 水平軸:表示調用的時間段和時間
  • 垂直軸:顯示被調用方
  • 橙色系統API
  • 綠色應用自有方法
  • 藍色第三方API(包括Java API)。
提示

右鍵點擊 Jump to source 跳轉至指定函數。

二、Flame Chart

將具備相同調用方順序的徹底相同的方法收集起來

  • 水平軸執行每一個方法的相對時間量
  • 垂直軸:顯示被調用方
使用技巧

頂層哪一個函數佔據的寬度最大(表現爲平頂),可能存在性能問題

三、Top Down

  • 遞歸調用列表,提供self、children、total時間和比率來表示被調用的函數信息
  • Flame Chart是Top Down列表數據的圖形化

四、Bottom Up

  • 展開函數會顯示其調用方
  • 按照消耗CPU時間由多到少的順序對函數排序

注意事項

咱們在查看上面4個跟蹤數據的區域時,應該注意右側的兩個時間,以下所示:

  • Wall Clock Time程序執行時間
  • Thread TimeCPU執行的時間

三、TraceView小結

特色

  • 一、圖形的形式展現執行時間、調用棧等。
  • 二、信息全面,包含全部線程
  • 三、運行時開銷嚴重,總體都會變慢,得出的結果並不真實
  • 四、找到最耗費時間的路徑:Flame Chart、Top Down
  • 五、找到最耗費時間的節點:Bottom Up

做用

主要作熱點分析,用來獲得如下兩種數據:

  • 單次執行最耗時的方法
  • 執行次數最多的方法

五、啓動速度分析工具 — Systrace

一、使用方式:代碼插樁

首先,咱們能夠定義一個Trace靜態工廠類,將Trace.begainSection(),Trace.endSection()封裝成i、o方法,而後再在想要分析的方法先後進行插樁便可。

而後,在命令行下執行systrace.py腳本,命令以下所示:

python /Users/quchao/Library/Android/sdk/platform-tools/systrace/systrace.py -t 20 sched gfx view wm am app webview -a "com.wanandroid.json.chao" -o ~/Documents/open-project/systrace_data/wanandroid_start_1.html
複製代碼

具體參數含義以下:

  • -t:指定統計時間爲20s。
  • shced:cpu調度信息。
  • gfx:圖形信息。
  • view:視圖。
  • wm:窗口管理。
  • am:活動管理。
  • app:應用信息。
  • webview:webview信息。
  • -a:指定目標應用程序的包名。
  • -o:生成的systrace.html文件。

如何查看數據?

UIThread一欄能夠看到核心的系統方法時間區域和咱們本身使用代碼插樁捕獲的方法時間區域

二、Systrace原理

  • 首先,在系統的一些關鍵鏈路(如SystemServcie、虛擬機、Binder驅動)插入一些信息(Label)
  • 而後,經過Label的開始和結束來肯定某個核心過程的執行時間,並把這些Label信息收集起來獲得系統關鍵路徑的運行時間信息,最後獲得整個系統的運行性能信息;

其中,Android Framework 裏面一些重要的模塊都插入了label信息,用戶App中也能夠添加自定義的Lable。

三、Systrace小結

特色

  • 結合Android內核的數據,生成Html報告。
  • 系統版本越高,Android Framework中添加的系統可用Label就越多,可以支持和分析的系統模塊也就越多
  • 必須手動縮小範圍,會幫助你加速收斂問題的分析過程,進而快速地定位和解決問題

做用

  • 主要用於分析繪製性能方面的問題
  • 分析系統關鍵方法和應用方法耗時

六、啓動監控

一、實驗室監控:視頻錄製

  • 80%繪製
  • 圖像識別

注意

覆蓋高中低端機型不一樣的場景。

二、線上監控

目標

須要準確地統計啓動耗時。

一、啓動結束的統計時機

是不是使用界面顯示且用戶真正能夠操做的時間做爲啓動結束時間。

二、啓動時間扣除邏輯

閃屏、廣告和新手引導這些時間都應該從啓動時間裏扣除。

三、啓動排除邏輯

Broadcast、Server拉起,啓動過程進入後臺都須要排除統計。

四、使用什麼指標來衡量啓動速度的快慢?

平均啓動時間的問題

一些體驗不好的用戶極可能被平均了。

建議的指標
  • 一、快開慢開比

如2s快開比,5s慢開比,能夠看到有多少比例的用戶體驗好,多少比例的用戶比較糟糕

  • 二、90%用戶的啓動時間

若是90%用戶的啓動時間都小於5s,那麼90%區間的啓動耗時就是5s。

五、啓動的類型有哪幾種?

  • 首次安裝啓動
  • 覆蓋安裝啓動
  • 冷啓動(指標)
  • 熱啓動(反映程序的活躍或保活能力)

借鑑Facebook的 profilo 工具原理,對啓動整個流程進行耗時監控,在後臺對不一樣的版本作自動化對比,監控新版本是否有新增耗時的函數。

4、啓動優化常規方案

啓動過程當中的常見問題

  • 一、點擊圖標好久都不響應:預覽窗口被禁用或設置爲透明。
  • 二、首頁顯示太慢:初始化任務太多。
  • 三、首頁顯示後沒法進行操做:太多延遲初始化任務佔用主線程CPU時間片。

優化區域

Application、Activity建立以及回調等過程。

一、主題切換

使用Activity的windowBackground主題屬性預先設置一個啓動圖片(layer-list實現),在啓動後,在Activity的onCreate()方法中的super.onCreate()前再setTheme(R.style.AppTheme)。

優勢

  • 使用簡單
  • 避免了啓動白屏和點擊啓動圖標不響應的狀況

缺點

  • 治標不治本,表面上產生一種快的感受。
  • 對於中低端機,總的閃屏時間會更長,建議只在Android6.0/7.0以上才啓用「預覽閃屏」方案,讓手機性能好的用戶能夠有更好的體驗

二、第三方庫懶加載

按需初始化,特別是針對於一些應用啓動時不須要初始化的庫,能夠等到用時才進行加載。

三、異步初始化預備知識-線程優化

一、Android線程調度原理剖析

線程調度原理

  • 一、任意時刻,只有一個線程佔用CPU,處於運行狀態
  • 二、多線程併發,輪流獲取CPU使用權。
  • 三、JVM負責線程調度,按照特定機制分配CPU使用權。

線程調度模型

一、分時調度模型

輪流獲取、均分CPU。

二、搶佔式調度模型

優先級高的獲取。

如何幹預線程調度?

設置線程優先級。

Android線程調度

一、nice值
  • Process中定義。
  • 值越小,優先級越高。
  • 默認是THREAD_PRIORITY_DEFAUT,0。
二、cgroup

它是一種更嚴格的羣組調度策略,主要分爲以下兩種類型:

  • 後臺group(默認)。
  • 前臺group,保證前臺線程能夠獲取到更多的CPU

注意點

  • 線程過多會致使CPU頻繁切換,下降線程運行效率
  • 正確認識任務重要性以決定使用哪一種線程優先級
  • 優先級具備繼承性。

二、Android異步方式

一、Thread

  • 最簡單、常見的異步方式。
  • 不易複用,頻繁建立及銷燬開銷大。
  • 複雜場景不易使用。

二、HandlerThread

  • 自帶消息循環的線程。
  • 串行執行。
  • 長時間運行,不斷從隊列中獲取任務。

三、IntentService

  • 繼承自Service在內部建立HandlerThread。
  • 異步,不佔用主線程。
  • 優先級較高,不易被系統Kill。

四、AsyncTask

  • Android提供的工具類。
  • 無需本身處理線程切換。
  • 需注意版本不一致問題(API 14以上解決)

五、線程池

  • Java提供的線程池。
  • 易複用,減小頻繁建立、銷燬的時間。
  • 功能強大,如定時、任務隊列、併發數控制等。

六、RxJava

由強大的調度器Scheduler集合提供。

不一樣類型的Scheduler:

  • IO
  • Computation

異步方式總結

  • 推薦度:從後往前排列。
  • 正確場景選擇正確的方式。

三、Android線程優化實戰

線程使用準則

  • 一、嚴禁使用new Thread方式
  • 二、提供基礎線程池供各個業務線使用,避免各個業務線各自維護一套線程池,致使線程數過多。
  • 三、根據任務類型選擇合適的異步方式:優先級低,長時間執行,HandlerThread;定時執行耗時任務,線程池。
  • 四、建立線程必須命名,以方便定位線程歸屬,在運行期 Thread.currentThread().setName 修更名字。
  • 五、關鍵異步任務監控,注意異步不等於不耗時,建議使用AOP的方式來作監控
  • 六、重視優先級設置(根據任務具體狀況),Process.setThreadPriority() 能夠設置屢次

四、如何鎖定線程建立者

鎖定線程建立背景

  • 項目變大以後收斂線程。
  • 項目源碼、三方庫、aar中都有線程的建立。

鎖定線程建立方案

特別適合Hook手段,找Hook點:構造函數或者特定方法,如Thread的構造函數。

實戰

這裏咱們直接使用維數的 epic 對Thread進行Hook。在attachBaseContext中調用DexposedBridge.hookAllConstructors方法便可,以下所示:

DexposedBridge.hookAllConstructors(Thread.class, new XC_MethodHook() { 
    @Override protected void afterHookedMethod(MethodHookParam param)throws Throwable {                         
        super.afterHookedMethod(param); 
        Thread thread = (Thread) param.thisObject; 
        LogUtils.i("stack " + Log.getStackTraceString(new Throwable());
    }
);
複製代碼

從log找到線程建立信息,根據堆棧信息跟相關業務方溝通解決方案。

五、線程收斂優雅實踐初步

線程收斂常規方案

  • 根據線程建立堆棧考量合理性,使用同一線程庫。
  • 各業務線下掉本身的線程庫。

問題:基礎庫怎麼使用線程?

直接依賴線程庫,但問題在於線程庫更新可能會致使基礎庫更新

基礎庫優雅使用線程

  • 基礎庫內部暴露API:setExecutor
  • 初始化的時候注入統一的線程庫

統一線程庫時區分任務類型

  • IO密集型任務:IO密集型任務不消耗CPU,核心池能夠很大。常見的IO密集型任務如文件讀取、寫入,網絡請求等等。
  • CPU密集型任務:核心池大小和CPU核心數相關。常見的CPU密集型任務如比較複雜的計算操做,此時須要使用大量的CPU計算單元。

實現用於執行多類型任務的基礎線程池組件

目前基礎線程池組件位於啓動器sdk之中,使用很是簡單,示例代碼以下所示:

// 若是當前執行的任務是CPU密集型任務,則從基礎線程池組件
// DispatcherExecutor中獲取到用於執行 CPU 密集型任務的線程池
DispatcherExecutor.getCPUExecutor().execute(YourRunable());

// 若是當前執行的任務是IO密集型任務,則從基礎線程池組件
// DispatcherExecutor中獲取到用於執行 IO 密集型任務的線程池
DispatcherExecutor.getIOExecutor().execute(YourRunable());
複製代碼

具體的實現源碼也比較簡單,而且我對每一處代碼都進行了詳細的解釋,就不一一具體分析了。代碼以下所示:

public class DispatcherExecutor {

    /**
     * CPU 密集型任務的線程池
     */
    private static ThreadPoolExecutor sCPUThreadPoolExecutor;

    /**
     * IO 密集型任務的線程池
     */
    private static ExecutorService sIOThreadPoolExecutor;

    /**
     * 當前設備能夠使用的 CPU 核數
     */
    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();

    /**
     * 線程池核心線程數,其數量在2 ~ 5這個區域內
     */
    private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 5));

    /**
     * 線程池線程數的最大值:這裏指定爲了核心線程數的大小
     */
    private static final int MAXIMUM_POOL_SIZE = CORE_POOL_SIZE;

    /**
    * 線程池中空閒線程等待工做的超時時間,當線程池中
    * 線程數量大於corePoolSize(核心線程數量)或
    * 設置了allowCoreThreadTimeOut(是否容許空閒核心線程超時)時,
    * 線程會根據keepAliveTime的值進行活性檢查,一旦超時便銷燬線程。
    * 不然,線程會永遠等待新的工做。
    */
    private static final int KEEP_ALIVE_SECONDS = 5;

    /**
    * 建立一個基於鏈表節點的阻塞隊列
    */
    private static final BlockingQueue<Runnable> S_POOL_WORK_QUEUE = new LinkedBlockingQueue<>();

    /**
     * 用於建立線程的線程工廠
     */
    private static final DefaultThreadFactory S_THREAD_FACTORY = new DefaultThreadFactory();

    /**
     * 線程池執行耗時任務時發生異常所須要作的拒絕執行處理
     * 注意:通常不會執行到這裏
     */
    private static final RejectedExecutionHandler S_HANDLER = new RejectedExecutionHandler() {
        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            Executors.newCachedThreadPool().execute(r);
        }
    };

    /**
     * 獲取CPU線程池
     *
     * @return CPU線程池
     */
    public static ThreadPoolExecutor getCPUExecutor() {
        return sCPUThreadPoolExecutor;
    }

    /**
     * 獲取IO線程池
     *
     * @return IO線程池
     */
    public static ExecutorService getIOExecutor() {
        return sIOThreadPoolExecutor;
    }

    /**
     * 實現一個默認的線程工廠
     */
    private static class DefaultThreadFactory implements ThreadFactory {
        private static final AtomicInteger POOL_NUMBER = new AtomicInteger(1);
        private final ThreadGroup group;
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String namePrefix;

        DefaultThreadFactory() {
            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGroup() :
                    Thread.currentThread().getThreadGroup();
            namePrefix = "TaskDispatcherPool-" +
                    POOL_NUMBER.getAndIncrement() +
                    "-Thread-";
        }

        @Override
        public Thread newThread(Runnable r) {
            // 每個新建立的線程都會分配到線程組group當中
            Thread t = new Thread(group, r,
                    namePrefix + threadNumber.getAndIncrement(),
                    0);
            if (t.isDaemon()) {
                // 非守護線程
                t.setDaemon(false);
            }
            // 設置線程優先級
            if (t.getPriority() != Thread.NORM_PRIORITY) {
                t.setPriority(Thread.NORM_PRIORITY);
            }
            return t;
        }
    }

    static {
        sCPUThreadPoolExecutor = new ThreadPoolExecutor(
                CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
                S_POOL_WORK_QUEUE, S_THREAD_FACTORY, S_HANDLER);
        // 設置是否容許空閒核心線程超時時,線程會根據keepAliveTime的值進行活性檢查,一旦超時便銷燬線程。不然,線程會永遠等待新的工做。
        sCPUThreadPoolExecutor.allowCoreThreadTimeOut(true);
        // IO密集型任務線程池直接採用CachedThreadPool來實現,
        // 它最多能夠分配Integer.MAX_VALUE個非核心線程用來執行任務
        sIOThreadPoolExecutor = Executors.newCachedThreadPool(S_THREAD_FACTORY);
    }

}
複製代碼

六、線程優化核心問題

一、線程使用爲何會遇到問題?

項目發展階段忽視基礎設施建設,沒有采用統一的線程池,致使線程數量過多。

表現形式

異步任務執行太耗時,致使主線程卡頓。

問題緣由
  • 一、Java線程調度是搶佔式的,線程優先級比較重要,須要區分
  • 二、沒有區分IO和CPU密集型任務,致使主線程搶不到CPU

二、怎麼在項目中對線程進行優化?

核心:線程收斂
  • 經過Hook方式找到對應線程的堆棧信息,和業務方討論是否應該單獨起一個線程,儘量使用統一線程池
  • 每一個基礎庫都暴露一個設置線程池的方法,以免線程庫更新致使基礎庫須要更新的問題
  • 統一線程池應注意IO、CPU密集型任務區分
  • 其它細節:重要異步任務統計耗時、注重異步任務優先級和線程名的設置

四、異步初始化

一、核心思想

子線程分擔主線程任務,並行減小時間。

二、異步優化注意點

  • 一、不符合異步要求
  • 二、須要在某個階段完成(採用CountDownLatch確保異步任務完成後纔到下一個階段)
  • 三、如出現主線程要使用時還沒初始化則在這次使用前初始化
  • 四、區分CPU密集型和IO密集型任務

三、異步初始化方案演進

  • 一、new Thread
  • 二、IntentService
  • 三、線程池(合理配置並選擇CPU密集型和IO密集型線程池)
  • 四、異步啓動器

四、異步優化最優解:異步啓動器

異步啓動器源碼及使用demo地址

常規異步優化痛點

  • 一、代碼不優雅:例如使用線程池實現多個並行異步任務時會有多個executorService.submit代碼塊。
  • 二、場景很差處理:各個初始化任務之間存在依賴關係,例如推送sdk的初始化任務須要依賴於獲取設備id的初始化任務。此外,有些任務是須要在某些特定的時候就初始化完成,例如須要在Application的onCreate方法執行完以前就初始化完成。
  • 三、維護成本高。

啓動器核心思想

充分利用CPU多核,自動梳理任務順序。

啓動器流程

啓動器的流程圖以下所示:

image

啓動器的主題流程爲上圖中的中間區域,即主線程與併發兩個區域塊。須要注意的是,在上圖中的 head task與tail task 並不包含在啓動器的主題流程中,它僅僅是用於處理啓動前/啓動後的一些通用任務,例如咱們能夠在head task中作一些獲取通用信息的操做,在tail task能夠作一些log輸出、數據上報等操做。

那麼,這裏咱們總結一下啓動的核心流程,以下所示:

  • 一、任務Task化,啓動邏輯抽象成Task(Task即對應一個個的初始化任務)。
  • 二、根據全部任務依賴關係排序生成一個有向無環圖:例如上述說到的推送SDK初始化任務須要依賴於獲取設備id的初始化任務,各個任務之間均可能存在依賴關係,因此將它們的依賴關係排序生成一個有向無環圖能將並行效率最大化
  • 三、多線程按照排序後的優先級依次執行:例如必須先初始化獲取設備id的初始化任務,才能去進行推送SDK的初始化任務。

異步啓動器優化實戰與源碼剖析

下面,咱們就來使用異步啓動器來在Application的onCreate方法中進行異步優化,代碼以下所示:

// 一、啓動器初始化
TaskDispatcher.init(this);
// 二、建立啓動器實例,這裏每次獲取的都是新對象
TaskDispatcher dispatcher = TaskDispatcher.createInstance();
// 三、給啓動器配置一系列的(異步/非異步)初始化任務並啓動啓動器
dispatcher
        .addTask(new InitAMapTask())
        .addTask(new InitStethoTask())
        .addTask(new InitWeexTask())
        .addTask(new InitBuglyTask())
        .addTask(new InitFrescoTask())
        .addTask(new InitJPushTask())
        .addTask(new InitUmengTask())
        .addTask(new GetDeviceIdTask())
        .start();
        
// 四、須要等待微信SDK初始化完成,程序才能往下執行
dispatcher.await();
複製代碼

這裏的 TaskDispatcher 就是咱們的啓動器調用類。首先,在註釋1處,咱們須要先調用TaskDispatcher的init方法進行啓動器的初始化,其源碼以下所示:

public static void init(Context context) {
    if (context != null) {
        sContext = context;
        sHasInit = true;
        sIsMainProcess = Utils.isMainProcess(sContext);
    }
}
複製代碼

能夠看到,僅僅是初始化了幾個基礎字段。接着,在註釋2處,咱們建立了啓動器實例,其源碼以下所示:

/**
 * 注意:這裏咱們每次獲取的都是新對象
 */
public static TaskDispatcher createInstance() {
    if (!sHasInit) {
        throw new RuntimeException("must call TaskDispatcher.init    first");
    }
    return new TaskDispatcher();
}
複製代碼

在createInstance方法的中咱們每次都會建立一個新的TaskDispatcher實例。而後,在註釋3處,咱們給啓動器配置了一系列的初始化任務並啓動啓動器,須要注意的是,這裏的Task既能夠是用於執行異步任務(子線程)的也能夠是用於執行非異步任務(主線程)。下面,咱們來分析下這兩種Task的用法,好比InitStethoTask這個異步任務的初始化,代碼以下所示:

/**
 * 異步的Task
*/
public class InitStethoTask extends Task {

    @Override
    public void run() {
        Stetho.initializeWithDefaults(mContext);
    }
}
複製代碼

這裏的InitStethoTask直接繼承自Task,Task中的runOnMainThread方法返回爲false,說明 task 是用於處理異步任務的task,其中的run方法就是Runnable的run方法。下面,咱們再看看另外一個用於初始化非異步任務的例子,例如用於微信SDK初始化的InitWeexTask,代碼以下所示:

/**
* 主線程執行的task
*/
public class InitWeexTask extends MainTask {

    @Override
    public boolean needWait() {
        return true;
    }

    @Override
    public void run() {
        InitConfig config = new InitConfig.Builder().build();
        WXSDKEngine.initialize((Application) mContext, config);
    }
}
複製代碼

能夠看到,它直接繼承了MainTask,MainTask的源碼以下所示:

public abstract class MainTask extends Task {

    @Override
    public boolean runOnMainThread() {
        return true;
    }
    
}
複製代碼

MainTask 直接繼承了Task,並僅僅是重寫了runOnMainThread方法返回了true,說明它就是用來初始化主線程中的非異步任務的

此外,咱們注意到InitWeexTask中還重寫了一個needWait方法並返回了true,其目的是爲了在某個時刻以前必須等待InitWeexTask初始化完成程序才能繼續往下執行,這裏的某個時刻指的就是咱們在Application的onCreate方法中的註釋4處的代碼所執行的地方:dispatcher.await(),其實現源碼以下所示:

/**
 * 須要等待的任務數
 */
private AtomicInteger mNeedWaitCount = new AtomicInteger();

/**
 * 調用了 await 還沒結束且須要等待的任務列表
 */
private List<Task> mNeedWaitTasks = new ArrayList<>();

private CountDownLatch mCountDownLatch;

private static final int WAITTIME = 10000;

@UiThread
public void await() {
    try {
        // 一、僅僅在測試階段才輸出需等待的任務列表數與任務名稱
        if (DispatcherLog.isDebug()) {
            DispatcherLog.i("still has " + mNeedWaitCount.get());
            for (Task task : mNeedWaitTasks) {
                DispatcherLog.i("needWait: " + task.getClass().getSimpleName());
            }
        }

        // 二、只要還有須要等待的任務沒有執行完成,就調用mCountDownLatch的await方法進行等待,這裏咱們設定超時時間爲10s
        if (mNeedWaitCount.get() > 0) {
            if (mCountDownLatch == null) {
                throw new RuntimeException("You have to call start() before call await()");
            }
            mCountDownLatch.await(WAITTIME, TimeUnit.MILLISECONDS);
        }
    } catch (InterruptedException e) {
    }
}
複製代碼

首先,在註釋1處,咱們僅僅只會在測試階段纔會輸出需等待的任務列表數與任務名稱。而後,在註釋2處,只要須要等待的任務數mNeedWaitCount大於0,即只要還有須要等待的任務沒有執行完成,就調用mCountDownLatch的await方法進行等待,注意咱們這裏設定了超時時間爲10s。當一個task執行完成後,不管它是異步仍是非異步的,最終都會執行到mTaskDispatcher的markTaskDone(mTask)方法,咱們看看它的實現源碼,以下所示:

/**
* 已經結束的Task
*/
private volatile List<Class<? extends Task>> mFinishedTasks = new ArrayList<>(100);

public void markTaskDone(Task task) {
    if (ifNeedWait(task)) {
        mFinishedTasks.add(task.getClass());
        mNeedWaitTasks.remove(task);
        mCountDownLatch.countDown();
        mNeedWaitCount.getAndDecrement();
    }
}
複製代碼

能夠看到,這裏每執行完成一個task,就會將mCountDownLatch的鎖計數減1,與此同時,也會將咱們的mNeedWaitCount這個原子整數包裝類的數量減1。

此外,咱們在前面說到了啓動器將各個任務之間的依賴關係抽象成了一個有向無環圖,在上面一系列的初始化代碼中,InitJPushTask是須要依賴於GetDeviceIdTask的,那麼,咱們怎麼告訴啓動器它們二者之間的依賴關係呢

這裏只須要在InitJPushTask中重寫dependsOn()方法,並返回包含GetDeviceIdTask的task列表便可,代碼以下所示:

/**
* InitJPushTask 須要在 getDeviceId 以後執行
*/
public class InitJPushTask extends Task {

    @Override
    public List<Class<? extends Task>> dependsOn() {
        List<Class<? extends Task>> task = new ArrayList<>();
        task.add(GetDeviceIdTask.class);
        return task;
    }

    @Override
    public void run() {
        JPushInterface.init(mContext);
        MyApplication app = (MyApplication) mContext;
        JPushInterface.setAlias(mContext, 0, app.getDeviceId());
    }
}
複製代碼

至此,咱們的異步啓動器就分析完畢了。下面咱們來看看如何高效地進行延遲初始化。

五、延遲初始化

一、常規方案:利用閃屏頁的停留時間進行部分初始化

  • new Handler().postDelayed()。
  • 界面UI展現後調用。

二、常規初始化痛點

  • 時機不容易控制:handler postDelayed指定的延遲時間很差估計。
  • 致使界面UI卡頓:此時用戶可能還在滑動列表。

三、延遲優化最優解:延遲啓動器

延遲啓動器源碼及使用demo地址

核心思想

利用IdleHandler特性,在CPU空閒時執行,對延遲任務進行分批初始化

延遲啓動器優化實戰與源碼剖析

延遲初始化啓動器的代碼很簡單,以下所示:

/**
 * 延遲初始化分發器
 */
public class DelayInitDispatcher {

    private Queue<Task> mDelayTasks = new LinkedList<>();

    private MessageQueue.IdleHandler mIdleHandler = new     MessageQueue.IdleHandler() {
        @Override
        public boolean queueIdle() {
            // 分批執行的好處在於每個task佔用主線程的時間相對
            // 來講很短暫,而且此時CPU是空閒的,這些能更有效地避免UI卡頓
            if(mDelayTasks.size()>0){
                Task task = mDelayTasks.poll();
                new DispatchRunnable(task).run();
            }
            return !mDelayTasks.isEmpty();
        }
    };

    public DelayInitDispatcher addTask(Task task){
        mDelayTasks.add(task);
        return this;
    }

    public void start(){
        Looper.myQueue().addIdleHandler(mIdleHandler);
    }

}
複製代碼

在DelayInitDispatcher中,咱們提供了mDelayTasks隊列用於將每個task添加進來,使用者只需調用addTask方法便可。當CPU空閒時,mIdleHandler便會回調自身的queueIdle方法,這個時候咱們能夠將task一個一個地拿出來並執行。這種分批執行的好處在於每個task佔用主線程的時間相對來講很短暫,而且此時CPU是空閒的,這樣能更有效地避免UI卡頓,真正地提高用戶的體驗。

至於使用就很是簡單了,咱們能夠直接利用SplashActivity的廣告頁停留時間去進行延遲初始化,代碼以下所示:

@Override
public void onWindowFocusChanged(boolean hasFocus) {
    super.onWindowFocusChanged(hasFocus);
    GlobalHandler.getInstance().getHandler().post((Runnable) () -> {
        if (hasFocus) {
            DelayInitDispatcher delayInitDispatcher = new DelayInitDispatcher();
            delayInitDispatcher.addTask(new InitOtherTask())
                    .start();
        }
    });
}
複製代碼

須要注意的是,能異步的task咱們會優先使用異步啓動器在Application的onCreate方法中加載(或者是必須在Application的onCreate方法完成前必須執行完的非異task務),對於不能異步的task,咱們能夠利用延遲啓動器進行加載。若是任務能夠到用時再加載,能夠使用懶加載的方式

延遲啓動器優點

  • 執行時機明確。
  • 緩解界面UI卡頓。
  • 真正提高用戶體驗。

六、Multidex預加載優化

咱們都知道,安裝或者升級後首次 MultiDex 花費的時間過於漫長,咱們須要進行Multidex的預加載優化。

一、優化步驟

  • 一、啓動時單獨開一個進程去異步進行Multidex的第一次加載,即Dex提取和Dexopt操做。
  • 二、此時,主進程Application進入while循環,不斷檢測Multidex操做是否完成。
  • 三、執行到Multidex時,則已經發現提取並優化好了Dex,直接執行。MultiDex執行完以後主進程Application繼續執行ContentProvider初始化和Application的onCreate方法。

Multidex優化Demo地址

注意

5.0以上默認使用ART,在安裝時已將Class.dex轉換爲oat文件了,無需優化,因此應判斷只有在主進程及SDK 5.0如下才進行Multidex的預加載。

二、dex-opt過程是怎樣的?

主要包括inline以及quick指令的優化。

那麼,inline是什麼?

使編譯器在函數調用處用函數體代碼代替函數調用指令。

inline的做用?

函數調用的轉移操做有必定的時間和空間方面的開銷,特別是對於一些函數體不大且頻繁調用的函數,解決其效率問題更爲重要,引入inline函數就是爲了解決這一問題。

inline又是如何進行優化的?

inline函數至少在三個方面提高了程序的時間性能:

  • 一、避免了函數調用必須執行的壓棧出棧等操做。
  • 二、因爲函數體代碼被移到函數調用處,編譯器能夠得到更多的上下文信息,並根據這些信息對函數體代碼和被調用者代碼進行更進一步的優化。
  • 三、若不使用inline函數,程序執行至函數調用處,須要轉而去執行函數體所在位置的代碼。通常函數調用位置和函數代碼所在位置在代碼段中並不相近,這樣很容易造成操做系統的缺頁中斷。操做系統須要把缺頁地址的代碼從硬盤移入內存,所需時間將成數量級增長。而使用inline函數則能夠減小缺頁中斷髮生的機會。

對於inline的使用,咱們應該注意的問題?

  • 一、因爲inline函數在函數調用處插入函數體代碼代替函數調用,若該函數在程序的不少位置被調用,有可能形成內存空間的浪費。
  • 二、通常程序的壓棧出棧操做也須要必定的代碼,這段代碼完成棧指針調整、參數傳遞、現場保護和恢復等操做。 若函數的函數體代碼量小於編譯器生成的函數壓棧出棧代碼,則能夠放心地定義爲inline,這個時候佔用內存空間反而會減少。而當函數體代碼大於函數壓棧出棧代碼時,將函數定義爲inline就會增長內存空間的使用。
  • 三、C++程序應該根據應用的具體場景、函數體大小、調用位置多少、函數調用的頻率、應用場景對時間性能的要求,應用場景對內存性能的要求等各方面因素合理決定是否認義inline函數。
  • 四、inline函數內不容許用循環語句和開關語句。

三、抖音BoostMultiDex優化

爲了完全解決MutiDex加載時間慢的問題,抖音團隊深刻挖掘了 Dalvik 虛擬機的底層系統機制,對 DEX 相關的處理邏輯進行了從新設計與優化,並推出了 BoostMultiDex 方案,它可以減小 80% 以上的黑屏等待時間,挽救低版本 Android 用戶的升級安裝體驗。若有興趣的同窗能夠看看這篇文章:抖音BoostMultiDex優化實踐:Android低版本上APP首次啓動時間減小80%

四、預加載SharedPreferences

能夠利用MultiDex預加載期間的這段CPU去預加載SharedPreferences。

注意

需重寫getApplicationContext返回this,不然此時可能獲取不到context。

七、類預加載優化

在Application中提早異步加載初始化耗時較長的類。

如何找到耗時較長的類?

替換系統的ClassLoader,打印類加載的時間,按需選取須要異步加載的類。

注意

  • Class.forName()只加載類自己及其靜態變量的引用類。
  • new 類實例 能夠額外加載類成員變量的引用類。

八、WebView啓動優化

  • 一、WebView首次建立比較耗時,須要預先建立WebView提早將其內核初始化。
  • 二、使用WebView緩存池,用到WebView的時候都從緩存池中拿,注意內存泄漏問題。
  • 三、本地離線包,即預置靜態頁面資源。

九、頁面數據預加載

在主頁空閒時,將其它頁面的數據加載好保存到內存或數據庫,等到打開該頁面時,判斷已經預加載過,就直接從內存或數據庫取數據並顯示。

十、啓動階段不啓動子進程

子進程會共享CPU資源,致使主進程CPU緊張。此外,在多進程狀況下必定要能夠在onCreate中去區分進程作一些初始化工做。

注意啓動順序

App onCreate以前是ContentProvider初始化。

十一、閃屏頁與主頁的繪製優化

  • 一、佈局優化。
  • 二、過渡繪製優化。

關於佈局與繪製優化能夠參考Android性能優化之繪製優化

5、啓動優化黑科技

一、啓動階段抑制GC

啓動時CG抑制,容許堆一直增加,直到手動或OOM中止GC抑制。(空間換時間)

前提條件

  • 一、設備廠商沒有加密內存中的Dalvik庫文件。
  • 二、設備廠商沒有改動Google的Dalvik源碼。

實現原理

  • 一、首先,在源碼級別找到抑制GC的修改方法,例如改變跳轉分支
  • 二、而後,在二進制代碼裏找到 A 分支條件跳轉的"指令指紋",以及用於改變分支的二進制代碼,假設爲 override_A
  • 三、最後,應用啓動後掃描內存中的 libdvm.so,根據"指令指紋"定位到修改位置,並使用 override_A 覆蓋

缺點

須要白名單覆蓋全部設備,但維護成本高。

二、CPU鎖頻

一個設備的CPU一般都是4核或者8核,可是應用在通常狀況下對CPU的利用率並不高,可能只有30%或者50%,若是咱們在啓動速度暴力拉伸CPU頻率,以此提升CPU的利用率,那麼,應用的啓動速度會提高很多。

在Android系統中,CPU相關的信息存儲在/sys/devices/system/cpu目錄的文件中,經過對該目錄下的特定文件進行寫值,實現對CPU頻率等狀態信息的更改

缺點

暴力拉伸CPU頻率,致使耗電量增長。

CPU工做模式

  • performance最高性能模式,即便系統負載很是低,cpu也在最高頻率下運行。
  • powersave省電模式,與performance模式相反,cpu始終在最低頻率下運行。
  • ondemand:CPU頻率跟隨系統負載進行變化
  • userspace:能夠簡單理解爲自定義模式,在該模式下能夠對頻率進行設定

CPU的工做頻率範圍

對應的文件有:

  • cpuinfo_max_freq
  • cpuinfo_min_freq
  • scaling_max_freq
  • scaling_min_freq

三、IO優化

  • 一、啓動過程不建議出現網絡IO
  • 二、爲了只解析啓動過程當中用到的數據,應選擇合適的數據結構,如將ArrayMap改形成支持隨機讀寫、延時解析的數據存儲結構以替代SharePreference。

這裏須要注意的是,須要考慮重度用戶的使用場景

補充加油站:Linux IO知識

一、磁盤高速緩存技術

利用內存中的存儲空間來暫存從磁盤中讀出的一系列盤塊中的信息。所以,磁盤高速緩存在邏輯上屬於磁盤,物理上則是駐留在內存中的盤塊。

其內存中分爲兩種形式:

  • 在內存中開闢一個單獨的存儲空間做爲磁速緩存,大小固定。
  • 把未利用的內存空間做爲一個緩沖池,供請求分頁系統和磁盤I/O時共享。

二、分頁

  • 存儲器管理的一種技術。
  • 能夠使電腦的主存使用存儲在輔助存儲器中的數據。
  • 操做系統會將輔助存儲器(一般是磁盤)中的數據分區成固定大小的區塊,稱爲「頁」(pages)。 當不須要時,將分頁由主存(一般是內存)移到輔助存儲器;當須要時,再將數據取回,加載主存中。
  • 相對於分段,分頁容許存儲器存儲於不連續的區塊以維持文件系統的整齊。
  • 分頁是磁盤和內存間傳輸數據塊的最小單位。

三、高速緩存/緩衝器

  • 都是介於高速設備和低速設備之間。
  • 高速緩存存放的是低速設備中某些數據的複製數據,而緩衝器則可同時存儲高低速設備之間的數據。
  • 高速緩存存放的是高速設備常常要訪問的數據。

四、linux同步IO:sync、fsync、msync、fdatasync

爲何要使用同步IO?

當數據寫入文件時,內核一般先將該數據複製到緩衝區高速緩存或頁面緩存中,若是該緩衝區還沒有寫滿,則不會將其排入輸入隊列,而是等待其寫滿或內核須要重用該緩衝區以便存放其餘磁盤塊數據時,再將該緩衝排入輸出隊列,最後等待其到達隊首時,才進行實際的IO操做—延遲寫。

延遲寫減小了磁盤讀寫次數,可是卻下降了文件內容的更新速度,可能會形成文件更新內容的丟失。爲了保證數據一致性,則需使用同步IO。

sync
  • sync函數只是將全部修改過的塊緩衝區排入寫隊列,而後就返回,它並不等待實際磁盤寫操做結束再返回。
  • 一般稱爲update的系統守護進程會週期性地(通常每隔30秒)調用sync函數。這就保證了按期沖洗內核的塊緩衝區。
fsync
  • fsync函數只對文件描述符filedes指定的單一文件起做用,而且等待磁盤IO寫結束後再返回。一般應用於須要確保將修改內容當即寫到磁盤的應用如數據庫。
  • 文件的數據和metadata一般存放在硬盤的不一樣地方,所以fsync至少須要兩次IO操做。
msync

若是當前硬盤的平均尋道時間是3-15ms,7200RPM硬盤的平均旋轉延遲大約爲4ms,所以一次IO操做的耗時大約爲10ms。

若是使用內存映射文件的方式進行文件IO(mmap),將文件的page cache直接映射到進程的地址空間,這時須要使用msync系統調用確保修改的內容徹底同步到硬盤之上。

fdatasync
  • fdatasync函數相似於fsync,但它隻影響文件的數據部分。而fsync還會同步更新文件的屬性。
  • 僅僅只在必要(如文件尺寸須要當即同步)的狀況下才會同步metadata,所以能夠減小一次IO操做。
日誌文件都是追加性的,文件尺寸一致在增大,如何利用好fdatasync減小日誌文件的同步開銷?

建立每一個log文件時先寫文件的最後一個page,將log文件擴展爲10MB大小,這樣即可以使用fdatasync,每寫10MB只有一次同步metadata的開銷。

五、磁盤IO與網絡IO

磁盤IO(緩存IO)

標準IO,大多數文件系統默認的IO操做。

  • 數據先從磁盤複製到內核空間的緩衝區,而後再從內核空間中的緩衝區複製到應用程序的緩衝區。
  • 讀操做:操做系統檢查內核的緩衝區有沒有須要的數據,若是已經有緩存了,那麼直接從緩存中返回;不然,從磁盤中返回,再緩存在操做系統的磁盤中。
  • 寫操做:將數據從用戶空間複製到內核空間中的緩衝區中,這時對用戶來講寫操做就已經完成,至於何時寫到磁盤中,由操做系統決定,除非顯示地調用了sync同步命令。

優勢

  • 在必定程度上分離了內核空間和用戶空間,保護系統自己安全。
  • 能夠減小磁盤IO的讀寫次數,從而提升性能。

缺點

DMA方式能夠將數據直接從磁盤讀到頁緩存中,或者將數據從頁緩存中寫回到磁盤,而不能在應用程序地址空間和磁盤之間進行數據傳輸,這樣,數據在傳輸過程當中須要在應用程序地址空間(用戶空間)和緩存(內核空間)中進行屢次數據拷貝操做,這帶來的CPU以及內存開銷是很是大的。

磁盤IO主要的延時(15000RPM硬盤爲例)

機械轉動延時(平均2ms)+ 尋址延時(2~3ms)+ 塊傳輸延時(0.1ms左右)=> 平均5ms

網絡IO主要延時

服務器響應延時 + 帶寬限制 + 網絡延時 + 跳轉路由延時 + 本地接收延時(通常爲幾十毫秒到幾千毫秒,受環境影響極大)

六、PIO與DMA

PIO

很早以前,磁盤和內存之間的數據傳輸是須要CPU控制的,也就是讀取磁盤文件到內存中時,數據會通過CPU存儲轉發,這種方式稱爲PIO。

DMA(直接內存訪問,Direct Memory Access)
  • 能夠不通過CPU而直接進行磁盤和內存的數據交換。
  • CPU只須要向DMA控制器下達指令,讓DMA控制器來處理數據的傳送便可。
  • DMA控制器經過系統總線來傳輸數據,傳送完畢再通知CPU,這樣就在很大程度上下降了CPU佔用率,大大節省了系統資源,而它的傳輸速度與PIO的差別並不明顯,而這主要取決於慢速設備的速度。

七、直接IO與異步IO

直接IO

應用程序直接訪問磁盤數據,而不通過內核緩衝區。以減小從內核緩衝區到用戶數據緩存的數據複製。

異步IO

當訪問數據的線程發出請求後,線程會接着去處理其它事情,而不是阻塞等待。

八、VFS(虛擬文件系統,Virtual File System)

能夠爲訪問文件系統的系統調用提供一個統一的抽象接口。

四、數據重排

Dex文件用到的類和APK裏面各類資源文件都比較小,讀取頻繁,且磁盤地址分佈範圍比較廣。咱們能夠利用Linux文件IO流程中的page cache機制將它們按照讀取順序從新排列在一塊兒,以減小真實的磁盤IO次數。

一、類重排

使用Facebook的 ReDex 的Interdex調整類在Dex中的排列順序。

二、資源文件重排

  • 一、最佳方案是修改內核源碼,實現統計、度量、自動化,其次也能夠使用Hook框架進行統計得出資源加載順序列表。
  • 二、最後,調整apk文件列表須要修改7zip源碼以支持傳入文件列表順序。

技術視野

  • 所謂的創新,不必定是要創造史無前例的東西,也能夠將已有的方案移植到新的平臺,並結合該平臺的特性落地,就是一個很大的創新。
  • 當咱們足夠熟悉底層的知識時,能夠利用系統的特性去作更加深層次的優化。

三、瞭解Hook框架

Xposed框架是什麼?

一個能夠不修改APK就影響程序運行的Hook框架。

原理

用自身實現的app_process替換掉系統/system/bin/app_process,加載一個額外的XposedBridge的jar包,用於將入口osZygoteInit.main()替換成XposedBridge.main()。以後,建立的Zygote進程和其子進程都是Hook過的了。

使用具體細節參見Xposed教程

五、類加載優化(Dalvik)

一、類預加載原理

對象第一次建立的時候,JVM首先檢查對應的Class對象是否已經加載。若是沒有加載,JVM會根據類名查找.class文件,將其Class對象載入。同一個類第二次new的時候就不須要加載類對象,而是直接實例化,建立時間就縮短了。

二、類加載優化過程

  • 在Dalvik VM加載類的時候會有一個類校驗過程,它須要校驗方法的每個指令。
  • 經過Hook去掉verify步驟 -> 幾十ms的優化
  • 最大優化場景在於首次安裝和覆蓋安裝時,在Dalvik平臺上,一個2MB的Dex正常須要350ms,將classVerifyMode設爲VERIFY_MODE_NONE後,只需150ms,節省超過50%時間。

ART比較複雜,Hook須要兼容幾個版本。並且在安裝時,大部分Dex已經優化好了,去掉ART平臺的verify只會對動態加載的Dex帶來一些好處。因此暫時不建議在ART平臺使用。

三、延伸:插件化和熱修復

它們在設計上都存在大量的Hook和私有API調用,共同的缺點有以下兩類問題。

一、穩定性較差

因爲廠商的兼容性、安裝失敗、ART加載時dex2oat失敗等緣由,仍是會有一些代碼和資源的異常。Android P推出的non-sdk-interface調用限制,之後適配只會愈來愈難,成本愈來愈高。

二、性能問題

用到一些黑科技致使底層Runtime的優化享受不到。如Tinker加載補丁後,啓動速度會下降5%~10%。

一、各項熱補丁技術的優缺點

缺點
  • 只針對單一客戶端版本,隨着版本差別變大補丁體積也會變大。
  • 不支持全部修改,如AndroidManifest。
  • 對代碼和資源的更新成功率沒法達到100%。
優勢
  • 下降開發成本,輕量而快速地升級。發佈補丁等同於發佈版本,也應該完整地執行測試與上線流程。
  • 遠端調試,只爲特定用戶發送補丁。
  • 數據統計,對同一批用戶更換補丁版本,可以更好地進行ABTest,獲得更精確的數據。

二、InstanceRun實現機制

Android官方使用熱補丁技術實現InstantRun。

應用構建流程

構建 -> 部署 -> 安裝 -> 重啓app -> 重啓activity

實現目標

儘量多的剔除沒必要要的步驟,而後提高必要步驟的速度。

InstantRun構建的三種方式

一、HotSwap

增量構建 -> 改變部署

場景:

適用於多數簡單的改變(包括一些方法實現的修改,或者變量值修改)。

二、Warm Swap

增量構建 -> 改變部署 -> activity重啓

場景:

通常是修改了resources。

三、Cold Swap

增量構建 -> 改變部署 -> 應用重啓 -> activity重啓

場景:

涉及結構性變化,如修改了繼承規則或方法簽名。

首次運行Instant Run,Gradle執行的操做
  • 在有Instant Run的環境下:一個新的App Server會被注入到App中,與Bytecode instrumentation協同監控代碼的變化。
  • 同時會有一個新的Application類,它注入了一個自定義類加載器。同時該Application會啓動咱們所需的新注入的App Server。因而,AndroidManifest會被修改來確保咱們能使用這個新的Application。
  • 使用的時候,它會經過決策,合理運用冷溫熱拔插來協助咱們大量地縮短構建程序的時間。
HotSwap原理

Android Studio monitors 運行着Gradle任務來生成增量.dex文件(dex對應着開發中的修改類),AS會提取這些.dex文件發送到App Server,而後部署到App。由於原來版本的類都裝載在運行中的程序了,Gradle會解釋更新好這些.dex文件,發送到App Server的時候,交給自定義的類加載器來加載.dex文件。 App Server會不斷地監聽是否須要重寫類文件,若是須要,任務會被立馬執行,新的更改便能當即被響應。

須要注意的是,此時InstantRun是不能回退的,必須重啓應用響應修改。

WarmSwap原理

由於資源文件是在Activity建立時加載,因此必須重啓Activity加載資源文件。

注意:AndroidManifest的值是在APK安裝的時候被讀取的,因此須要觸發一個完整的應用構建和部署。

ColdSwap原理

應用部署的時候,會把工程拆分紅十個部分,每一個部分都擁有本身的.dex文件,而後全部的類會根據包名被分配給相應的.dex文件。當ColdSwap開啓時,修改過的類所對應的的.dex文件,會重組生成新的.dex文件,而後再部署到設備上。

注意:應用多進程會被降級爲ColdSwap。

三、apk打包流程

manifest文件合併、打包,和res一塊兒被AAPT合併到APK中,同時項目代碼被編譯成字節碼,而後轉換成.dex文件,也被合併到APK中。

Android打包流程回顧,最後對於release簽名apk須要進行zipalign優化,它是指什麼?

在回答這個問題以前,咱們須要先了解下內存對齊(DSA,Data Structure Alignment):

各類類型的數據按照必定的規則在內存空間上排列,這就是對齊。
複製代碼

內存對齊的優點在於可以以空間換時間,減小數據存取指令週期,提高程序運行時的速度

編譯器內存字節對齊的原則是什麼?
  • 一、數據類型的自身對齊值就是其長度(64位 OS)。
  • 二、結構體或類的自身對齊值就是成員中自身對齊值最大的那個。須要起始地址必須是其相應有效對齊值的整數,並要求結構體的大小也爲該結構體有效對齊值的整數倍。

zipalign優化的最根本目的是幫助操做系統更高效地根據請求索引資源,使用resource-handling code統一將DSA限定爲4byte。

手動執行Align優化

利用build-tools文件夾下對應Android版本中的zipalign工具:

zipalign -v 4 source.apk androidres.apk
複製代碼

檢查當前APK是否已經執行過Align優化:

zipalign -c -v 4 androidres.apk
複製代碼

其中:

  • -c:檢查。
  • -v:表明詳細輸出。
  • 4:表明對齊爲4個字節。

四、AndFix

實現原理

native hook -> dalvik_repleaceMethod -> 沒法支持新增或刪除filed的狀況 -> 需修復特定問題

優勢
  • 當即生效
  • 補丁較小
缺點
  • 兼容性不佳
  • 開發不透明

五、Qzone

它是一個基於Android Dex分包方案。它將多個dex文件放入到app的classloader中,可是android dex拆包方案中的類是沒有重複的,若是classes.dex和classes1.dex中有重複的類,當用到這個重複的類時,系統會選擇哪一個類進行加載呢?

一個ClassLoader能夠包含多個dex文件,每一個dex文件是一個Elements,多個dex文件排列成有序的dexElements,當找類的時候,會按順序遍歷dex文件,而後從當前遍歷的dex文件中找類,若是找到則返回,若是找不到從下一個dex文件繼續查找。

因此,若是在不一樣的dex中有相同的類存在,那麼會優先選擇排在前面的dex文件的類。

Qzone熱補丁方案就是把有問題的類打包到一個dex(patch.dex)中去,而後把這個dex插入到Elements的最前面。

實現中遇到的問題

一、當其它dex文件中的類引用了patch.dex中的類時,會出現校驗錯誤。拆分dex的不少類都不是在同一個dex內的,怎麼沒有問題?

由於這個校驗有個前提,當引用類被打上了CLASS_ISPREVERIFIED標誌,那麼就會進行dex的校驗。

二、CLASS_ISPREVERIFIED標誌是何時被打上去的?

  • 在dex轉換成odex(dexopt過程)時,當apk在安裝的時候,apk中的classes.dex會被虛擬機(dexopt)優化成odex文件,而後纔會拿去執行。
  • 虛擬機在啓動的時候,會有許多的啓動參數,其中一項就是verify選項,當verify選項被打開時,doVerify變量爲true,那麼就會執行dvmVerifyClass進行類的校驗,若是校驗成功,這個類會被打上CLASS_ISPREVERIFIED標誌。
具體的校驗過程是怎麼樣的?

有兩步驗證:

一、驗證clazz -> directMethods方法,其包含如下方法:

  • static方法
  • private方法
  • 構造函數

二、clazz -> virtualMethods

  • 虛函數 = override方法

若是以上方法中直接引用到的類(第一層級關係,不會進行遞歸搜索)和clazz都在同一個dex中的話,那麼這個類就會被打上CLASS_ISPREVERIFED標誌。

爲了解決補丁方案中遇到的問題,因此必須從這些方法中入手,防止類被打上CLASS_ISPREVERIFIED標誌。空間的方案是往全部類的構造函數裏面插入一段代碼:

If (ClassVerifier.PREVENT_VERIFY) {
    System.out.println(AntilazyLoad.class);
}
複製代碼

其中AntilazyLoad類會被打包成單獨的hack.dex,這樣當安裝apk的時候,classes.dex中的類都會引用一個在不一樣dex中的AntilazyLoad類,這樣就防止類被打上了CLASS_ISPREVERIFILED標誌,只要沒被打上這個標誌的類均可以進行打補丁操做。

注意:

  • 一、在應用啓動進行加載時,AntilazyLoad類所在的dex包必須先加載進來,否則AntilazyLoad類會被標記爲不存在,即便後續加載了hack.dex包,那麼它也是不存在的。
  • 二、當在Application的onCreate中加載hack.dex時,Application不能插入上述代碼。

爲何要選擇構造函數?

由於他不增長方法數,一個類即便沒有顯示的構造函數,也有一個隱式的默認構造函數。

如何更高效地插入上述代碼?

能夠使用ASM/javaassist庫在編譯期間將相應的字節碼插入Class文件中。

Art的處理

Art採用了新的方式,插樁對代碼的執行效率沒有影響。可是補丁中的類出現修改類變量或者方法,可能會致使出現內存地址錯亂的狀況。

緣由:

dex2oat時fast*已經將類能肯定的各個地址寫死。若是運行時補丁包的地址出現改變,原始類去調用時就會出現地址錯亂。

解決方法:

將其父類以及調用類的全部類都加入到補丁包中。

虛擬機在安裝期間爲類打上CLASS_ISPREVERIFIED標誌是爲了什麼?

爲了提升性能。

禁用CLASS_ISPREVERIFIED是否會影響APP的性能?

因爲如今不少App都使用了MultiDex分包方案,這致使了不少類都沒有被打上這個標誌,因此此時禁用全部類打上CLASS_ISPREVERIFIED標誌對性能的影響不是很大。

如何有效地生成補丁包?
  • 一、在正式版本發佈的時候,會生成一份緩存文件,裏面記錄了全部class文件的MD5值,還有一份mapping混淆文件。
  • 二、在後續的版本中使用-applaymapping選項,應用正式版本的mapping文件,而後計算編譯完成的class文件的MD5和正式版本進行比較,把不相同的class文件打包成補丁包。
Qzone方案缺點

在補丁包大小與性能損耗上有必定的侷限性。

六、ASM字節碼插樁

插樁就是將一段代碼插入或者替換本來的代碼。 字節碼插樁就是在咱們的代碼編譯成字節碼(Class)後,在Android下生成dex以前修改Class文件,修改或者加強原有代碼邏輯的操做。

除了AspectJ、Javassist框架外,還有一個應用更爲普遍的ASM框架一樣也是字節碼操做框架,Instant Run包括Javassist就是藉助ASM來實現各自的功能。

能夠這樣理解Class字節碼與ASM之間的聯繫:

JSON對於GSON就相似於字節碼Class對於Javassist/ASM。
複製代碼
Android ASM自動埋點方案實踐

Android 1.5.0版本之後提供了Transform API,容許第三方Plugin在打包dex文件以前的編譯過程當中操做.class文件,咱們作的就是實現Transform進行.class文件遍歷拿到全部方法,修改完成後對文件進行替換。

大體的流程以下所示:

一、自動埋點追蹤,遍歷全部文件更換字節碼

AutoTransform -> transform -> inputs.each {TransformInput input -> input.jarInput.each { JarInput jarInput -> … } input.directoryInputs.each { DirectoryInput directoryInput -> … }}
複製代碼

二、Gradle插件實現

PluginEntry -> apply -> def android = project.extensions.getByType(AppExtension)

registerTransform(android) -> AutoTransform transform = new AutoTransform

android.registerTransform(transform)
複製代碼

三、使用ASM進行字節碼編寫

ASM框架核心類

  • ClassReader:讀取編譯後的.class文件。
  • ClassWriter:從新構建編譯後的類。
  • ClassVisitor:拜訪類成員信息。
  • AdviceAdapter:實現MethodVisitor接口,拜訪方法的信息。

一、visit -> 在ClassVisitor中根據判斷是不是實現View$OnClickListener接口的類,只有知足條件的類纔會遍歷其中的方法進行操做。

二、在MethodVisitor中對該方法進行修改

visitAnnotation -> onMethodEnter -> onMethodExit
複製代碼

三、先在java文件中編寫要插入的代碼,而後使用ASM插件查看對應的字節碼,根據其用ASM提供的Api一一對應地把代碼填進來便可。

關於編譯插樁的知識,筆者後面會有一系列的文章進行深刻講解,具體的文章目錄能夠在這裏查看

七、Tinker

原理
  • 全量替換新的Dex
  • 在編譯時經過新舊兩個Dex生成差別patch.dex。在運行時,將差別patch.dex從新跟原始安裝包的舊Dex還原爲新的Dex。因爲比較耗費時間與內存,放在後臺進程:patch中,爲了補丁包儘量小,微信自研了DexDiff算法,它深度利用Dex的格式來減小差別的大小。

DexDiff的粒度是Dex格式的每一項,BsDiff的粒度是文件,AndFix/Qzone的粒度爲class。

缺點
  • 一、佔用Rom體積,1.5倍所修改Dex大小 = Dex.jar + dexopt文件。
  • 二、一個額外的合成過程,合成時間長短和額外的內存消耗也會影響最終的成功率。
熱補丁方案對比

若不care性能損耗與補丁包大小,Qzone是最簡單且成功率最高的方案。

八、完善的熱補丁系統構建

1、網絡通道

負責將補丁包交付給用戶,包括特定用戶和全量用戶。

一、pull通道

在登陸/24小時等時機,經過pull方式查詢後臺是否有對應的補丁包更新。

二、指定版本的push通道

在緊急狀況下,咱們能夠在一個小時內向全部用戶下發補丁包更新。

三、指定特定用戶的push通道

對特定用戶或用戶組作遠程調試。

2、上線與管理平臺

快速上線,管理歷史記錄,以及監控補丁的運行狀況。

六、保活

一、廠商合做

二、微信Hardcoder

構建了App與系統(ROM)之間可靠的通訊框架,讓系統知道App的需求。

原理

  • 一、其實質是讓App跨過Framework直接跟廠商ROM通訊
  • 二、分爲Client端和Server端,Server端由廠商系統側自行實現
  • 三、它們直接採用 LocalSocket 方式,Hardcoder是 Native 實現的,使用了Linux的Socket接口實現了一套本身的LocalSocket。

性能提高有多少?

平均10%~30%。

三、OPPO Hyper Boost加速引擎

一種優化資源調度的技術。

原理

讓應用程序與系統資源實現實時"雙向對話"。當來自應用和遊戲程序的不一樣場景和用戶行爲被Hyper Boost識別後,手機會智能地匹配到合理的系統資源,讓手機SoC的CPU、GPU、ISP、DSP提供的運算資源更加合理地利用,從而讓用戶使用手機更加流暢

6、啓動優化的常見問題

一、啓動優化是怎麼作的?

  • 一、分析現狀、確認問題
  • 二、針對性優化(先歸納,引導其深刻)
  • 三、長期保持優化效果

在某一個版本以後呢,咱們會發現這個啓動速度變得特別慢,同時用戶給咱們的反饋也愈來愈多,因此,咱們開始考慮對應用的啓動速度來進行優化。而後,咱們就對啓動的代碼進行了代碼層面的梳理,咱們發現應用的啓動流程已經很是複雜,接着,咱們經過一系列的工具來確認是否在主線程中執行了太多的耗時操做。

咱們通過了細查代碼以後,發現應用主線程中的任務太多,咱們就想了一個方案去針對性地解決,也就是進行異步初始化。(引導=>第2題) 而後,咱們還發現了另一個問題,也能夠進行鍼對性的優化,就是在咱們的初始化代碼當中有些的優先級並非那麼高,它能夠不放在Application的onCreate中執行,而徹底能夠放在以後延遲執行的,由於咱們對這些代碼進行了延遲初始化,最後,咱們還結合了idealHandler作了一個更優的延遲初始化的方案,利用它能夠在主線程的空閒時間進行初始化,以減小啓動耗時致使的卡頓現象。作完這些以後,咱們的啓動速度就變得很快了。

最後,我簡單說下咱們是怎麼長期來保持啓動優化的效果的。首先,咱們作了咱們的啓動器,而且結合了咱們的CI,在線上加上了不少方面的監控。(引導=> 第4題)

二、是怎麼異步的,異步遇到問題沒有?

  • 一、體現演進過程
  • 二、詳細介紹啓動器

咱們最初是採用的普通的一個異步的方案,即new Thread + 設置線程優先級爲後臺線程的方式在Application的onCreate方法中進行異步初始化,後來,咱們使用了線程池、IntentService的方式,可是,在咱們應用的演進過程中,發現代碼會變得不夠優雅,而且有些場景很是很差處理,好比說多個初始化任務直接的依賴關係,好比說某一個初始化任務須要在某一個特定的生命週期中初始化完成,這些都是使用線程池、IntentService沒法實現的。因此說,咱們就開始思考一個新的解決方案,它可以完美地解決咱們剛剛所遇到的這些問題。

這個方案就是咱們目前所使用的啓動器,在啓動器的概念中,咱們將每個初始化代碼抽象成了一個Task,而後,對它們進行了一個排序,根據它們之間的依賴關係排了一個有向無環圖,接着,使用一個異步隊列進行執行,而且這個異步隊列它和CPU的核心數是強烈相關的,它可以最大程度地保證咱們的主線程和別的線程都可以執行咱們的任務,也就是你們幾乎均可以同時完成。

三、啓動優化有哪些容易忽略的注意點?

  • 一、cpu time與wall time
  • 二、注意延遲初始化的優化
  • 三、介紹下黑科技

首先,在CPU Profiler和Systrace中有兩個很重要的指標,即cpu time與wall time,咱們必須清楚cpu time與wall time之間的區別,wall time指的是代碼執行的時間,而cpu time指的是代碼消耗CPU的時間,鎖衝突會形成二者時間差距過大。咱們須要以cpu time來做爲咱們優化的一個方向。

其次,咱們不只只追求啓動速度上的一個提高,也須要注意延遲初始化的一個優化,對於延遲初始化,一般的作法是在界面顯示以後纔去進行加載,可是若是此時界面須要進行滑動等與用戶交互的一系列操做,就會有很嚴重的卡頓現象,所以咱們使用了idealHandler來實現cpu空閒時間來執行耗時任務,這極大地提高了用戶的體驗,避免了因啓動耗時任務而致使的頁面卡頓現象。

最後,對於啓動優化,還有一些黑科技,首先,就是咱們採用了類預先加載的方式,咱們在MultiDex.install方法以後起了一個線程,而後用Class.forName的方式來預先觸發類的加載,而後當咱們這個類真正被使用的時候,就不用再進行類加載的過程了。同時,咱們再看Systrace圖的時候,有一部分手機其實並無給咱們應用去跑滿cpu,好比說它有8核,可是卻只給了咱們4核等這些狀況,而後,有些應用對此作了一些黑科技,它會將cpu的核心數以及cpu的頻率在啓動的時候去進行一個暴力的提高。

四、版本迭代致使的啓動變慢有好的解決方式嗎?

  • 啓動器
  • 結合CI
  • 監控完善

這種問題其實咱們以前也遇到過,這的確很是難以解決。可是,咱們後面對此進行了反覆的思考與嘗試,終於找到了一個比較好的解決方式。

首先,咱們使用了啓動器去管理每個初始化任務,而且啓動器中每個任務的執行都是被其自動進行分配的,也就是說這些自動分配的task咱們會盡可能保證它會平均分配在咱們每個線程當中的,這和咱們普通的異步是不同的,它能夠很好地緩解咱們應用的啓動變慢。

其次,咱們還結合了CI,好比說,咱們如今限制了一些類,如Application,若是有人修改了它,咱們不會讓這部分代碼合併到主幹分支或者是修改以後會有一些內部的工具如郵件的形式發送到我,而後,我就會和他確認他加的這些代碼究竟是耗時多少,可否異步初始化,不能異步的話就考慮延遲初始化,若是初始化時間太長,則能夠考慮是否能進行懶加載,等用到的時候再去使用等等。

而後,咱們會將問題儘量地暴露在上線以前。同時,咱們真正已經到了線上的一個環境下時,咱們進行了監控的一個完善,咱們不只是監控了App的整個的啓動時間,同時呢,咱們也將每個生命週期都進行了一個監控。好比說Application的onCreate與onAttachBaseContext方法的耗時,以及這兩個生命週期之間間隔的時間,咱們都進行了一個監控,若是說下一次咱們發現了這個啓動速度變慢了,咱們就能夠去查找究竟是哪個環節變慢了,咱們會和之前的版本進行對比,對比完成以後呢,咱們就能夠來找這一段新加的代碼。

7、總結

一、優化總方針

  • 異步、延遲、懶加載
  • 技術、業務相結合

二、注意事項

一、cpu time 和 wall time

  • wall time(代碼執行時間)與cpu time(代碼消耗CPU時間),鎖衝突會形成這二者時間差距過大
  • cpu time纔是優化方向,應盡力按照systrace的cpu time和wall time跑滿cpu

二、監控的完善

  • 線上監控多階段時間(App、Activity、生命週期間隔時間)。
  • 處理聚合看趨勢。
  • 收斂啓動代碼修改權限。
  • 結合CI修改啓動代碼須要Review通知。

至此,探索Android啓動速度優化的旅途也應該告一段落了,若是你耐心讀到最後的話,會發現要想極致地提高App的性能,須要有必定的技術廣度,如咱們引入了始於後端的AOP編程來實現無侵入式的函數插樁,也須要有必定的深度,從前面的探索之旅來看,咱們前後涉及了Framework層、Native層、Dalvik虛擬機、甚至是Linux IO和文件系統相關的原理。所以,我想說,Android開發並不簡單,即便是App層面的性能優化這一知識體系,也是須要咱們不斷地加深自身知識的深度和廣度

ps:在文章的黑科技部分涉及到了許多基礎架構研發領域的知識,這部分沒法理解的同窗不要灰心,先了解便可,
    筆者以後的文章都會一一詳細講解。
複製代碼
參考連接:

一、Android開發高手課之啓動優化

二、支付寶客戶端架構解析:Android 客戶端啓動速度優化之「垃圾回收」

三、支付寶 App 構建優化解析:經過安裝包重排布優化 Android 端啓動性能

四、Facebook Redex字節碼優化工具

五、微信Android熱補丁實踐演進之路

六、安卓App熱補丁動態修復技術介紹

七、Dalvik Optimization and Verification With dexopt

八、微信在Github開源了Hardcoder,對Android開發者有什麼影響?

九、歷時三年研發,OPPO 的 Hyper Boost 引擎如何對系統、遊戲和應用實現加速?

十、抱歉,Xposed真的能夠隨心所欲

十一、牆上時鐘時間 ,用戶cpu時間 ,系統cpu時間的理解

十二、《Android應用性能優化最佳實踐》

1三、必知必會 | Android 性能優化的方面方面都在這兒

1四、極客時間之Top團隊大牛帶你玩轉Android性能分析與優化

1五、啓動器源碼

1六、MultiDex優化源碼

1七、使用gradle自動化增長Trace Tag

Contanct Me

● 微信:

歡迎關注個人微信:bcce5360

● 微信羣:

微信羣若是不能掃碼加入,麻煩你們想進微信羣的朋友們,加我微信拉你進羣。

● QQ羣:

2千人QQ羣,Awesome-Android學習交流羣,QQ羣號:959936182, 歡迎你們加入~

About me

很感謝您閱讀這篇文章,但願您能將它分享給您的朋友或技術羣,這對我意義重大。

但願咱們能成爲朋友,在 Github掘金上一塊兒分享知識。

相關文章
相關標籤/搜索