更快!更高效!啓動優化框架Alpha完全解析

背景

啓動優化,其實就是優化從點擊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圖.jpg

通過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的工廠模式。

諸如此類的一些細節感興趣朋友的可以自己下源碼看看。

最後用一張流程圖總結下吧:
Alpha流程圖.jpg

總結

分析下來,這個異步啓動框架應該算比較簡單的,但是能解決問題啊!其實我們平時工作中也可以做一些積累,然後寫成工具或者框架,如果能開源出來大家一起使用還真是一件不錯的事情呢!


你的一個👍,就是我分享的動力❤️。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章