經過上期的學習(一線大廠資深APP性能優化系列-卡頓定位(一)),咱們學會了 定位及獲取程序的耗費時間 並找到卡頓的地方。這期咱們來談談具體的優化方案,首先是 異步優化 html
異步優化的核心思想:子線程來分擔主線程的任務,並減小運行時間java
接着上期的內容,經過卡頓定位,找到咱們卡頓處的代碼node
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
// Debug.startMethodTracing("MyApplication");
initBugly();
initBaiduMap();
initJPushInterface();
initShareSDK();
// Debug.stopMethodTracing();
}
private void initBugly() throws InterruptedException {
initBaiduMap();
Thread.sleep(1000);
}
private void initBaiduMap() throws InterruptedException {
Thread.sleep(2000);
}
private void initJPushInterface() throws InterruptedException {
Thread.sleep(3000);
}
private void initShareSDK() throws InterruptedException {
Thread.sleep(500);
}
}
複製代碼
執行完畢打開頁面大約是 5.5秒,太慢了,須要優化!git
啓動優化是一個大型軟件開發中要作的第一步優化,由於不管你的APP作的內容有多麼豐富,若是啓動比較慢的話,那麼給用戶的第一印象就會很是很差。github
想到異步首先想到的就是開線程,可是須要注意的是不要直接就去開線程,由於線程缺少統一管理,可能無限制新建線程,相互之間競爭,及可能佔用過多系統資源致使死機或oom,因此咱們使用線程池的方式去執行異步。算法
對線程池不瞭解的就去看 大話線程池原理性能優化
這裏咱們採用 FixedThreadPool 來解決卡頓。可是問題來了, 這個線程池須要指定多少個線程合適尼? bash
有人會說,有幾個方法就指定幾個唄。。(忽然想吐槽下,阿里裏面曾經有個項目,就是這麼作的,開的線程池就是按心情隨便指定了個數字。。。實際上是不對的,但咱人小勢微的也不敢說啊)併發
爲了更高效的利用CPU,儘可能線程池數量不要寫死,由於不一樣的手機廠商的CPU核數也是不同的,設置的線程數太小的話,有的CPU是浪費的,設置過多的話,均搶佔CUP,也會增長負擔。因此正確的寫法是:框架
// 得到當前CPU的核心數
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
// 設置線程池的核心線程數2-4之間,可是取決於CPU核數
private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
// 建立線程池
ExecutorService executorService = Executors.newFixedThreadPool(CORE_POOL_SIZE);
複製代碼
實驗證實,根據這個公式來算出的線程數是最合理的,至於爲何是這樣算,不是全佔用了CPU數是最好的嗎?
表示不信做者的同窗你就本身看下面的公式,歡迎來懟做者!
好了咱們用線程池改造下
@Override
public void onCreate() {
super.onCreate();
// Debug.startMethodTracing("MyApplication");
ExecutorService executorService = Executors.newFixedThreadPool(CORE_POOL_SIZE);
executorService.submit(new Runnable() {
@Override
public void run() {
initBugly();
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
initBaiduMap();
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
initJPushInterface();
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
initShareSDK();
}
});
// Debug.stopMethodTracing();
}
複製代碼
通過測試,如今打開頁面基本是秒開的。
可是又出現了問題,好比有的是必需要先執行完畢才能進入頁面的。也就是說,得先讓它執行完畢,才能進入主頁。
很簡單-加鎖就行,關於鎖的介紹就不在這裏贅述了,有不少,可是做者用的是CountDownLatch, 又稱之爲門栓,構造函數就是指定門栓的個數, latch.countDown(); 每次調用都減小一個門栓數, latch.await(); 就是開啓等待,當門栓數爲0時,就放開去執行下面的邏輯。
@Override
public void onCreate() {
super.onCreate();
// Debug.startMethodTracing("MyApplication");
final CountDownLatch latch = new CountDownLatch(1);
ExecutorService executorService = Executors.newFixedThreadPool(CORE_POOL_SIZE);
executorService.submit(new Runnable() {
@Override
public void run() {
initBugly();
latch.countDown();
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
initBaiduMap();
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
initJPushInterface();
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
initShareSDK();
}
});
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// Debug.stopMethodTracing();
}
複製代碼
這樣就好了,只有當initBugly執行完畢才能繼續跳轉頁面,固然值得補充的是,之因此要加門栓,是由於在onCreate方法裏面,可能還有其餘必須在主線程才能初始化的其餘耗時任務,而initBugly能夠不須要在主線程裏面初始化,可是又必須得初始化完畢才能跳轉頁面。因此爲了避免再增長時間,才啓動線程池+門栓去初始化
好了,既加快了速度,又能夠保證一些不須要在主線程優化而又啓動以前必須初始化完成的任務不出問題,
可是尼,這麼寫,有點Low,並且若是有的耗時方法存在關聯,好比必須先執行完A,根據A的返回值,再執行B,而後根據B的返回再執行C,那麼必須串聯的話,咱們該如何優化尼?
先看圖,目的就是進行任務分類
看了圖,是否是略微明白了?咱們的任務基本就是這樣的,有的必需要在因此任務以前初始化,有的必需要在主線程初始化,有的能夠有空在初始化,有的必需要在有的任務執行完畢再初始化,好比激光推送須要設備ID,那麼就必需要在獲取設備ID這個方法執行完才能執行,因此咱們要對耗時任務先分類。
因而有了上圖
好了分好類了以後,咱們發現若是用常規的方式去作,好比線程池+門栓就很麻煩了,並且效率不高,爲了極致的性能體驗,咱們會本身作一個啓動器
先看下咱們完善後的代碼
// 使用啓動器的方式
TaskDispatcher.init(this);
TaskDispatcher instance = TaskDispatcher.createInstance();
instance.addTask(new initBuglyTask()) // 默認添加,併發處理
instance.addTask(new initBaiduMapTask()) // 在這裏須要先處理了另一個耗時任務initShareSDK,才能再處理它
instance.addTask(new initJPushInterface()) // 等待主線程處理完畢,再進行執行
.start();
instance.await();
複製代碼
看無語倫比的簡單,不管是須要別的任務執行完再執行的繼承關係,仍是必須主線程執行完的等待,仍是能夠併發的執行,在定義task裏面,只須要一個方法便可。
什麼是啓動器?爲啥用它?
在應用啓動的時候,咱們一般會有不少工做須要作,爲了提升啓動速度,咱們會盡量讓這些工做併發進行。但這些工做之間可能存在先後依賴的關係,因此咱們又須要想辦法保證他們執行順序的正確性,很麻煩。
雖然阿里也出了個啓動器 alibaba / alpha
可是尼,在狗東用阿里的框架總感受怪怪的,接下來的時間就帶領你們去打造一款屬於本身的啓動器。
要想作啓動器,首先是要解決一些依賴關係,好比,咱們傳入的任務是A,B,C可是尼,若是A依賴於B,那麼就須要先初始化B,同時處理C,而後再處理A。
怎麼排序尼,這個有個專業的名稱,叫作了 任務的有向無環圖的拓撲排序
不知道的去看這裏 有向無環圖的拓撲排序
好了,我先貼出算法的代碼,有個簡單的認識即可,下期帶領你們一步一步的打架本身的啓動器。
TaskSortUtil.java
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 * * @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;
}
}
複製代碼
Graph.java
/** * 有向無環圖的拓撲排序算法 */
public class Graph {
//頂點數
private int mVerticeCount;
//鄰接表
private List<Integer>[] mAdj;
public Graph(int verticeCount) {
this.mVerticeCount = verticeCount;
mAdj = new ArrayList[mVerticeCount];
for (int i = 0; i < mVerticeCount; i++) {
mAdj[i] = new ArrayList<Integer>();
}
}
/** * 添加邊 * */
public void addEdge(int u, int v) {
mAdj[u].add(v);
}
/** * 拓撲排序 */
public Vector<Integer> topologicalSort() {
int indegree[] = new int[mVerticeCount];
for (int i = 0; i < mVerticeCount; i++) {//初始化全部點的入度數量
ArrayList<Integer> temp = (ArrayList<Integer>) mAdj[i];
for (int node : temp) {
indegree[node]++;
}
}
Queue<Integer> queue = new LinkedList<Integer>();
for (int i = 0; i < mVerticeCount; i++) {//找出全部入度爲0的點
if (indegree[i] == 0) {
queue.add(i);
}
}
int cnt = 0;
Vector<Integer> topOrder = new Vector<Integer>();
while (!queue.isEmpty()) {
int u = queue.poll();
topOrder.add(u);
for (int node : mAdj[u]) {//找到該點(入度爲0)的全部鄰接點
if (--indegree[node] == 0) {//把這個點的入度減一,若是入度變成了0,那麼添加到入度0的隊列裏
queue.add(node);
}
}
cnt++;
}
if (cnt != mVerticeCount) {//檢查是否有環,理論上拿出來的點的次數和點的數量應該一致,若是不一致,說明有環
throw new IllegalStateException("Exists a cycle in the graph");
}
return topOrder;
}
}
複製代碼
網上有不少這種類型的代碼,百度便可
好了,原本打算想把啓動器全寫在這一章節裏面,可是吧,這周就放了一天,休息時間太少了,只能把一章的內容拆爲2章,有興趣的小夥伴記得點贊加關注,下期關於自定義啓動器的章節,大概3天后,若是不忙的話,更新出來。
這章基本就是認識下不要寫死線程池,那並非最優解,並且Application裏面或者Activity的啓動項,是能夠按照上面的圖片進行分別分類的。可是普通方法執行起來實在太麻煩。關注下一期的啓動器章節,end!