Android 效能優化 ---- 啟動優化
阿新 • • 發佈:2020-07-13
# Android 效能優化 ---- 啟動優化
### 1、為什麼要進行啟動優化
一款應用的第一印象很重要,第一印象往往決定了使用者的去留。開啟一款應用,如果速度很快,很順暢,那麼很容易讓人覺得這款應用背後的技術實力很強,使用者潛意識中會對這款應用更加的信賴。
其次,網上也流行一種說法,就是8秒定律,意思是說,如果使用者在開啟一個頁面,在8秒的時間內還沒有開啟,那麼使用者大概的會放棄掉,意味著一個使用者的流失。從這裡就可以看出,啟動優化的重要性了。
### 2、啟動的分類
#### 2.1 冷啟動
先來看看冷啟動的流程圖
![](https://img2020.cnblogs.com/blog/967362/202007/967362-20200713104410670-1775448711.png)
從圖中可以看出,APP啟動的過程是:ActivityManagerProxy 通過IPC來呼叫AMS(ActivityManagerService),AMS通過IPC啟動一個APP程序,ApplicationThread通過反射來建立Application並且繫結,最後通過ActivityThread來控制activity的生命週期,在相關頁面的生命週期中通過ViewRootImpl來對view的實現。從而完成應用的啟動。
#### 2.2 熱啟動
熱啟動的速度是最快的,它就是程序從後臺切換到前臺的一個過程。
#### 2.3 溫啟動
溫啟動只會重新走一遍頁面的生命週期,但是對於程序,application不會重新在建立。
### 3、優化方向
上面介紹了啟動的幾種方式可以看出,我們針對啟動優化,基本只是優化冷啟動就可以了。但是從冷啟動的啟動流程中很多都是系統做的,我們沒有辦法操控。我們能做的,就是application的生命週期和activity的生命週期這部分,啟動優化往往就是從這兩塊入手。
### 4、啟動時間的測量方式
#### 4.1 使用adb 命令方式(線下使用方便)
``` stylus
adb shell am start -W 包名/包名+類名
```
![](https://img2020.cnblogs.com/blog/967362/202007/967362-20200713104506333-2051042502.png)
ThisTime:最後一個activity的啟動耗時
TotalTime:所有activity的啟動耗時
WaitTime:AMS啟動activity的總耗時
這裡由於我直接進入到主介面,中間並沒有SplashActivity,所有ThisTime 和 TotalTime的時間是一樣的
優勢:線上下使用方便,適合於跑線下的產品,和獲取競品的時間,然後比對
缺點:不能帶到線上,獲取的時間,只能說是一個大概時間,不是很嚴謹。
#### 4.2 手動打點方式
![](https://img2020.cnblogs.com/blog/967362/202007/967362-20200713104529833-723497896.png)
通過`System.currentTimeMillis()`來打時間戳
缺點:很明顯,對程式碼侵入性非常的大,如果說我想要打出每一個任務花費的時間,那麼程式碼看起來就很噁心了
### 5、優雅獲取方法耗時
#### 5.1 AOP Aspect Oriented Programming 面向切面程式設計
AOP:通過預編譯方式和執行期動態代理實現程式功能的統一維護的一種技術。它的核心思想就是將應用程式中的業務邏輯處理部分同對其提供通用服務部分即“橫切關注點”進行分離。
OOP:引入封裝,繼承,多型等概念來建立一種物件層次結構,它允許開發者定義縱向的關係,但並不適合橫向的關係。
可以說AOP是OOP的一種補充和完善。
#### 5.2 aspectj的使用
AspectJ是一個面向切面程式設計的框架,是對java的擴充套件且相容java,AspectJ定義了AOP語法,它有一個專門的編譯器來生成遵守java位元組編碼規範的Class檔案。
在專案的根目錄的build.gradle新增依賴:
``` stylus
dependencies {
classpath 'com.android.tools.build:gradle:3.5.2'
classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
```
在app下的build.gradle新增依賴
``` stylus
apply plugin: 'android-aspectjx'
```
在dependencies中新增
``` stylus
implementation 'org.aspectj:aspectjrt:1.9.4'
```
然後建立一個類
``` stylus
package com.noahedu.myapplication.aspectj;
import android.util.Log;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
/**
* @Description: //@Before 在切入點之前執行
* // @After("")
* //@Around 在切入點前後都執行
* @Author: huangjialin
* @CreateDate: 2020/7/10 14:07
*/
@Aspect
public class MyApplicationAspectj {
@Around("call(* com.noahedu.myapplication.MyApplication.**(..))")
public void getTime(ProceedingJoinPoint joinPoint){
Signature signature = joinPoint.getSignature();
String name = signature.getName();
long time = System.currentTimeMillis();
try {
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
Log.e("MyApplicationAspectj " ,(name + " cost " + (System.currentTimeMillis() - time)));
}
}
```
這樣我們執行的時候,就會直接在logcat中打印出application中的onCreate方法中所有呼叫方法的耗時情況了
``` stylus
2020-07-10 14:22:27.151 1619-1619/? E/MyApplicationAspectj: taskOne cost 150
2020-07-10 14:22:29.203 1619-1619/com.noahedu.myapplication E/MyApplicationAspectj: taskTwo cost 2052
2020-07-10 14:22:29.554 1619-1619/com.noahedu.myapplication E/MyApplicationAspectj: taskThrid cost 351
2020-07-10 14:22:30.556 1619-1619/com.noahedu.myapplication E/MyApplicationAspectj: taskFour cost 1001
```
這樣我們幾乎沒有碰Application中的任何程式碼,也就夠得出各個方法的耗時,幾乎對程式碼無侵入。
### 6、啟動優化的工具選擇
#### 6.1 traceview
TraceView是Android SDK中內建的一個工具,他可以載入trace檔案,以圖形化的形式展示相應程式碼的執行時間,次數及呼叫棧,便於我們分析。
``` stylus
Debug.startMethodTracing("MyApplication");
//TODO
Debug.stopMethodTracing();
```
執行專案就可以我們的SD卡中找到對應的trace檔案了,如果是Android studio可以直接在右下角找到
DeviceFileExporer -->sdcard --> Android -- > data -->files --->自己專案的包名
然後雙擊即可檢視檔案了
優點:使用簡單,圖形形式展示所執行的時間,呼叫棧等。
缺點:會影響到我們優化的方向,由於是圖形化展示,也是比較佔用CPU資源的,所以得到的時間往往是比實際的要大。
### 7、啟動器
上面介紹了多了幾個獲取任務執行時間的方式和工具,那麼當我們知道某個方法耗時了,我們該怎麼處理呢?
``` stylus
package com.noahedu.myapplication;
import android.app.Application;
import android.os.Debug;
import android.util.Log;
/**
* @Description: java類作用描述
* @Author: huangjialin
* @CreateDate: 2020/7/10 9:59
*/
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
Debug.startMethodTracing("MyApplication");
taskOne();
taskTwo();
taskThrid();
taskFour();
Debug.stopMethodTracing();
}
public void taskOne(){
try {
Thread.sleep(150);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void taskTwo(){
try {
Thread.sleep(2050);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void taskThrid(){
try {
Thread.sleep(350);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void taskFour(){
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
```
現在application的onCreate方法中有幾個任務,各個耗時是不一樣的。可能很多同學就會說了,非同步處理啊,開執行緒,放到執行緒池中,或者建立一個IntentService來執行。那麼我們就要考慮幾個問題了
第一:非同步處理,如果在某個頁面需要用到某個SDK,但是又沒有初始化完成呢?
第二:假如說taskTwo需要taskOne的某個返回值呢?怎麼保證taskOne在taskTwo之前執行完畢呢?
第三:開執行緒,開多少個執行緒呢?多了會造成資源浪費,少了資源又沒有合理的利用。
我個人覺得針對於啟動優化,在Application中的onCreate()進行初始化任務操作,我們首先需要對這些任務進行一個優先順序劃分,針對於那些優先順序高的任務,我們可以優先進行處理,對於那些優先順序較低的,我們可以適當的延遲進行載入。
其次有很多同學喜歡把那些優先順序較低的任務進行延遲載入,比如`new Handler().postDelayed()`,這種我覺得是非常不可取的,假如說放在postDelayed中的任務耗時2s,延遲1s進行處理,那麼在執行2s任務的過程中,有使用者進行操作,那豈不是很卡嗎,很明顯,這是指標不治本的。
#### 7.1 啟動器的思想
針對上面說的幾個痛點,怎麼在處理上面的幾個痛點,又能保證程式碼的可維護性呢?換句話說就是一個新人不需要理解整個過程,直接就可以開幹呢?那麼,啟動器來了。
啟動器核心思想:充分利用CPU多核,自動梳理任務順序
#### 7.2 啟動器的原理
1、任務全部封裝成Task物件,傳入到集合中。
2、根據所有的任務依賴關係,形成一個有向無環圖,然後通過拓撲排序排列出任務的執行流程
3、通過CountDownLatch來控制某一個任務是否執行完畢才進行下一步。
4、執行緒池建立核心執行緒的數量,由手機的核數量決定的。
#### 7.3啟動器使用方式
![](https://img2020.cnblogs.com/blog/967362/202007/967362-20200713104836295-1224915585.png)
#### 7.4啟動器核心程式碼
![](https://img2020.cnblogs.com/blog/967362/202007/967362-20200713104624260-418660317.png)
進行任務排序
``` stylus
package com.noahedu.launchertool.launchstarter.sort;
import com.noahedu.launchertool.launchstarter.task.Task;
import com.noahedu.launchertool.launchstarter.utils.DispatcherLog;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import androidx.annotation.NonNull;
import androidx.collection.ArraySet;
public class TaskSortUtil {
private sta