前言
成為一名優秀的Android開發,需要一份完備的知識體系,在這裡,讓我們一起成長為自己所想的那樣~。
在性能優化的整個知識體系中,最重要的就是穩定性優化,在上一篇文章
《深入探索Android穩定性優化》
我們已經深入探索了Android穩定性優化的疆域。那麼,除了穩定性以外,對于性能緯度來說,哪個方面的性能是最重要的呢?毫無疑問,就是 應用的啟動速度。下面,就讓我們揚起航帆,一起來逐漸深入探索Android啟動速度優化的奧秘。
思維導圖大綱
目錄
- 一、啟動優化的意義
- 二、應用啟動流程
- 1 、應用啟動的類型
- 2、冷啟動分析及其優化方向
- 三、啟動耗時檢測
- 1、檢視Logcat
- 2、adb shell
- 3、代碼打點(函數插樁)
- 4、AOP(Aspect Oriented Programming) 打點
- 5、啟動速度分析工具 — TraceView
- 6、啟動速度分析工具 — Systrace
- 7、啟動監控
- 四、啟動優化正常方案
- 1、主題切換
- 2、第三方庫懶加載
- 3、異步初始化預備知識-線程優化
- 4、異步初始化
- 5、延遲初始化
- 6、Multidex預加載優化
- 7、類預加載優化
- 8、WebView啟動優化
- 9、頁面資料預加載
- 10、啟動階段不啟動子程序
- 11、閃屏頁與首頁的繪制優化
一、啟動優化的意義
如果我們去一家餐廳吃飯,在點餐的時候等了半天都沒有服務人員過來,可能就沒有耐心等待直接走了。
對于App來說,也是同樣如此,如果使用者點選App後,App半天都打不開,使用者就可能失去耐心解除安裝應用。
啟動速度是使用者對我們App的第一體驗,打開應用後才能去使用其中提供的強大功能,就算我們應用的内部界面設計的再精美,功能再強大,如果啟動速度過慢,使用者第一印象就會很差。
是以,拯救App的啟動速度,迫在眉睫。
二、應用啟動流程
1 、應用啟動的類型
應用啟動的類型總共分為如下三種:
- 冷啟動
- 熱啟動
- 溫啟動
下面,我們來詳細分析下各個啟動類型的特點及流程。
冷啟動
從點選應用圖示到UI界面完全顯示且使用者可操作的全部過程。
特點
耗時最多,衡量标準。
啟動流程
Click Event -> IPC -> Process.start -> ActivityThread -> bindApplication -> LifeCycle -> ViewRootImpl
首先,使用者進行了一個點選操作,這個點選事件它會觸發一個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進行布局、測量和繪制。
2、冷啟動分析及其優化方向
冷啟動涉及的相關任務
冷啟動之前
- 首先,會啟動App
- 然後,加載空白Window
- 最後,建立程序
需要注意的是,這些都是系統的行為,一般情況下我們是無法直接幹預的。
随後任務
- 首先,建立Application
- 啟動主線程
- 建立MainActivity
- 加載布局
- 布置螢幕
- 首幀繪制
通常到了界面首幀繪制完成後,我們就可以認為啟動已經結束了。
優化方向
我們的優化方向就是 Application和Activity的生命周期 這個階段,因為這個階段的時機對于我們來說是可控的。
三、啟動耗時檢測
1、檢視Logcat
在Android Studio Logcat中過濾關鍵字“Displayed”,可以看到對應的冷啟動耗時日志。
2、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初始化到界面顯示 的過程。
特點:
- 1、線下使用友善,不能帶到線上。
- 2、非嚴謹、精确時間。
3、代碼打點(函數插樁)
可以寫一個統計耗時的工具類來記錄整個過程的耗時情況。其中需要注意的有:
- 在上傳資料到伺服器時建議根據使用者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);
}
特點
精确,可帶到線上,但是代碼有侵入性,修改成本高。
注意事項
- 1、在上傳資料到伺服器時建議根據使用者ID的尾号來抽樣上報。
- 2、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隻是界面繪制的首幀時機,但是清單中的資料是需要從網絡中下載下傳得到的,是以應該以清單的首幀資料作為啟動結束點。
4、AOP(Aspect Oriented Programming) 打點
面向切面程式設計,通過預編譯和運作期動态代理實作程式功能統一維護的一種技術。
1、作用
利用AOP可以對業務邏輯的各個部分進行隔離,進而使得業務邏輯各部分之間的耦合性降低,提高程式的可重用性,同時大大提高了開發效率。
2、AOP核心概念
1、橫切關注點
對哪些方法進行攔截,攔截後怎麼處理。
2、切面(Aspect)
類是對物體特征的抽象,切面就是對橫切關注點的抽象。
3、連接配接點(JoinPoint)
被攔截到的點(方法、字段、構造器)。
4、切入點(PointCut)
對JoinPoint進行攔截的定義。
5、通知(Advice)
攔截到JoinPoint後要執行的代碼,分為前置、後置、環繞三種類型。
3、準備:接入AspectJx進行切面編碼
首先,為了在Android使用AOP埋點需要引入AspectJ,在項目根目錄的build.gradle下加入:
然後,在app目錄下的build.gradle下加入:
apply plugin: 'android-aspectjx'
implement 'org.aspectj:aspectjrt:1.8.+'
4、AOP埋點實戰
JoinPoint一般定位在如下位置
- 1、函數調用
- 2、擷取、設定變量
- 3、類初始化
使用PointCut對我們指定的連接配接點進行攔截,通過Advice,就可以攔截到JoinPoint後要執行的代碼。Advice通常有以下幾種類型:
- 1、Before:PointCut之前執行
- 2、After:PointCut之後執行
- 3、Around:PointCut之前、之後分别執行
首先,我們舉一個小栗子:
@Before("execution(* android.app.Activity.on**(..))")
public void onActivityCalled(JoinPoint joinPoint) throws Throwable {
...
}
在 execution 中的是一個比對規則,第一個 * 代表比對任意的方法傳回值,後面的文法代碼比對所有Activity中on開頭的方法。
其中execution是處理Join Point的類型,在AspectJx中共有兩種類型,如下所示:
- 1、call:插入在函數體裡面
- 2、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方法執行目标方法。
5、總結AOP特性
- 1、無侵入性
- 2、修改友善,建議使用
5、啟動速度分析工具 — TraceView
1、使用方式
- 1、代碼中添加:Debug.startMethodTracing()、檢測方法、Debug.stopMethodTracing()。(需要使用adb pull将生成的.trace檔案導出到電腦,然後使用Android Studio的Profiler進行加載)
- 2、打開 Profiler -> CPU -> 點選 Record -> 點選 Stop -> 檢視Profiler下方Top Down/Bottom Up 區域,以找出耗時的熱點方法。
2、Profile CPU
使用 Profile 的 CPU 子產品可以幫我們快速找到耗時的熱點方法,下面,我們來詳細來分析一下這個子產品。
1、Trace types
Trace types 有四種,如下所示。
1、Trace Java Methods
會記錄每個方法的時間、CPU資訊。對運作時性能影響較大。
2、Sample Java Methods
相比于Trace Java Methods會記錄每個方法的時間、CPU資訊,它會在應用的Java代碼執行期間頻繁捕獲應用的調用堆棧,對運作時性能的影響比較小,能夠記錄更大的資料區域。
3、Sample C/C++ Functions
需部署到Android 8.0及以上裝置,内部使用simpleperf跟蹤應用的native代碼,也可以指令行使用simpleperf。
4、Trace System Calls
- 檢查應用與系統資源的互動情況。
- 檢視所有核心的CPU瓶頸。
- 内部采用systrace,也可以使用systrace指令。
2、Event timeline
用于顯示應用程式在其生命周期中轉換不同狀态的活動,如使用者互動、螢幕旋轉事件等。
3、CPU timeline
用于顯示應用程式 實時CPU使用率、其它程序實時CPU使用率、應用程式使用的線程總數。
4、Thread activity timeline
列出應用程式程序中的每個線程,并使用了不同的顔色在其時間軸上訓示其活動。
- 綠色:線程處于活動狀态或準備好使用CPU。
- 黃色:線程正等待IO操作。(重要)
- 灰色:線程正在睡眠,不消耗CPU時間。
5、檢查跟蹤資料視窗
Profile提供的檢查跟蹤資料視窗有四種,如下所示:
1、Call Chart
提供函數跟蹤資料的圖形表示形式。
- 水準軸:表示調用的時間段和時間。
- 垂直軸:顯示被調用方。
- 橙色:系統API。
- 綠色:應用自有方法。
- 藍色:第三方API(包括Java API)。
提示
右鍵點選 Jump to source 跳轉至指定函數。
2、Flame Chart
将具有相同調用方順序的完全相同的方法收集起來。
- 水準軸:執行每個方法的相對時間量。
- 垂直軸:顯示被調用方。
使用技巧
看頂層的哪個函數占據的寬度最大(表現為平頂),可能存在性能問題。
3、Top Down
- 遞歸調用清單,提供self、children、total時間和比率來表示被調用的函數資訊。
- Flame Chart是Top Down清單資料的圖形化。
4、Bottom Up
- 展開函數會顯示其調用方。
- 按照消耗CPU時間由多到少的順序對函數排序。
注意事項
我們在檢視上面4個跟蹤資料的區域時,應該注意右側的兩個時間,如下所示:
- Wall Clock Time:程式執行時間。
- Thread Time:CPU執行的時間。
3、TraceView小結
特點
- 1、圖形的形式展示執行時間、調用棧等。
- 2、資訊全面,包含所有線程。
- 3、運作時開銷嚴重,整體都會變慢,得出的結果并不真實。
- 4、找到最耗費時間的路徑:Flame Chart、Top Down。
- 5、找到最耗費時間的節點:Bottom Up。
作用
主要做熱點分析,用來得到以下兩種資料:
- 單次執行最耗時的方法。
- 執行次數最多的方法。
6、啟動速度分析工具 — Systrace
1、使用方式:代碼插樁
首先,我們可以定義一個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一欄可以看到核心的系統方法時間區域和我們自己使用代碼插樁捕獲的方法時間區域。
2、Systrace原理
- 首先,在系統的一些關鍵鍊路(如SystemServcie、虛拟機、Binder驅動)插入一些資訊(Label)。
- 然後,通過Label的開始和結束來确定某個核心過程的執行時間,并把這些Label資訊收集起來得到系統關鍵路徑的運作時間資訊,最後得到整個系統的運作性能資訊;
其中,Android Framework 裡面一些重要的子產品都插入了label資訊,使用者App中也可以添加自定義的Lable。
3、Systrace小結
特點
- 結合Android核心的資料,生成Html報告。
- 系統版本越高,Android Framework中添加的系統可用Label就越多,能夠支援和分析的系統子產品也就越多。
- 必須手動縮小範圍,會幫助你加速收斂問題的分析過程,進而快速地定位和解決問題。
作用
- 主要用于分析繪制性能方面的問題。
- 分析系統關鍵方法和應用方法耗時。
7、啟動監控
1、實驗室監控:視訊錄制
- 80%繪制
- 圖像識别
注意
覆寫高中低端機型不同的場景。
2、線上監控
目标
需要準确地統計啟動耗時。
1、啟動結束的統計時機
是否是使用界面顯示且使用者真正可以操作的時間作為啟動結束時間。
2、啟動時間扣除邏輯
閃屏、廣告和新手引導這些時間都應該從啟動時間裡扣除。
3、啟動排除邏輯
Broadcast、Server拉起,啟動過程進入背景都需要排除統計。
4、使用什麼名額來衡量啟動速度的快慢?
平均啟動時間的問題
一些體驗很差的使用者很可能被平均了。
建議的名額
- 1、快開慢開比
如2s快開比,5s慢開比,可以看到有多少比例的使用者體驗好,多少比例的使用者比較糟糕。
- 2、90%使用者的啟動時間
如果90%使用者的啟動時間都小于5s,那麼90%區間的啟動耗時就是5s。
5、啟動的類型有哪幾種?
- 首次安裝啟動
- 覆寫安裝啟動
- 冷啟動(名額)
- 熱啟動(反映程式的活躍或保活能力)
借鑒Facebook的 profilo 工具原理,對啟動整個流程進行耗時監控,在背景對不同的版本做自動化對比,監控新版本是否有新增耗時的函數。
四、啟動優化正常方案
啟動過程中的常見問題
- 1、點選圖示很久都不響應:預覽視窗被禁用或設定為透明。
- 2、首頁顯示太慢:初始化任務太多。
- 3、首頁顯示後無法進行操作:太多延遲初始化任務占用主線程CPU時間片。
優化區域
Application、Activity建立以及回調等過程。
1、主題切換
使用Activity的windowBackground主題屬性預先設定一個啟動圖檔(layer-list實作),在啟動後,在Activity的onCreate()方法中的super.onCreate()前再setTheme(R.style.AppTheme)。
優點
- 使用簡單。
- 避免了啟動白屏和點選啟動圖示不響應的情況。
缺點
- 治标不治本,表面上産生一種快的感覺。
- 對于中低端機,總的閃屏時間會更長,建議隻在Android6.0/7.0以上才啟用“預覽閃屏”方案,讓手機性能好的使用者可以有更好的體驗。
2、第三方庫懶加載
按需初始化,特别是針對于一些應用啟動時不需要初始化的庫,可以等到用時才進行加載。
3、異步初始化預備知識-線程優化
1、Android線程排程原理剖析
線程排程原理
- 1、任意時刻,隻有一個線程占用CPU,處于運作狀态。
- 2、多線程并發,輪流擷取CPU使用權。
- 3、JVM負責線程排程,按照特定機制配置設定CPU使用權。
線程排程模型
1、分時排程模型
輪流擷取、均分CPU。
2、搶占式排程模型
優先級高的擷取。
如何幹預線程排程?
設定線程優先級。
Android線程排程
1、nice值
- Process中定義。
- 值越小,優先級越高。
- 預設是THREAD_PRIORITY_DEFAUT,0。
2、cgroup
它是一種更嚴格的群組排程政策,主要分為如下兩種類型:
- 背景group(預設)。
- 前台group,保證前台線程可以擷取到更多的CPU
注意點
- 線程過多會導緻CPU頻繁切換,降低線程運作效率。
- 正确認識任務重要性以決定使用哪種線程優先級。
- 優先級具有繼承性。
2、Android異步方式
1、Thread
- 最簡單、常見的異步方式。
- 不易複用,頻繁建立及銷毀開銷大。
- 複雜場景不易使用。
2、HandlerThread
- 自帶消息循環的線程。
- 串行執行。
- 長時間運作,不斷從隊列中擷取任務。
3、IntentService
- 繼承自Service在内部建立HandlerThread。
- 異步,不占用主線程。
- 優先級較高,不易被系統Kill。
4、AsyncTask
- Android提供的工具類。
- 無需自己處理線程切換。
- 需注意版本不一緻問題(API 14以上解決)
5、線程池
- Java提供的線程池。
- 易複用,減少頻繁建立、銷毀的時間。
- 功能強大,如定時、任務隊列、并發數控制等。
6、RxJava
由強大的排程器Scheduler集合提供。
不同類型的Scheduler:
- IO
- Computation
異步方式總結
- 推薦度:從後往前排列。
- 正确場景選擇正确的方式。
3、Android線程優化實戰
線程使用準則
- 1、嚴禁使用new Thread方式。
- 2、提供基礎線程池供各個業務線使用,避免各個業務線各自維護一套線程池,導緻線程數過多。
- 3、根據任務類型選擇合适的異步方式:優先級低,長時間執行,HandlerThread;定時執行耗時任務,線程池。
- 4、建立線程必須命名,以友善定位線程歸屬,在運作期 Thread.currentThread().setName 修改名字。
- 5、關鍵異步任務監控,注意異步不等于不耗時,建議使用AOP的方式來做監控。
- 6、重視優先級設定(根據任務具體情況),Process.setThreadPriority() 可以設定多次。
4、如何鎖定線程建立者
鎖定線程建立背景
- 項目變大之後收斂線程。
- 項目源碼、三方庫、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找到線程建立資訊,根據堆棧資訊跟相關業務方溝通解決方案。
5、線程收斂優雅實踐初步
線程收斂正常方案
- 根據線程建立堆棧考量合理性,使用同一線程庫。
- 各業務線下掉自己的線程庫。
問題:基礎庫怎麼使用線程?
直接依賴線程庫,但問題在于線程庫更新可能會導緻基礎庫更新。
基礎庫優雅使用線程
- 基礎庫内部暴露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);
}
}
6、線程優化核心問題
1、線程使用為什麼會遇到問題?
項目發展階段忽視基礎設施建設,沒有采用統一的線程池,導緻線程數量過多。
表現形式
異步任務執行太耗時,導緻主線程卡頓。
問題原因
- 1、Java線程排程是搶占式的,線程優先級比較重要,需要區分。
- 2、沒有區分IO和CPU密集型任務,導緻主線程搶不到CPU。
2、怎麼在項目中對線程進行優化?
核心:線程收斂
- 通過Hook方式找到對應線程的堆棧資訊,和業務方讨論是否應該單獨起一個線程,盡可能使用統一線程池。
- 每個基礎庫都暴露一個設定線程池的方法,以避免線程庫更新導緻基礎庫需要更新的問題。
- 統一線程池應注意IO、CPU密集型任務區分。
- 其它細節:重要異步任務統計耗時、注重異步任務優先級和線程名的設定。
4、異步初始化
1、核心思想
子線程分擔主線程任務,并行減少時間。
2、異步優化注意點
- 1、不符合異步要求。
- 2、需要在某個階段完成(采用CountDownLatch確定異步任務完成後才到下一個階段)。
- 3、如出現主線程要使用時還沒初始化則在此次使用前初始化。
- 4、區分CPU密集型和IO密集型任務。
3、異步初始化方案演進
- 1、new Thread
- 2、IntentService
- 3、線程池(合理配置并選擇CPU密集型和IO密集型線程池)
- 4、異步啟動器
4、異步優化最優解:異步啟動器
異步啟動器源碼及使用demo位址
正常異步優化痛點
- 1、代碼不優雅:例如使用線程池實作多個并行異步任務時會有多個executorService.submit代碼塊。
- 2、場景不好處理:各個初始化任務之間存在依賴關系,例如推送sdk的初始化任務需要依賴于擷取裝置id的初始化任務。此外,有些任務是需要在某些特定的時候就初始化完成,例如需要在Application的onCreate方法執行完之前就初始化完成。
- 3、維護成本高。
啟動器核心思想
充分利用CPU多核,自動梳理任務順序。
啟動器流程
啟動器的流程圖如下所示:
啟動器的主題流程為上圖中的中間區域,即主線程與并發兩個區域塊。需要注意的是,在上圖中的 head task與tail task 并不包含在啟動器的主題流程中,它僅僅是用于處理啟動前/啟動後的一些通用任務,例如我們可以在head task中做一些擷取通用資訊的操作,在tail task可以做一些log輸出、資料上報等操作。
那麼,這裡我們總結一下啟動的核心流程,如下所示:
- 1、任務Task化,啟動邏輯抽象成Task(Task即對應一個個的初始化任務)。
- 2、根據所有任務依賴關系排序生成一個有向無環圖:例如上述說到的推送SDK初始化任務需要依賴于擷取裝置id的初始化任務,各個任務之間都可能存在依賴關系,是以将它們的依賴關系排序生成一個有向無環圖能将并行效率最大化。
- 3、多線程按照排序後的優先級依次執行:例如必須先初始化擷取裝置id的初始化任務,才能去進行推送SDK的初始化任務。
異步啟動器優化實戰與源碼剖析
下面,我們就來使用異步啟動器來在Application的onCreate方法中進行異步優化,代碼如下所示:
// 1、啟動器初始化
TaskDispatcher.init(this);
// 2、建立啟動器執行個體,這裡每次擷取的都是新對象
TaskDispatcher dispatcher = TaskDispatcher.createInstance();
// 3、給啟動器配置一系列的(異步/非異步)初始化任務并啟動啟動器
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();
// 4、需要等待微信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 {
// 1、僅僅在測試階段才輸出需等待的任務清單數與任務名稱
if (DispatcherLog.isDebug()) {
DispatcherLog.i("still has " + mNeedWaitCount.get());
for (Task task : mNeedWaitTasks) {
DispatcherLog.i("needWait: " + task.getClass().getSimpleName());
}
}
// 2、隻要還有需要等待的任務沒有執行完成,就調用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());
}
}
至此,我們的異步啟動器就分析完畢了。下面我們來看看如何高效地進行延遲初始化。
5、延遲初始化
1、正常方案:利用閃屏頁的停留時間進行部分初始化
- new Handler().postDelayed()。
- 界面UI展示後調用。
2、正常初始化痛點
- 時機不容易控制:handler postDelayed指定的延遲時間不好估計。
- 導緻界面UI卡頓:此時使用者可能還在滑動清單。
3、延遲優化最優解:延遲啟動器
延遲啟動器源碼及使用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卡頓。
- 真正提升使用者體驗。
6、Multidex預加載優化
我們都知道,安裝或者更新後首次 MultiDex 花費的時間過于漫長,我們需要進行Multidex的預加載優化。
1、優化步驟
- 1、啟動時單獨開一個程序去異步進行Multidex的第一次加載,即Dex提取和Dexopt操作。
- 2、此時,主程序Application進入while循環,不斷檢測Multidex操作是否完成。
- 3、執行到Multidex時,則已經發現提取并優化好了Dex,直接執行。MultiDex執行完之後主程序Application繼續執行ContentProvider初始化和Application的onCreate方法。
Multidex優化Demo位址
注意
5.0以上預設使用ART,在安裝時已将Class.dex轉換為oat檔案了,無需優化,是以應判斷隻有在主程序及SDK 5.0以下才進行Multidex的預加載。
2、dex-opt過程是怎樣的?
主要包括inline以及quick指令的優化。
那麼,inline是什麼?
使編譯器在函數調用處用函數體代碼代替函數調用指令。
inline的作用?
函數調用的轉移操作有一定的時間和空間方面的開銷,特别是對于一些函數體不大且頻繁調用的函數,解決其效率問題更為重要,引入inline函數就是為了解決這一問題。
inline又是如何進行優化的?
inline函數至少在三個方面提升了程式的時間性能:
- 1、避免了函數調用必須執行的壓棧出棧等操作。
- 2、由于函數體代碼被移到函數調用處,編譯器可以獲得更多的上下文資訊,并根據這些資訊對函數體代碼和被調用者代碼進行更進一步的優化。
- 3、若不使用inline函數,程式執行至函數調用處,需要轉而去執行函數體所在位置的代碼。一般函數調用位置和函數代碼所在位置在代碼段中并不相近,這樣很容易形成作業系統的缺頁中斷。作業系統需要把缺頁位址的代碼從硬碟移入記憶體,所需時間将成數量級增加。而使用inline函數則可以減少缺頁中斷發生的機會。
對于inline的使用,我們應該注意的問題?
- 1、由于inline函數在函數調用處插入函數體代碼代替函數調用,若該函數在程式的很多位置被調用,有可能造成記憶體空間的浪費。
- 2、一般程式的壓棧出棧操作也需要一定的代碼,這段代碼完成棧指針調整、參數傳遞、現場保護和恢複等操作。 若函數的函數體代碼量小于編譯器生成的函數壓棧出棧代碼,則可以放心地定義為inline,這個時候占用記憶體空間反而會減小。而當函數體代碼大于函數壓棧出棧代碼時,将函數定義為inline就會增加記憶體空間的使用。
- 3、C++程式應該根據應用的具體場景、函數體大小、調用位置多少、函數調用的頻率、應用場景對時間性能的要求,應用場景對記憶體性能的要求等各方面因素合理決定是否定義inline函數。
- 4、inline函數内不允許用循環語句和開關語句。
3、抖音BoostMultiDex優化
為了徹底解決MutiDex加載時間慢的問題,抖音團隊深入挖掘了 Dalvik 虛拟機的底層系統機制,對 DEX 相關的處理邏輯進行了重新設計與優化,并推出了 BoostMultiDex 方案,它能夠減少 80% 以上的黑屏等待時間,挽救低版本 Android 使用者的更新安裝體驗。
具體的實作原理為:在第一次啟動的時候,直接加載沒有經過 OPT 優化的原始 DEX,先使得 APP 能夠正常啟動。然後在背景啟動一個單獨程序,慢慢地做完 DEX 的 OPT 工作,盡可能避免影響到前台 APP 的正常使用。繞過 ODEX 直接加載 DEX 的方案如下:
- 1)、從 APK 中解壓擷取原始 Secondary DEX 檔案的位元組碼
- 2)、通過 dlsym 擷取dvm_dalvik_system_DexFile數組
- 3)、在數組中查詢得到Dalvik_dalvik_system_DexFile_openDexFile_bytearray函數
- 4)、調用該函數,逐個傳入之前從 APK 擷取的 DEX 位元組碼,完成 DEX 加載,得到合法的DexFile對象
- 5)、把DexFile對象都添加到 APP 的PathClassLoader的 pathList 裡
補充:getDex 會抛出異常,原因是 memMap 需要被指派,但是 Dalvik_dalvik_system_DexFile_openDexFile_bytearray 這個函數沒有這個操作。分析代碼後,我們發現,隻要 dex_object 對象不為空,就會直接傳回,不會再往下執行到取 memMap 的地方。是以,我們在加載完 DEX 數組之後,可以自己生成一個dex_object對象,并注入pDvmDex裡面。
如有興趣的同學可以看看這篇文章:抖音BoostMultiDex優化實踐:Android低版本上APP首次啟動時間減少80%
7、類預加載優化
在Application中提前異步加載初始化耗時較長的類。
如何找到耗時較長的類?
替換系統的ClassLoader,列印類加載的時間,按需選取需要異步加載的類。
注意
- Class.forName()隻加載類本身及其靜态變量的引用類。
- new 類執行個體 可以額外加載類成員變量的引用類。
8、WebView啟動優化
- 1、WebView首次建立比較耗時,需要預先建立WebView提前将其核心初始化。
- 2、使用WebView緩存池,用到WebView的時候都從緩存池中拿,注意記憶體洩漏問題。
- 3、本地離線包,即預置靜态頁面資源。
9、頁面資料預加載
在首頁空閑時,将其它頁面的資料加載好儲存到記憶體或資料庫,等到打開該頁面時,判斷已經預加載過,就直接從記憶體或資料庫取資料并顯示。
10、啟動階段不啟動子程序
子程序會共享CPU資源,導緻主程序CPU緊張。此外,在多程序情況下一定要可以在onCreate中去區分程序做一些初始化工作。
注意啟動順序
App onCreate之前是ContentProvider初始化。
11、閃屏頁與首頁的繪制優化
- 1、布局優化。
- 2、過渡繪制優化。
關于布局與繪制優化可以參考Android性能優化之繪制優化。
參考連結:
1、Android開發高手課之啟動優化
2、支付寶用戶端架構解析:Android 用戶端啟動速度優化之「垃圾回收」
3、支付寶 App 建構優化解析:通過安裝包重排布優化 Android 端啟動性能
4、Facebook Redex位元組碼優化工具
5、微信Android熱更新檔實踐演進之路
6、安卓App熱更新檔動态修複技術介紹
7、Dalvik Optimization and Verification With dexopt
8、微信在Github開源了Hardcoder,對Android開發者有什麼影響?
9、曆時三年研發,OPPO 的 Hyper Boost 引擎如何對系統、遊戲和應用實作加速?
10、抱歉,Xposed真的可以為所欲為
11、牆上時鐘時間 ,使用者cpu時間 ,系統cpu時間的了解
12、《Android應用性能優化最佳實踐》
13、必知必會 | Android 性能優化的方面方面都在這兒
14、極客時間之Top團隊大牛帶你玩轉Android性能分析與優化
15、啟動器源碼
16、MultiDex優化源碼
17、使用gradle自動化增加Trace Tag
轉載:https://juejin.cn/post/6844904093786308622