Android 效能優化 ---- 啟動優化
1、為什麼要進行啟動優化
一款應用的第一印象很重要,第一印象往往決定了使用者的去留。開啟一款應用,如果速度很快,很順暢,那麼很容易讓人覺得這款應用背後的技術實力很強,使用者潛意識中會對這款應用更加的信賴。
其次,網上也流行一種說法,就是8秒定律,意思是說,如果使用者在開啟一個頁面,在8秒的時間內還沒有開啟,那麼使用者大概的會放棄掉,意味著一個使用者的流失。從這裡就可以看出,啟動優化的重要性了。
2、啟動的分類
2.1 冷啟動
先來看看冷啟動的流程圖
從圖中可以看出,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 命令方式(線下使用方便)
adb shell am start -W 包名/包名+類名
ThisTime:最後一個activity的啟動耗時
TotalTime:所有activity的啟動耗時
WaitTime:AMS啟動activity的總耗時
這裡由於我直接進入到主介面,中間並沒有SplashActivity,所有ThisTime 和 TotalTime的時間是一樣的
優勢:線上下使用方便,適合於跑線下的產品,和獲取競品的時間,然後比對
缺點:不能帶到線上,獲取的時間,只能說是一個大概時間,不是很嚴謹。
4.2 手動打點方式
通過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新增依賴:
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新增依賴
apply plugin: 'android-aspectjx'
在dependencies中新增
implementation 'org.aspectj:aspectjrt:1.9.4'
然後建立一個類
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方法中所有呼叫方法的耗時情況了
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檔案,以圖形化的形式展示相應程式碼的執行時間,次數及呼叫棧,便於我們分析。
Debug.startMethodTracing("MyApplication");
//TODO
Debug.stopMethodTracing();
執行專案就可以我們的SD卡中找到對應的trace檔案了,如果是Android studio可以直接在右下角找到
DeviceFileExporer -->sdcard --> Android -- > data -->files --->自己專案的包名
然後雙擊即可檢視檔案了
優點:使用簡單,圖形形式展示所執行的時間,呼叫棧等。
缺點:會影響到我們優化的方向,由於是圖形化展示,也是比較佔用CPU資源的,所以得到的時間往往是比實際的要大。
7、啟動器
上面介紹了多了幾個獲取任務執行時間的方式和工具,那麼當我們知道某個方法耗時了,我們該怎麼處理呢?
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啟動器使用方式
7.4啟動器核心程式碼
進行任務排序
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 static List<Task> sNewTasksHigh = new ArrayList<>();// 高優先順序的Task
/**
* 任務的有向無環圖的拓撲排序
*
* @return
*/
public static synchronized List<Task> getSortResult(List<Task> originTasks,
List<Class<? extends Task>> clsLaunchTasks) {
long makeTime = System.currentTimeMillis();
Set<Integer> dependSet = new ArraySet<>();
Graph graph = new Graph(originTasks.size());
for (int i = 0; i < originTasks.size(); i++) {
Task task = originTasks.get(i);
if (task.isSend() || task.dependsOn() == null || task.dependsOn().size() == 0) {
continue;
}
for (Class cls : task.dependsOn()) {
int indexOfDepend = getIndexOfTask(originTasks, clsLaunchTasks, cls);
if (indexOfDepend < 0) {
throw new IllegalStateException(task.getClass().getSimpleName() +
" depends on " + cls.getSimpleName() + " can not be found in task list ");
}
dependSet.add(indexOfDepend);
graph.addEdge(indexOfDepend, i);
}
}
List<Integer> indexList = graph.topologicalSort();
List<Task> newTasksAll = getResultTasks(originTasks, dependSet, indexList);
DispatcherLog.i("task analyse cost makeTime " + (System.currentTimeMillis() - makeTime));
printAllTaskName(newTasksAll);
return newTasksAll;
}
@NonNull
private static List<Task> getResultTasks(List<Task> originTasks,
Set<Integer> dependSet, List<Integer> indexList) {
List<Task> newTasksAll = new ArrayList<>(originTasks.size());
List<Task> newTasksDepended = new ArrayList<>();// 被別人依賴的
List<Task> newTasksWithOutDepend = new ArrayList<>();// 沒有依賴的
List<Task> newTasksRunAsSoon = new ArrayList<>();// 需要提升自己優先順序的,先執行(這個先是相對於沒有依賴的先)
for (int index : indexList) {
if (dependSet.contains(index)) {
newTasksDepended.add(originTasks.get(index));
} else {
Task task = originTasks.get(index);
if (task.needRunAsSoon()) {
newTasksRunAsSoon.add(task);
} else {
newTasksWithOutDepend.add(task);
}
}
}
// 順序:被別人依賴的————》需要提升自己優先順序的————》需要被等待的————》沒有依賴的
sNewTasksHigh.addAll(newTasksDepended);
sNewTasksHigh.addAll(newTasksRunAsSoon);
newTasksAll.addAll(sNewTasksHigh);
newTasksAll.addAll(newTasksWithOutDepend);
return newTasksAll;
}
private static void printAllTaskName(List<Task> newTasksAll) {
if (true) {
return;
}
for (Task task : newTasksAll) {
DispatcherLog.i(task.getClass().getSimpleName());
}
}
public static List<Task> getTasksHigh() {
return sNewTasksHigh;
}
/**
* 獲取任務在任務列表中的index
*
* @param originTasks
* @return
*/
private static int getIndexOfTask(List<Task> originTasks,
List<Class<? extends Task>> clsLaunchTasks, Class cls) {
int index = clsLaunchTasks.indexOf(cls);
if (index >= 0) {
return index;
}
// 僅僅是保護性程式碼
final int size = originTasks.size();
for (int i = 0; i < size; i++) {
if (cls.getSimpleName().equals(originTasks.get(i).getClass().getSimpleName())) {
return i;
}
}
return index;
}
}
執行任務程式碼
package com.noahedu.launchertool.launchstarter.task;
import android.os.Looper;
import android.os.Process;
import com.noahedu.launchertool.launchstarter.TaskDispatcher;
import com.noahedu.launchertool.launchstarter.stat.TaskStat;
import com.noahedu.launchertool.launchstarter.utils.DispatcherLog;
/**
* 任務真正執行的地方
*/
public class DispatchRunnable implements Runnable {
private Task mTask;
private TaskDispatcher mTaskDispatcher;
public DispatchRunnable(Task task) {
this.mTask = task;
}
public DispatchRunnable(Task task,TaskDispatcher dispatcher) {
this.mTask = task;
this.mTaskDispatcher = dispatcher;
}
@Override
public void run() {
DispatcherLog.i(mTask.getClass().getSimpleName()
+ " begin run" + " Situation " + TaskStat.getCurrentSituation());
Process.setThreadPriority(mTask.priority());
long startTime = System.currentTimeMillis();
mTask.setWaiting(true);
mTask.waitToSatisfy();
long waitTime = System.currentTimeMillis() - startTime;
startTime = System.currentTimeMillis();
// 執行Task
mTask.setRunning(true);
mTask.run();
// 執行Task的尾部任務
Runnable tailRunnable = mTask.getTailRunnable();
if (tailRunnable != null) {
tailRunnable.run();
}
if (!mTask.needCall() || !mTask.runOnMainThread()) {
printTaskLog(startTime, waitTime);
TaskStat.markTaskDone();
mTask.setFinished(true);
if(mTaskDispatcher != null){
mTaskDispatcher.satisfyChildren(mTask);
mTaskDispatcher.markTaskDone(mTask);
}
DispatcherLog.i(mTask.getClass().getSimpleName() + " finish");
}
}
/**
* 列印出來Task執行的日誌
*
* @param startTime
* @param waitTime
*/
private void printTaskLog(long startTime, long waitTime) {
long runTime = System.currentTimeMillis() - startTime;
if (DispatcherLog.isDebug()) {
DispatcherLog.i(mTask.getClass().getSimpleName() + " wait " + waitTime + " run "
+ runTime + " isMain " + (Looper.getMainLooper() == Looper.myLooper())
+ " needWait " + (mTask.needWait() || (Looper.getMainLooper() == Looper.myLooper()))
+ " ThreadId " + Thread.currentThread().getId()
+ " ThreadName " + Thread.currentThread().getName()
+ " Situation " + TaskStat.getCurrentSituation()
);
}
}
}
基本核心程式碼就是上面這幾個,完整的程式碼會在後面的demo給出
8、其他優化方案
8.1 對延遲任務進行分批初始化,使用IdleHandler特性,進行空閒執行 (適合優先順序不是很高,不急於初始化的第三方SDK)
IdleHandler:IdleHandler 可以用來提升效能,主要用在我們希望能夠在當前執行緒訊息佇列空閒時做些事情(譬如 UI 執行緒在顯示完成後,如果執行緒空閒我們就可以提前準備其他內容)的情況下,不過最好不要做耗時操作。簡單來說就是,looper物件有空的時候就會執行IdleHandler中的任務。
前面說過,在application的任務進行優先順序劃分,那麼如果優先順序低的任務,我們是不是可以不再application的onCreate中進行初始化呢?是否可以放到activity中進行呢?如果可以在activity中,那麼在activity那個階段適合呢?其實在onCreate(),onResume()都是可以的。當然我們也可以使用view中的getViewTreeObserver().addOnPreDrawListener進行監聽,這個事件是檢視將要繪製的時候會回撥該方法。我們可以在回撥的方法中將要初始化的SDK放大IdleHandler中。
8.2 提前載入SP 可以放到multidex之前載入,利用此階段的CPU
SharedPreferences,以鍵值對的形式進行資料的儲存的,會一次性載入到記憶體中,所以我們可以考慮那個階段的CPU相對來說比較空閒。如可以放到multidex之前載入,充分利用此階段的CPU進行