背景
啓動優化
,其實就是優化從點擊icon到主頁面展示這個過程的速度,讓主界面儘量快的展現在用戶面前。
所以我們要做的就是找到那些耗時操作
,並將其優化。
怎麼找到?一般分成兩個場景:
1、線下(debug)場景
在應用的開發階段,我們一般通過AOP
進行函數的耗時統計,通過aspectj
庫可以很方便的將代碼插入到函數內部,從而統計到每個方法的耗時時間
。
或者直接通過Android Studio 自帶的Profiler CPU
工具,查看每個方法的時間,CPU信息。
2、線上場景
當應用已經發布到線上,統計就變得不是那麼容易了。所以我們一般就通過函數插樁
的方式,自己寫一個統計耗時
的工具類,部署到需要統計的地方,比如Application和Activity的生命週期,數據庫的初始化,第三方庫的初始化,然後最後上傳數據到服務器
即可。
找到耗時的地方後該怎麼優化解決呢?
一般就是通過分析這些要執行的任務,進行異步,懶加載,預加載
等操作。
這裏就涉及到我們今天要講的內容了,啓動器
。顧名思義就是幫我們優化啓動的一個工具,可以高效合理
的幫我們安排啓動過程中的一些任務處理。
接下來就帶大家從源碼開始分析,一起看看阿里的啓動器
——Alpha
。
作爲一個異步啓動框架,該有什麼功能
有人可能要問了,不就是異步任務嗎,我整幾個線程,把任務往裏面一丟不就行了。
事情可沒那麼簡單,比如現在有6個任務
需要在Application裏面執行,其中Task1,Task4,Tas6需要在主線程執行,Task2,Task3需要在Task1執行完才能執行,Task4,Task5需要Task2和Task3執行完才能執行,Task6需要Task4和Task5執行完才能執行,Task4的耗時要大於Task5。
這是個啥啊?我暈了。這麼多關係,我該怎麼處理?
既然文字看着太麻煩,就畫個圖吧,這裏涉及到一個用於時間管理的圖形——Pert圖
。
Pert 圖是一個有向圖,能清晰地描述子任務之間的依賴關係。比如我們這個項目的情況,畫成Pert 圖如下:
通過Pert圖
還是可以很直觀的看到每個Task的關係,其中當執行完Task2和Task3之後,我們有兩個選擇,先執行Task4或者先執行Task5,由於Task4的耗時要大於Task5,所以我們就選擇先執行Task4了。
其實制定任務執行的計劃在我們生活中也隨處可見,比如我們早起後也有很多事情要處理,比如燒水(5分鐘),刷牙(3分鐘),洗臉(2分鐘),上廁所(8分鐘)。怎麼選一條最優路線能讓我們最快完成這些事情呢?肯定是能一起並行的事情就安排到一起,然後並行的同時讓耗時久的事情先發生。比如先燒水,然後上廁所的同時刷牙洗臉?扯遠了扯遠了,哈哈哈,收!
好了,看看我們如果用Alpha框架實現該怎麼寫呢?
//構造方法,true爲主線程執行
Task1=new Task("task1",true);
Task2=new Task("task2",false);
Task3=new Task("task3",false);
Task4=new Task("task4",true);
Task5=new Task("task5",false);
Task6=new Task("task6",true);
//設置優先級,耗時操作優先級較高
Task4.setExecutePriority(1);
Task5.setExecutePriority(2);
Project.Builder builder = new Project.Builder().withTaskCreator(new MyTaskCreator());
builder.add(Task1);
builder.add(Task2).after(Task1);
builder.add(Task3).after(Task1);
builder.add(Task4).after(Task2,Task3);
builder.add(Task5).after(Task2,Task3);
builder.add(Task6).after(Task4,Task5);
builder.setProjectName("innerGroup");
AlphaManager.getInstance(mContext).addProject(builder.create());
AlphaManager.getInstance(mContext).start();
搞定!還不錯吧。那就來一起分析下它吧!
首先,我們自己如果好好想想,如果讓我們來做一個異步啓動框架
,需要考慮哪些問題?
- 多線程管理
- 任務的優先級
- 任務之間的先後關係
- 任務是否需要在主線程執行
- 多進程處理
就讓我們帶着這些問題去看看Alpha的內部源碼。
Alpha源碼解析
從上面的代碼可以看到,任務的開啓是由AlphaManager.getInstance(mContext).start()
方法開始調用,所以我們就從這個start
方法開始研究:
public void start() {
Project project = null;
do {
//1.是否有爲當前進程單獨配置的Project,此爲最高優先級
if (mProjectForCurrentProcess != null) {
project = (Project) mProjectForCurrentProcess;
break;
}
//2.如果當前是主進程,是否有配置主進程Project
if (AlphaUtils.isInMainProcess(mContext)
&& mProjectArray.indexOfKey(MAIN_PROCESS_MODE) >= 0) {
project = (Project) mProjectArray.get(MAIN_PROCESS_MODE);
break;
}
//3.如果是非主進程,是否有配置非主進程的Project
if (!AlphaUtils.isInMainProcess(mContext)
&& mProjectArray.indexOfKey(SECONDARY_PROCESS_MODE) >= 0) {
project = (Project) mProjectArray.get(SECONDARY_PROCESS_MODE);
break;
}
//4.是否有配置適用所有進程的Project
if (mProjectArray.indexOfKey(ALL_PROCESS_MODE) >= 0) {
project = (Project) mProjectArray.get(ALL_PROCESS_MODE);
break;
}
} while (false);
if (project != null) {
addListeners(project);
project.start();
} else {
AlphaLog.e(AlphaLog.GLOBAL_TAG, "No startup project for current process.");
}
}
哇,一開始就把我們多進程的疑惑給解決了。start方法首先就判斷了當前的進程以及是否能匹配到相關進程的任務。可以看到一共有三種進程配置變量:
MAIN_PROCESS_MODE
: 主進程任務SECONDARY_PROCESS_MODE
:非主進程任務ALL_PROCESS_MODE
:適用於所有進程的任務
那麼在哪裏配置這些進程選項呢?addProject
方法
public void addProject(Task project, int mode) {
if (project == null) {
throw new IllegalArgumentException("project is null");
}
if (mode < MAIN_PROCESS_MODE || mode > ALL_PROCESS_MODE) {
throw new IllegalArgumentException("No such mode: " + mode);
}
if (AlphaUtils.isMatchMode(mContext, mode)) {
mProjectArray.put(mode, project);
}
}
ok,夠簡單吧。繼續往下看start方法。
跳轉到project的start
方法:
@Override
public void start() {
mStartTask.start();
}
這麼簡單嗎,就開啓了一個mStartTask
?這個mStartTask
是之前設置的那些任務中第一個任務嗎?接着看:
//Project.java
private void init() {
...
mProject = new Project();
mFinishTask = new AnchorTask(false, "==AlphaDefaultFinishTask==");
mFinishTask.setProjectLifecycleCallbacks(mProject);
mStartTask = new AnchorTask(true, "==AlphaDefaultStartTask==");
mStartTask.setProjectLifecycleCallbacks(mProject);
mProject.setStartTask(mStartTask);
mProject.setFinishTask(mFinishTask);
...
}
private static class AnchorTask extends Task {
private boolean mIsStartTask = true;
private OnProjectExecuteListener mExecuteListener;
public AnchorTask(boolean isStartTask, String name) {
super(name);
mIsStartTask = isStartTask;
}
public void setProjectLifecycleCallbacks(OnProjectExecuteListener callbacks) {
mExecuteListener = callbacks;
}
@Override
public void run() {
if (mExecuteListener != null) {
if (mIsStartTask) {
mExecuteListener.onProjectStart();
} else {
mExecuteListener.onProjectFinish();
}
}
}
}
可以看到,在Project類
的初始化方法中,定義了一個開始任務
和一個結束任務
。這是因爲從執行角度看,一個任務序列必須有一個開始節點和一個結束節點。但是實際情況中,可能會有多個任務可以同時開始,而且有多個任務可以同時作爲結束點。所以就設置了這兩個節點方便控制整個流程
,標記流程的開始和結束,也方便了任務的監聽
。
說回上面,開始任務的start
方法走到哪裏去了呢?自然是AnchorTask的父類Task
,看看源碼:
public synchronized void start() {
...
switchState(STATE_WAIT);
if (mInternalRunnable == null) {
mInternalRunnable = new Runnable() {
@Override
public void run() {
android.os.Process.setThreadPriority(mThreadPriority);
long startTime = System.currentTimeMillis();
switchState(STATE_RUNNING);
Task.this.run();
switchState(STATE_FINISHED);
long finishTime = System.currentTimeMillis();
recordTime((finishTime - startTime));
notifyFinished();
recycle();
}
};
}
if (mIsInUiThread) {
sHandler.post(mInternalRunnable);
} else {
sExecutor.execute(mInternalRunnable);
}
}
源碼還是挺簡單的哈,定義了一個Runnable
,然後判斷是否主線程,並執行這個Runnable
。其中還穿插了一些狀態的改變,在Runnable
內部主要是執行了Task.this.run()
,也就是執行了任務本身。其中setThreadPriority
方法主要是設置了線程的優先級,比如THREAD_PRIORITY_DEFAULT
等,這裏的優先級是較線程而言的,主要是針對CPU資源的競爭,跟我們需要的Task之間的優先級關係不大。
如果是需要在主線程執行的任務,就會通過Handler(sHandler)
將事件傳遞給主線程執行。
如果是需要在非主線程執行的任務,就會通過線程池(sExecutor)
去執行線程任務。
誒,好像沒了?開始任務執行了就沒了嗎?再回頭看看,還有一個notifyFinished
方法。
按這個名字應該就是通知任務結束的一個方法,看看源碼:
void notifyFinished() {
if (!mSuccessorList.isEmpty()) {
AlphaUtils.sort(mSuccessorList);
for (Task task : mSuccessorList) {
task.onPredecessorFinished(this);
}
}
if (!mTaskFinishListeners.isEmpty()) {
for (OnTaskFinishListener listener : mTaskFinishListeners) {
listener.onTaskFinish(mName);
}
mTaskFinishListeners.clear();
}
}
這個方法主要做了三件事:
mSuccessorList
排序- 遍歷
mSuccessorList
列表,執行onPredecessorFinished
方法 - 監聽回調
onTaskFinish
方法
mSuccessorList
是什麼呢?我們叫它緊後任務列表,也就是接下來要執行的任務列表。所以流程就是先把當前任務之後的任務列表進行一個排序,根據優先級排序。然後按順序執行onPredecessorFinished
方法。
如果緊後任務列表爲空,也就代表沒有後續任務了,那麼就會走onTaskFinish
回調方法,告知當前Project已經執行完畢。
接下來就看看緊後任務是怎麼加進來的呢?又該怎麼排序?onPredecessorFinished
方法又執行了些什麼東西?
//1、緊後任務添加
public Builder after(Task task) {
task.addSuccessor(mCacheTask);
mFinishTask.removePredecessor(task);
mIsSetPosition = true;
return Builder.this;
}
void addSuccessor(Task task) {
task.addPredecessor(this);
mSuccessorList.add(task);
}
//2、緊後任務列表排序
public static void sort(List<Task> tasks) {
if (tasks.size() <= 1) {
return;
}
Collections.sort(tasks, sTaskComparator);
}
private static Comparator<Task> sTaskComparator = new Comparator<Task>() {
@Override
public int compare(Task lhs, Task rhs) {
return lhs.getExecutePriority() - rhs.getExecutePriority();
}
};
//3、緊後任務執行
synchronized void onPredecessorFinished(Task beforeTask) {
if (mPredecessorSet.isEmpty()) {
return;
}
mPredecessorSet.remove(beforeTask);
if (mPredecessorSet.isEmpty()) {
start();
}
}
ok,源碼寫的很清楚了,這裏逐步分析下:
由源碼得知,緊後任務列表主要是通過after方法
,還記得之前配置任務的時候嗎?
builder.add(Task2).after(Task1)
,所以這個after就代表Task2要在Task1後面執行,也就是Task2成了Task1的緊後任務。同理,Task1也就成了Task2的緊前任務。也就是代碼中的addPredecessor
方法,在添加緊後任務的同時也添加了緊前任務。
可能有人會問了,緊前任務添加了有什麼用呢?難不成還倒退回去執行?
試想一下,如果有多個任務的緊後任務都是一個呢?比如這種情況:builder.add(Task4).after(Task2,Task3)
。Task4是Task2和Task3的緊後任務,所以在Task2執行完之後,還要判斷Task3是否執行成功,然後才能執行Task4,這就是緊前任務列表的作用。這也就對應到上述代碼中onPredecessorFinished
方法的邏輯了。
然後這個緊後任務列表的排序是怎麼排的呢?其實就是通過getExecutePriority
方法獲取task的執行優先級數字,按照正序排列,越小的任務執行時機越早。還記得之前配置的時候我設置了setExecutePriority
方法嗎,就是這裏設置了優先級的。
至此主要邏輯就差不多了。好像還挺簡單的是不是。還有一些細節我也簡單的提下:
- 各種回調:包括一些task的回調,project的回調。
- 日誌記錄:比如耗時時間的記錄,剛纔執行任務時候的
recordTime方法
,就是記錄了每個task的耗時。 - 多種Task配置方法:除了上面用Java代碼配置,還可以通過
xml文件
來配置Project和裏面的Task,這個就主要是XmlPullParser
類來解析xml數據,然後生成Prject。 - 各種設計模式:比如構建Project的建造者模式,還有通過傳入task名稱就可以創建Task的工廠模式。
諸如此類的一些細節感興趣朋友的可以自己下源碼看看。
最後用一張流程圖總結下吧:
總結
分析下來,這個異步啓動框架應該算比較簡單的,但是能解決問題啊!其實我們平時工作中也可以做一些積累,然後寫成工具或者框架,如果能開源出來大家一起使用還真是一件不錯的事情呢!
你的一個👍,就是我分享的動力❤️。