Android性能分析與優化學習(二) App啓動優化

一、App啓動優化介紹

1、背景介紹
  • 第一體驗
  • 八秒定律
2、啓動分類
  • 冷啓動
  • 耗時最多,衡量標準
    ClickEvent -> IPC -> Process.start ->ActivityThread(單獨app進程入口類) ->bindApplication(通過反射創建Application以及調用與Application相關的生命週期) ->LifeCycle(Activity生命週期) -> ViewRootImpl(開始真正的界面繪製)
  • 熱啓動,最快

後臺 -> 前臺

  • 溫啓動,較快

LifeCycle(只會重走activity生命週期,不會重新進程創建)
#####3、相關任務

  • 冷啓動之前:啓動App,加載空白Window,創建進程
  • 隨後任務:創建Application,啓動主線程,創建MainActivity,加載佈局,佈置屏幕,首幀繪製
4、優化方向
  • Application和Activity生命週期

二、啓動時間測量方式

1、adc命令

adb shell am start -W packagename/首屏Activity
命令

  • ThisTime:最後一個Activity啓動耗時
  • TotalTime:所有Activity啓動耗時
  • WaitTime:AMS啓動Activity的總耗時
    問題:線下使用方便,不能帶到線上,非嚴謹精確時間
2、手動打點

啓動時埋點,啓動結束時埋點,差值

public class LaunchTimer {
    private static long sTime;

    public static void startRecord() {
        sTime = System.currentTimeMillis();
    }

    public static void endRecord() {
        long cost = System.currentTimeMillis() - sTime;
        Log.i("cost", cost + "");
    }
}

這個回調是應用程序能接收到的最早的回調,在這裏開始計時

@Override
    protected void attachBaseContext(Context base) {
        super.attachBaseContext(base);
        LaunchTimer.startRecord();
        MultiDex.install(this);
    }

誤區:onWindowFocusChanged只是首幀時間,並不能代表界面已經展示出來了,要在真實用戶展示時間埋點,比如在列表第一條數據展示時候

@Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        if (position == 0 && !mHasRecorded) {
            mHasRecorded = true;
            holder.itemView.getViewTreeObserver()
                    .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
                @Override
                public boolean onPreDraw() {
                    holder.itemView.getViewTreeObserver().removeOnPreDrawListener(this);
                    LaunchTimer.endRecord();
                    return false;
                }
            });
        }
    }

三、 啓動優化工具選擇

1、traceview
  • 圖形化界面形式展示代碼執行時間,調用棧等
  • 信息全面,包含所有線程
  • 使用方式
//在開始時調用
Debug.startMethodTracing("文件名");
//結束時調用
Debug.stopMethodTracing();

生成的文件存放在sd卡:Android/data/packagename/files,運行app後開路徑下找到文件生成的文件雙擊打開文件生成的文件
最上面的是時間範圍,可移動鼠標拖動
中間的THREADS是線程,可以看到有17個線程,選中一個線程就可以看到下面線程做的事情,當前看的是mian線程

  • Call Chart
    每一行都是一個方法的調用時間消耗,他的垂直方向是被調用者
    系統api是橙色,應用自身的方法是綠色,第三方api是藍色(包括java語言的api)
  • TopDownTopDown可以看到方法執行的總時間Total,Self時間和Children時間
    可以選擇看Wall Colock Time (線程真正執行時間),Thread Time (CPU執行時間)
  • Flame Chart
    收集相同的調用順序Flame Chart
  • Bottom Up
    一個函數的調用列表,誰調用了我Bottom Up
  • 問題:加入了traceview,運行開銷嚴重,整體開銷都會變慢,可能會帶偏優化方向
2、systrace
  • 結合Android內核的數據,生成Html報告
  • 使用方式
python systrace.py t 10 [other-options] [categories]
//開始時
TraceCompat.beginSection("systrace");
//結束時
TraceCompat.endSection();

到systrace目錄下運行腳本,我的是E:\SDK\platform-tools\systrace
注意python要2.7版本的
python systrace.py --time=10 -o mytrace.html sched gfx view wm
也可以用Android Device Monitor來生成
image.png
打開生成的html
image.png
該報告列出了呈現UI幀並指示沿時間線的每個渲染幀的每個進程。用綠色框架圓圈表示在16.6毫秒內渲染以保持每秒60幀穩定所需的幀。渲染時間超過16.6毫秒的幀用黃色或紅色框架圓圈表示。
可以選中一個線程查看他的時間消耗
image.png

  • 官方文檔:https://source.android.com/devices/tech/debug/systrace
  • 優點:輕量級,開銷小,直觀反饋CPU利用率
  • cputime和walltime的區別:cputime是代碼消耗cpu的時間(重點指標),walltime是代碼執行時間。
    舉例:鎖衝突,可能某個線程在等待鎖,導致walltime時間看起來很長,但是對CPU沒有佔用
3、優雅獲取方法耗時

1.常規方式:
背景:需要知道啓動階段所有方法耗時
實現:手動埋點
入侵性強,工作量大
2.AOP
Aspect Oriented Programming,面向切面編程,針對同一類問題的統一處理,無侵入添加代碼
https://github.com/HujiangTechnology/gradle_plugin_android_aspectjx
使用AspectJ,在根build.gradle下配置:

buildscript {
    ...
    dependencies {
        ...
        classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.4'
    }
}

app項目的build.gradle及新建的module的build.gradle裏都應用插件

apply plugin: 'android-aspectjx'
dependencies {
    implementation 'org.aspectj:aspectjrt:1.8.9'
}
  • Join Points
    程序運行時的執行點,可以作爲切面的地方。(函數調用、執行,獲取、設置變量,類的初始化)
  • PointCut
    帶條件的JoinPoints
  • Advice
    一種Hook,要插入代碼的位置。before:PointCut之前執行。after:PointCut之後執行。around:之前之後分別執行
  • 語法介紹
@Before("execution(*android.app.Activity.on**(...))
public void onActivityCalled(JoinPoint joinPoint) throws Throwable{
    ...
}

Before:Advice,具體插入位置
execution:處理Join Point的類型,call(插入在函數體裏面)、execution(插入在函數體外面)
(android.app.Activity.on*(…)):匹配規則
onActivityCalled:要插入的代碼
使用AOP實現啓動時間監聽

@Aspect
public class PerformanceAop {
    //Around在每個方法執行之前和之後分別插入代碼,只切了BaseApplication裏面的函數**(..)任何方法參數
    @Around("call(* com.test.common.BaseApplication.**(..))")
    public void getTime(ProceedingJoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        String name=signature.getName();
        long time = System.currentTimeMillis();
        Log.i("cost", System.currentTimeMillis() - time + "");
        try {
            joinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        Log.i("cost", name + (System.currentTimeMillis() - time));
    }
}

有點:無侵入性,修改方便

四、異步優化

1、優化小技巧
  • Theme切換:感覺上的快,先進入一個閃屏頁
    image.png先使用這個theme,在MainActivity的super.onCreate()之前切換回來
2、異步優化
  • 核心思想:子線程分擔主線程任務,並行減少時間
    image.png
    異步優化注意
  • 不符合異步要求的,一種是修改代碼使其滿足,一種是放棄異步的優化
  • 需要在某個階段完成,可以使用CountDownLatch
  • 區分CPU密集型和IO密集型任務
public class AppApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        //線程池數量,可以參照AsyncTask,
        int CPU_COUNT = Runtime.getRuntime().availableProcessors();
        // We want at least 2 threads and at most 4 threads in the core pool,
        // preferring to have 1 less than the CPU count to avoid saturating
        // the CPU with background work
        int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
        int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
        ExecutorService service=Executors.newFixedThreadPool(CORE_POOL_SIZE);
        service.submit(new Runnable() {
            @Override
            public void run() {
                initBugly();
            }
        });
        service.submit(new Runnable() {
            @Override
            public void run() {
                initMap();
            }
        });
        ......
    }
}

當然並不是所有代碼都可以滿足異步優化,比如在子線程中

Handler handler = new Handler();

就會崩潰,因爲在子線程中他找不到looper,解決方案

Handler handler = new Handler(Looper.getMainLooper());

但是項目中,總是會有一些代碼必須要在主線程中執行,這種情況就要放棄異步優化。
比如初始化的代碼必須在application的onCreate()中結束,在activity中調用不到就會崩潰,針對這種情況可以使用CountDownLatch

    //條件被滿足1次
    private CountDownLatch countDownLatch=new CountDownLatch(1);
    @Override
    public void onCreate() {
        super.onCreate();
        service.submit(new Runnable() {
            @Override
            public void run() {
                initMap();
                //條件被滿足
                countDownLatch.countDown();
            }
        });

        //如果條件沒滿足就等待
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //條件被滿足了,onCreate()纔會執行結束
    }

以上常規的異步方案,是有一些問題的:代碼不夠優雅維護成本高,有些操作有依賴關係,需要有先後順序執行的,不方便統計

3、異步優化升級

啓動器介紹
核心思想:充分利用CPU多核,自動梳理任務順序
啓動器流程
-代碼Task化,啓動邏輯抽象爲Task

  • 根據所有任務依賴關係排序生成一個有向無環圖
  • 多線程按照排序後的優先級依次執行
    啓動器流程圖

4、更優秀的延遲初始化方案

常規方案

  • new Handler().postDelayed
  • 痛點:時機不受控制,可能導致卡頓
    更優方案
  • 核心思想:對延遲任務進行分批初始化
    利用IdleHandler特性空閒執行
public class DelayInitDispatcher {

    private Queue<Task> mDelayTasks = new LinkedList<>();

    private MessageQueue.IdleHandler mIdleHandler = new MessageQueue.IdleHandler() {
        @Override
        public boolean queueIdle() {
            if(mDelayTasks.size()>0){
                Task task = mDelayTasks.poll();
                new DispatchRunnable(task).run();
            }
            return !mDelayTasks.isEmpty();
        }
    };

    public DelayInitDispatcher addTask(Task task){
        mDelayTasks.add(task);
        return this;
    }

    public void start(){
        Looper.myQueue().addIdleHandler(mIdleHandler);
    }

}

5、優化總方針

  • 異步,延遲,懶加載
  • 技術、、業務相結合
    注意事項
  • wall time和cpu time
    cpu time纔是優化方向,按照systrace及cpu time跑滿cpu
  • 監控的完善:線上監控多階段時間(App,Application,生命週期間隔時間),處理聚合看趨勢
  • 收斂啓動代碼修改權限,結合ci修改啓動代碼需要Review或通知
    其他方案
  • 提前加載SharePreference
    如果不加限制,可能有幾十個類在使用幾十個文件,調用get方法會異步加載配置文件,加載到內存當中,在get或者put一個屬性時候如果load到內存中的操作沒有執行完,就會阻塞進行等待
    Multidex之前加載,利用此階段的CPU
    覆寫getApplicationContext()返回this
  • 啓動階段不啓動子進程
    很多App會有多個進程,子進程會影響主進程的啓動時間,因爲子進程會共享CPU資源,導致主進程CPU緊張。
    注意啓動順序:App onCreate之前是ContentProvider,
  • 類加載優化:提前異步類加載
    每隻用一個類都是通過classloader,如果啓動太多類,會延遲啓動時間。
    通過Class.forName()只加載類本身及其靜態變量的引用類,如果new類實例可以額外加載類成員變量的引用類,主要需要根據業務情況判斷。
    哪些類需要提前異步類加載,可以通過替換系統的ClassLoader,在自定義的ClassLoader打印log
  • 啓動階段抑制GC
  • CPU鎖頻

6、模擬問題

  • 你啓動優化怎麼做的?要講整個過程
    分析現狀,確認問題:比如,在某個版本發現啓動速度變得特別卡頓,用戶反饋變多,所以進行優化,對啓動代碼進行梳理,發現啓動流程非常複雜了,通過一系列工具來確認出來在主線程中執行了太多代碼。
    針對性優化:比如進行了異步初始化,比如有些代碼優先級不是很高,可以延遲執行
    長期保持優化效果:
  • 是怎麼異步的,異步過程中遇到了哪些問題
    體現演進過程:最初採用普通異步方案,之後發現不方便,尋找新的解決方案,介紹啓動器
  • 做了啓動優化,覺得有哪些容易忽略的點
    cpu time,wall time
    注意延遲初始化是優化
    介紹下黑科技,比如類加載,cup拉高頻率
  • 版本迭代導致啓動變慢有什麼好的解決方案
    啓動器,結合CI,監控完善
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章