一線大廠資深APP性能優化系列-卡頓定位(一)

0.序

做者將近7年Android開發,經歷過不少一線公司的APP開發,如中石油,阿里,京東等,想把真正一些一線的APP裏的優秀的經驗分享出來,打算利用休息時間更新一個系列的《APP性能優化》,大約是20章節,原本是打算申請小冊的,可是也沒審覈經過,就打算免費分享了,每週大約會更新2章,喜歡的朋友記得加個關注和點贊。若有筆誤歡迎指出。java

講解的內容大致包含,異步優化,啓動優化,卡頓優化,內存優化,ARTHook, 監控耗時盲區,網絡,電量,瘦身及APP容災方案等
android

1.簡介

本篇文章是該系列文章中的第一篇,主要介紹的是在一些一線大廠的實際項目中,若是APP發生卡頓是如何進行定位問題的。主要介紹 程序的耗費時間shell


2.測量時間方式

首先,若是要查看頁面加載花費的時間有3種方式編程

  1. adb命令查看
  2. 手動打點的方式
  3. traceView

3.adb命令

只須要一行命令,就能夠查看加載頁面的時間。api

adb shell am start -W 包名/包名.Activity
複製代碼

使用後會顯示性能優化

ThisTime: 表明啓動最後一個Activity的耗時
TotalTime: 表明啓動全部的Activity的耗時
WaitTime: 表明AMS啓動全部的Activity的耗時markdown

注意點:該命令只能是獲取配置了的Activity, 其餘的無效,由於Android組件中有個 exported 屬性,沒有intent-filter時exported 屬性默認爲false,此組件只能由本應用用戶訪問,配備了intent-filter後此值改變爲true,容許外部調用。網絡

缺點: adb命令只能查看配置了的Activity,其餘的沒法查看,而且也沒法精準的查看其方法具體耗費的時間,因此由於其侷限性 並不能很好的解決咱們問題,咱們採用手動打點的方式查看。app


4.手動打點

先看代碼異步

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();

        initBugly();

        initBaiduMap();

        initJPushInterface();

        initShareSDK();
        ...
    }

    private void initBugly() throws InterruptedException {

        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); // 模擬耗費的時間
    }
}
複製代碼

代碼不用我說,項目中很常見,如今的問題是APP啓動加載很慢, 那麼如何精準的查詢到具體耗時的方法?

long startTime = System.currentTimeMillis();
  initBugly();
  Log.d("lybj", "initBugly()方法耗時:"+ (System.currentTimeMillis() - startTime));
  
  long startTime = System.currentTimeMillis();
  initBaiduMap();
  Log.d("lybj", "initBaiduMap()方法耗時:"+ (System.currentTimeMillis() - startTime));
  ...
複製代碼

這樣能夠嗎?固然不行,耦合性太大,每個方法都加,那麼測試完了,刪除代碼也是個體力活,一不當心刪錯一個,就會形成災難性的問題,在實際項目中,好比中石油的一些項目,就會採用 AOP 的方式來測量方法的耗費時長。

4.1 AOP

AOP : Aspect Oriented Programming的縮寫,意爲:面向切面編程

優勢:

  1. 針對同一問題的統一處理
  2. 無侵入添加代碼

這裏咱們使用的是Aspectj

4.2 Aspectj 的使用

1.添加依賴

根目錄的build.gradle裏

buildscript {
    ...
    dependencies {
        ...
        classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.0'
    }
}
複製代碼

app項目的build.gradle及新建的module的build.gradle裏添加

apply plugin: 'android-aspectjx'

dependencies {
    ...
    implementation 'org.aspectj:aspectjrt:1.8.+'
}
複製代碼

2.建立切面

@Aspect
public class PerformanceAop {

    @Around("call(* com.bj.performance.MyApplication.**(..))")
    public void getTime(ProceedingJoinPoint joinPoint){

        long startTime = System.currentTimeMillis();
        String methodName = joinPoint.getSignature().getName();
        try {
            joinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        Log.d("lybj", methodName + "方法耗時:"+ (System.currentTimeMillis() - startTime));
    }
}
複製代碼

看,根本無需修改任何工程代碼,就能夠獲取運行時長了,點擊運行顯示

AspectJ語法參考

缺點: 若是項目比較龐大,上百個方法,總不能所有打點,而後一個一個的分析究竟是哪一個地方運行時間過長吧,因此咱們須要一個比較直觀的工具,一眼就能看到具體哪一個方法運行時間過長。


5. traceView的使用

5.1 特色

  1. 圖形的形式展現其執行時間調用棧
  2. 信息全面,包含全部進程

5.2 使用方式

Debug.startMethodTracing("文件名");

Debug.stopMethodTracing();
複製代碼

在代碼中相應位置的地方打入埋點便可, startMethodTracing 有3個構造參數分別是

  1. tracePath:文件名/路徑
  2. bufferSize:文件的容量大小
  3. flag:TRACE_COUNT_ALLOCS 只有默認的這一種

代碼運行完成後,會在

mnt/sdcard/Android/data/包名/files

生成一個.trace後綴的文件,能夠用Profiler添加打開它。

固然也可使用Profiler的錄製功能,可是由於要測量啓動時間,點擊錄製手速並不會那麼的精準,因此採用埋點的方式獲取trace文件進行分析。

5.3 性能分析

打開文件後爲上圖所示

5.4 時間模式

上圖標籤 1 所示:wallclock time 和 cpu time

  1. Wall Clock Time:從進程開始運行到結束,時鐘走過的時間,這其中包含了進程在阻塞和等待狀態的時間。
  2. Thread Time :就是CPU執行用戶指令所用的時間。

注意:若是線程A執行函數b,可是由於函數b加了鎖,線程A進入等待狀態,那麼wallclock time也是要計算時間的,而Thread time則只是計算CPU花費在它身上的時間。

通常根據經驗來說wall duration時間久說明耗時多,而後看Thread time 這個值說明cpu花費在其身上的時間多很少。很少的話基本能夠直接異步(由於不搶佔CPU)多的話須要合理調度好執行順序。

5.5 Call Chart

上圖標籤 2 所示:Call Chart

它的做用就是能夠很直觀的查看究竟是哪裏耗時比較久,x軸爲調用的時間線,越寬的表示耗時越久,y軸爲調用的深度,也就是調用的子方法。父類在最上面,很明顯initBottomTab()方法調用是最耗時的。

鼠標懸浮能夠查看耗費時間,雙擊能夠跳轉到相應代碼

  • 橙色:系統方法
  • 藍色:第三方API(包括java語言的api)
  • 綠色:App自身方法

簡易圖以下:

5.6 Flame Chart

上圖標籤 3 所示:Flame Chart 又稱之爲火焰圖

y 軸表示調用棧,每一層都是一個函數。調用棧越深,火焰就越高,頂部就是正在執行的函數,下方都是它的父函數。 x 軸表示抽樣數,若是一個函數在 x 軸佔據的寬度越寬,就表示它被抽到的次數多,即執行的時間長。注意,x 軸不表明時間,而是全部的調用棧合併後,按字母順序排列的。

火焰圖就是看頂層的哪一個函數佔據的寬度最大。只要有"平頂"(plateaus),就表示該函數可能存在性能問題。

練習1:

假如火焰圖如上圖所示,咱們須要分析哪一個函數嗎?

答:最頂層的函數g()佔用 CPU 時間最多。d()的寬度最大,可是它直接耗用 CPU 的部分不多。b()和c()沒有直接消耗 CPU。所以,若是要調查性能問題,首先應該調查g(),其次是i()。 另外,從圖中可知a()有兩個分支b()和h(),這代表a()裏面可能有一個條件語句,而b()分支消耗的 CPU 大大高於h()。

與Call Chart區別

方法D對B(B一、B2和B3)進行屢次調用,其中一些調用B對C(C1和C3)進行調用。若是用Call Chart表示,則爲

由於B一、B2和B3共享相同的序列調用者(A→D→B)聚合,以下所示。一樣,C1和C3聚合,由於它們共享相同的序列調用者(A→D→B→C)注意不包括C2, 由於它有不一樣的調用者序列(A→D→C)。

那麼若是使用火焰圖,則表示:

也就是說,收集相同的調用序列的相同方法被收集並表示爲火焰圖中的一個較長的欄(而不是將它們顯示爲多個更短的條)

5.7 Top Down

上圖標籤 4 所示: Top Down 顯示一個函數調用列表,在該列表中展開函數節點會顯示函數的被調用方

看上圖右邊區域:

  • Self:方法調用用於執行本身的代碼而不是它的子類的時間量。
  • Children:方法調用花費的時間用於執行其被調用者,而不是其本身的代碼
  • Total:方法的Self和Children的時間的總和。這表示應用程序執行方法調用的總時間量

5.8 Bottom Up

上圖標籤 5 所示: Bottom Up 顯示一個函數調用列表,在該列表中展開函數節點將顯示函數的調用方。

Bottom Up選項卡對於那些消耗最多(或最少)CPU時間的方法的排序方法頗有用。能夠檢查每一個節點,以肯定哪些調用者在調用這些方法上花費最多的CPU時間。

  • Self:方法調用用於執行本身的代碼而不是它的子類的時間量。
  • Children:方法調用花費的時間用於執行其被調用者,而不是其本身的代碼
  • Total:方法的Self和Children的時間的總和。這表示應用程序執行方法調用的總和

簡略圖以下

缺點: 在項目中用到的,最多的仍是Call Chart 和 Top Down, 可是traceView的原理就是抓取全部線程的全部函數裏的信息,因此會致使程序變慢, 因此經常使用的是SysTrace

SysTrace 做者用的很少,有興趣的朋友能夠自行百度,用法和traceView的使用差很少,就不在分析了

相關文章
相關標籤/搜索