2021年面試【騰訊+阿里+華為】必問的Android效能最佳化解讀

南方吳彥祖_藍斯發表於2021-10-09

對於Android開發者來說,懂得基本的應用開發技能往往是不夠,因為不管是工作還是面試,都需要開發者懂得大量的效能最佳化,這對提升應用的體驗是非常重要的。對於Android開發來說,效能最佳化主要圍繞如下方面展開:啟動最佳化、渲染最佳化、記憶體最佳化、網路最佳化、卡頓檢測與最佳化、耗電最佳化、安裝包體積最佳化、安全問題等。

1,啟動最佳化

一個應用的啟動快慢是能夠直接影響使用者的使用體驗的,如果啟動較慢可能會導致使用者解除安裝放棄該應用程式。

1.1 冷啟動、熱啟動和溫啟動的最佳化

1.1.1 概念

對於Android應用程式來說,根據啟動方式可以分為冷啟動,熱啟動和溫啟動三種。

  • 冷啟動:系統不存在App程式(如APP首次啟動或APP被完全殺死)時啟動App稱為冷啟動。
  • 熱啟動:按了Home鍵或其它情況app被切換到後臺,再次啟動App的過程。
  • 溫啟動:溫啟動包含了冷啟動的一些操作,不過App程式依然存在,這代表著它比熱啟動有更多的開銷。

可以看到,熱啟動是啟動最快的,溫啟動則是介於冷啟動和熱啟動之間的一種啟動方式。下而冷啟動則是最慢的,因為它會涉及很多程式的建立,下面是冷啟動相關的任務流程:


2021年面試【騰訊+阿里+華為】必問的Android效能最佳化解讀

1.1.2 視覺最佳化

在冷啟動模式下,系統會啟動三個任務:

  • 載入並啟動應用程式。
  • 啟動後立即顯示應用程式空白的啟動視窗。
  • 建立應用程式程式。

一旦系統建立應用程式程式,應用程式程式就會進入下一階段,並完成如下的一些事情。

  • 建立app物件
  • 啟動主執行緒(main thread)
  • 建立應用入口的Activity物件
  • 填充載入佈局View
  • 在螢幕上執行View的繪製過程.measure -> layout -> draw

應用程式程式完成第一次繪製後,系統程式會交換當前顯示的背景視窗,將其替換為主活動。此時,使用者可以開始使用該應用程式了。因為App應用程式的建立過程是由手機的軟硬體決定的,所以我們只能在這個建立過程中進行一些視覺最佳化。

1.1.3 啟動主題最佳化

在冷啟動的時候,當應用程式程式被建立後,就需要設定啟動視窗的主題。目前,大部分的 應用在啟動會都會先進入一個閃屏頁(LaunchActivity) 來展示應用資訊,如果在 Application 初始化了其它第三方的服務,就會出現啟動的白屏問題。

為了更順滑無縫銜接我們的閃屏頁,可以在啟動 Activity 的 Theme中設定閃屏頁圖片,這樣啟動視窗的圖片就會是閃屏頁圖片,而不是白屏。

    <style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="android:windowBackground">@drawable/lunch</item>  //閃屏頁圖片        <item name="android:windowFullscreen">true</item>
        <item name="android:windowDrawsSystemBarBackgrounds">false</item>
    </style>複製程式碼

1.2 程式碼方面的最佳化

設定主題的方式只能應用在要求不是很高的場景,並且這種最佳化治標不治本,關鍵還在於程式碼的最佳化。為了進行最佳化,我們需要掌握一些基本的資料。

1.2.1 冷啟動耗時統計

ADB命令方式 在Android Studio的Terminal中輸入以下命令可以檢視頁面的啟動的時間,命令如下:

adb shell am start  -W packagename/[packagename].首屏Activity
複製程式碼

執行完成之後,會在控制檯輸出如下的資訊:

Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.optimize.performance/.MainActivity }
Status: ok
Activity: com.optimize.performance/.MainActivity
ThisTime: 563
TotalTime: 563
WaitTime: 575
Complete
複製程式碼

在上面的日誌中有三個欄位資訊,即ThisTime、TotalTime和WaitTime。

  • ThisTime:最後一個Activity啟動耗時
  • TotalTime:所有Activity啟動耗時
  • WaitTime:AMS啟動Activity的總耗時

日誌方式 埋點方式是另一種統計線上時間的方式,這種方式透過記錄啟動時的時間和結束的時間,然後取二者差值即可。首先,需要定義一個統計時間的工具類:

class LaunchRecord {
    companion object {        private var sStart: Long = 0
        fun startRecord() {
            sStart = System.currentTimeMillis()
        }        fun endRecord() {
            endRecord("")
        }        fun endRecord(postion: String) {
            val cost = System.currentTimeMillis() - sStart            println("===$postion===$cost")
        }
    }
}
複製程式碼

啟動時埋點我們直接在Application的attachBaseContext中進行打點。那麼啟動結束應該在哪裡打點呢?結束埋點建議是在頁面資料展示出來進行埋點。可以使用如下方法:

class MainActivity : AppCompatActivity() {    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        mTextView.viewTreeObserver.addOnDrawListener {
            LaunchRecord.endRecord("onDraw")
        }
    }    override fun onWindowFocusChanged(hasFocus: Boolean) {
        super.onWindowFocusChanged(hasFocus)
        LaunchRecord.endRecord("onWindowFocusChanged")
    }
}
複製程式碼

1.2.2 最佳化檢測工具

在做啟動最佳化的時候,可以藉助三方工具來幫助我們理清各個階段的方法或者執行緒、CPU的執行耗時等情況。這裡主要介紹以下TraceView和SysTrace兩款工具。

TraceView

TraceView是以圖形的形式展示執行時間、呼叫棧等資訊,資訊比較全面,包含所有執行緒,如下圖所示。

2021年面試【騰訊+阿里+華為】必問的Android效能最佳化解讀

使用TraceView檢測生成生成的結果會放在Andrid/data/packagename/files路徑下。因為Traceview收集的資訊比較全面,所以會導致執行開銷嚴重,整體APP的執行會變慢,因此我們無法區分是不是Traceview影響了我們的啟動時間。

SysTrace Systrace是結合Android核心資料,生成HTML報告,從報告中我們可以看到各個執行緒的執行時間以及方法耗時和CPU執行時間等。

2021年面試【騰訊+阿里+華為】必問的Android效能最佳化解讀

再API 18以上版本,可以直接使用TraceCompat來抓取資料,因為這是相容的API。

開始:TraceCompat.beginSection("tag ")
結束:TraceCompat.endSection()
複製程式碼

然後,執行如下指令碼。

python systrace.py -b 32768 -t 10 -a packagename -o outputfile.html sched gfx view wm am app複製程式碼

這裡可以大家普及下各個字端的含義:

  • b: 收集資料的大小
  • t:時間
  • a:監聽的應用包名
  • o: 生成檔案的名稱

Systrace開銷較小,屬於輕量級的工具,並且可以直觀反映CPU的利用率。

2,UI渲染最佳化

Android系統每隔16ms就會重新繪製一次Activity,因此,我們的應用必須在16ms內完成螢幕重新整理的全部邏輯操作,每一幀只能停留16ms,否則就會出現掉幀現象。Android應用卡頓與否與UI渲染有直接的關係。

2.1CPU、GPU

對於大多數手機的螢幕重新整理頻率是60hz,也就是如果在1000/60=16.67ms內沒有把這一幀的任務執行完畢,就會發生丟幀的現象,丟幀是造成介面卡頓的直接原因,渲染操作通常依賴於兩個核心元件:CPU與GPU。CPU負責包括Measure,Layout等計算操作,GPU負責Rasterization(柵格化)操作。

所謂柵格化,就是將向量圖形轉換為點陣圖的過程,手機上顯示是按照一個個畫素來顯示的,比如將一個Button、TextView等元件拆分成一個個畫素顯示到手機螢幕上。而UI渲染最佳化的目的就是減輕CPU、GPU的壓力,除去不必要的操作,保證每幀16ms以內處理完所有的CPU與GPU的計算、繪製、渲染等等操作,使UI順滑、流暢的顯示出來。

2.2 過度繪製

UI渲染最佳化的第一步就是找到Overdraw(過度繪製),即描述的是螢幕上的某個畫素在同一幀的時間內被繪製了多次。在重疊的UI佈局中,如果不可見的UI也在做繪製的操作或者後一個控制元件將前一個控制元件遮擋,會導致某些畫素區域被繪製了多次,從而增加了CPU、GPU的壓力。

那麼如何找出佈局中Overdraw的地方呢?很簡單,就是開啟手機裡開發者選項,然後將除錯GPU過度繪製的開關開啟即可,然後就可以看到應用的佈局是否被Overdraw,如下圖所示:

2021年面試【騰訊+阿里+華為】必問的Android效能最佳化解讀

藍色、淡綠、淡紅、深紅代表了4種不同程度的Overdraw情況,1x、2x、3x和4x分別表示同一畫素上同一幀的時間內被繪製了多次,1x就表示一次(最理想情況),4x表示4次(最差的情況),而我們需要消除的就是3x和4x。

2.3 解決自定義View的OverDraw

我們知道,自定義View的時候有時會重寫onDraw方法,但是Android系統是無法檢測onDraw裡面具體會執行什麼操作,從而系統無法為我們做一些最佳化。這樣對程式設計人員要求就高了,如果View有大量重疊的地方就會造成CPU、GPU資源的浪費,此時我們可以使用canvas.clipRect()來幫助系統識別那些可見的區域。

這個方法可以指定一塊矩形區域,只有在這個區域內才會被繪製,其他的區域會被忽視。下面我們透過谷歌提供的一個小的Demo進一步說明OverDraw的使用。

2021年面試【騰訊+阿里+華為】必問的Android效能最佳化解讀

在下面的程式碼中,DroidCard類封裝的是卡片的資訊,程式碼如下。

public class DroidCard {public int x;//左側繪製起點public int width;public int height;public Bitmap bitmap;public DroidCard(Resources res,int resId,int x){this.bitmap = BitmapFactory.decodeResource(res,resId);this.x = x;this.width = this.bitmap.getWidth();this.height = this.bitmap.getHeight();
 }
}
複製程式碼

自定義View的程式碼如下:

public class DroidCardsView extends View {//圖片與圖片之間的間距private int mCardSpacing = 150;//圖片與左側距離的記錄private int mCardLeft = 10;private List<DroidCard> mDroidCards = new ArrayList<DroidCard>();private Paint paint = new Paint();public DroidCardsView(Context context) {super(context);
initCards();
}public DroidCardsView(Context context, AttributeSet attrs) {super(context, attrs);
initCards();
}/**
* 初始化卡片集合
*/protected void initCards(){
Resources res = getResources();
mDroidCards.add(new DroidCard(res,R.drawable.alex,mCardLeft));
mCardLeft+=mCardSpacing;
mDroidCards.add(new DroidCard(res,R.drawable.claire,mCardLeft));
mCardLeft+=mCardSpacing;
mDroidCards.add(new DroidCard(res,R.drawable.kathryn,mCardLeft));
}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);for (DroidCard c : mDroidCards){
drawDroidCard(canvas, c);
}
invalidate();
}/**
* 繪製DroidCard
*/private void drawDroidCard(Canvas canvas, DroidCard c) {
canvas.drawBitmap(c.bitmap,c.x,0f,paint);
}
}
複製程式碼

然後,我們執行程式碼,開啟手機的overdraw開關,效果如下:

2021年面試【騰訊+阿里+華為】必問的Android效能最佳化解讀

可以看到,淡紅色區域明顯被繪製了三次,是因為圖片的重疊造成的。那怎麼解決這種問題呢?其實,分析可以發現,最下面的圖片只需要繪製三分之一即可,保證最下面兩張圖片只需要回執其三分之一最上面圖片完全繪製出來就可。最佳化後的程式碼如下:

public class DroidCardsView extends View {//圖片與圖片之間的間距private int mCardSpacing = 150;//圖片與左側距離的記錄private int mCardLeft = 10;private List<DroidCard> mDroidCards = new ArrayList<DroidCard>();private Paint paint = new Paint();public DroidCardsView(Context context) {super(context);
initCards();
}public DroidCardsView(Context context, AttributeSet attrs) {super(context, attrs);
initCards();
}/**
* 初始化卡片集合
*/protected void initCards(){
Resources res = getResources();
mDroidCards.add(new DroidCard(res, R.drawable.alex,mCardLeft));
mCardLeft+=mCardSpacing;
mDroidCards.add(new DroidCard(res, R.drawable.claire,mCardLeft));
mCardLeft+=mCardSpacing;
mDroidCards.add(new DroidCard(res, R.drawable.kathryn,mCardLeft));
}@Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);for (int i = 0; i < mDroidCards.size() - 1; i++){
drawDroidCard(canvas, mDroidCards,i);
}
drawLastDroidCard(canvas,mDroidCards.get(mDroidCards.size()-1));
invalidate();
}/**
* 繪製最後一個DroidCard
* @param canvas
* @param c
*/private void drawLastDroidCard(Canvas canvas,DroidCard c) {
canvas.drawBitmap(c.bitmap,c.x,0f,paint);
}/**
* 繪製DroidCard
* @param canvas
* @param mDroidCards
* @param i
*/private void drawDroidCard(Canvas canvas,List<DroidCard> mDroidCards,int i) {
DroidCard c = mDroidCards.get(i);
canvas.save();
canvas.clipRect((float)c.x,0f,(float)(mDroidCards.get(i+1).x),(float)c.height);
canvas.drawBitmap(c.bitmap,c.x,0f,paint);
canvas.restore();
 }
}
複製程式碼

在上面的程式碼中,我們使用Canvas的clipRect方法,繪製之前裁剪出一個區域,這樣繪製的時候只在這區域內繪製,超出部分不會繪製出來。重新執行上面的程式碼,效果如下圖所示。

2021年面試【騰訊+阿里+華為】必問的Android效能最佳化解讀

2.4 Hierarchy Viewer

Hierarchy Viewer 是 Android Device Monitor 中內建的一種工具,可讓開發者測量佈局層次結構中每個檢視的佈局速度,以及幫助開發者查詢檢視層次結構導致的效能瓶頸。Hierarchy Viewer可以透過紅、黃、綠三種不同的顏色來區分佈局的Measure、Layout、Executive的相對效能表現情況。

開啟

  1. 將裝置連線到計算機。如果裝置上顯示對話方塊提示您允許 USB 除錯嗎?,請點按確定。
  2. 在 Android Studio 中開啟您的專案,在您的裝置上構建並執行專案。
  3. 啟動 Android Device Monitor。Android Studio 可能會顯示 Disable adb integration 對話方塊,因為一次只能有一個程式可以透過 adb 連線到裝置,並且 Android Device Monitor 正在請求連線。因此,請點選 Yes。
  4. 在選單欄中,依次選擇 Window > Open Perspective,然後點選 Hierarchy View。
  5. 在左側的 Windows 標籤中雙擊應用的軟體包名稱。這會使用應用的檢視層次結構填充相關窗格。
2021年面試【騰訊+阿里+華為】必問的Android效能最佳化解讀

注意:提升佈局效能的關鍵點是儘量保持佈局層級的扁平化,避免出現重複的巢狀佈局。如果我們寫的佈局層級比較深會嚴重增加CPU的負擔,造成效能的嚴重卡頓。

2.5 記憶體抖動

在我們最佳化過view的樹形結構和overdraw之後,可能還是感覺自己的app有卡頓和丟幀,或者滑動慢等問題,我們就要檢視一下是否存在記憶體抖動情況了。所謂記憶體抖動,指的是記憶體頻繁建立和GC造成的UI執行緒被頻繁阻塞的現象。

Android有自動管理記憶體的機制,但是對記憶體的不恰當使用仍然容易引起嚴重的效能問題。在同一幀裡面建立過多的物件是件需要特別引起注意的事情,在同一幀裡建立大量物件可能引起GC的不停操作,執行GC操作的時候,所有執行緒的任何操作都會需要暫停,直到GC操作完成。大量不停的GC操作則會顯著佔用幀間隔時間。如果在幀間隔時間裡面做了過多的GC操作,那麼就會造成頁面卡頓。

2021年面試【騰訊+阿里+華為】必問的Android效能最佳化解讀

在Android開發中,導致GC頻繁操作有兩個主要原因:

  • 記憶體抖動,所謂記憶體抖動就是短時間產生大量物件又在短時間內馬上釋放。
  • 短時間產生大量物件超出閾值,記憶體不夠,同樣會觸發GC操作。

Android的記憶體抖動可以使用Android Studio的Profiler進行檢測。

2021年面試【騰訊+阿里+華為】必問的Android效能最佳化解讀

然後,點選record記錄記憶體資訊,查詢發生記憶體抖動位置,當然也可直接透過Jump to Source定位到程式碼位置。

2021年面試【騰訊+阿里+華為】必問的Android效能最佳化解讀

為了避免發生記憶體抖動,我們需要避免在for迴圈裡面分配物件佔用記憶體,需要嘗試把物件的建立移到迴圈體之外,自定義View中的onDraw方法也需要引起注意,每次螢幕發生繪製以及動畫執行過程中,onDraw方法都會被呼叫到,避免在onDraw方法裡面執行復雜的操作,避免建立物件。對於那些無法避免需要建立物件的情況,我們可以考慮物件池模型,透過物件池來解決頻繁建立與銷燬的問題,但是這裡需要注意結束使用之後,需要手動釋放物件池中的物件。

3,記憶體最佳化

3.1 記憶體管理

3.1.1 記憶體區域

在Java的記憶體模型中,將記憶體區域劃分為方法區、堆、程式計數器、本地方法棧、虛擬機器棧五個區域,如下圖。

2021年面試【騰訊+阿里+華為】必問的Android效能最佳化解讀

方法區

  • 執行緒共享區域,用於儲存類資訊、靜態變數、常量、即時編譯器編譯出來的程式碼資料。
  • 無法滿足記憶體分配需求時會發生OOM。

  • 執行緒共享區域,是JAVA虛擬機器管理的記憶體中最大的一塊,在虛擬機器啟動時建立。
  • 存放物件例項,幾乎所有的物件例項都在堆上分配,GC管理的主要區域。

虛擬機器棧

  • 執行緒私有區域,每個java方法在執行的時候會建立一個棧幀用於儲存區域性變數表、運算元棧、動態連結、方法出口等資訊。方法從執行開始到結束過程就是棧幀在虛擬機器棧中入棧出棧過程。
  • 區域性變數表存放編譯期可知的基本資料型別、物件引用、returnAddress型別。所需的記憶體空間會在編譯期間完成分配,進入一個方法時在幀中區域性變數表的空間是完全確定的,不需要執行時改變。
  • 若執行緒申請的棧深度大於虛擬機器允許的最大深度,會丟擲SatckOverFlowError錯誤。
  • 虛擬機器動態擴充套件時,若無法申請到足夠記憶體,會丟擲OutOfMemoryError錯誤。

本地方法棧

  • 為虛擬機器中Native方法服務,對本地方法棧中使用的語言、資料結構、使用方式沒有強制規定,虛擬機器可自有實現。
  • 佔用的記憶體區大小是不固定的,可根據需要動態擴充套件。

程式計數器

  • 一塊較小的記憶體空間,執行緒私有,儲存當前執行緒執行的位元組碼行號指示器。
  • 位元組碼直譯器透過改變這個計數器的值來選取下一條需要執行的位元組碼指令:分支、迴圈、跳轉等。
  • 每個執行緒都有一個獨立的程式計數器
  • 唯一一個在java虛擬機器中不會OOM的區域

3.1.2 垃圾回收

標記清除演算法 標記清除演算法主要分為有兩個階段,首先標記出需要回收的物件,然後咋標記完成後統一回收所有標記的物件; 缺點:

  • 效率問題:標記和清除兩個過程效率都不高。
  • 空間問題:標記清除之後會導致很多不連續的記憶體碎片,會導致需要分配大物件時無法找到足夠的連續空間而不得不觸發GC的問題。

複製演算法 將可用記憶體按空間分為大小相同的兩小塊,每次只使用其中的一塊,等這塊記憶體使用完了將還存活的物件複製到另一塊記憶體上,然後將這塊記憶體區域物件整體清除掉。每次對整個半區進行記憶體回收,不會導致碎片問題,實現簡單且效率高效。 缺點: 需要將記憶體縮小為原來的一半,空間代價太高。

標記整理演算法 標記整理演算法標記過程和標記清除演算法一樣,但清除過程並不是對可回收物件直接清理,而是將所有存活物件像一端移動,然後集中清理到端邊界以外的記憶體。

分代回收演算法 當代虛擬機器垃圾回收演算法都採用分代收集演算法來收集,根據物件存活週期不同將記憶體劃分為新生代和老年代,再根據每個年代的特點採用最合適的回收演算法。

  • 新生代存活物件較少,每次垃圾回收都有大量物件死去,一般採用複製演算法,只需要付出複製少量存活物件的成本就可以實現垃圾回收;
  • 老年代存活物件較多,沒有額外空間進行分配擔保,就必須採用標記清除演算法和標記整理演算法進行回收;

3.2 記憶體洩漏

所謂記憶體洩露,指的是記憶體中存在的沒有用的確無法回收的物件。表現的現象是會導致記憶體抖動,可用記憶體減少,進而導致GC頻繁、卡頓、OOM。

下面是一段模擬記憶體洩漏的程式碼:

/**
 * 模擬記憶體洩露的Activity
 */public class MemoryLeakActivity extends AppCompatActivity implements CallBack{    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memoryleak);
        ImageView imageView = findViewById(R.id.iv_memoryleak);
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.splash);
        imageView.setImageBitmap(bitmap);        // 新增靜態類引用
        CallBackManager.addCallBack(this);
    }    @Override
    protected void onDestroy() {        super.onDestroy();//        CallBackManager.removeCallBack(this);
    }    @Override
    public void dpOperate() {        // do sth
    }
複製程式碼

當我們使用Memory Profiler工具檢視記憶體曲線,發現記憶體在不斷的上升, 如下圖所示:

2021年面試【騰訊+阿里+華為】必問的Android效能最佳化解讀

如果想分析定位具體發生記憶體洩露位置,我們可以藉助MAT工具。首先,使用MAT工具生成hprof檔案,點選dump將當前記憶體資訊轉成hprof檔案,需要對生成的檔案轉換成MAT可讀取檔案。執行一下轉換命令即可完成轉換,生成的檔案位於Android/sdk/platorm-tools路徑下。

hprof-conv 剛剛生成的hprof檔案 memory-mat.hprof複製程式碼

使用mat開啟剛剛轉換的hprof檔案,然後使用Android Studio開啟hprof檔案, 如下圖所示:

2021年面試【騰訊+阿里+華為】必問的Android效能最佳化解讀

然後點選皮膚的【Historygram】,搜尋MemoryLeakActivity,即可檢視對應的洩漏檔案的相關資訊。

2021年面試【騰訊+阿里+華為】必問的Android效能最佳化解讀

然後,檢視所有引用物件,並得到相關的引用鏈, 如下圖:

2021年面試【騰訊+阿里+華為】必問的Android效能最佳化解讀
2021年面試【騰訊+阿里+華為】必問的Android效能最佳化解讀

可以看到GC Roots是CallBackManager


2021年面試【騰訊+阿里+華為】必問的Android效能最佳化解讀

所以,我們在Activity銷燬時將CallBackManager引用移除即可。

@Overrideprotected void onDestroy() {    super.onDestroy();
    CallBackManager.removeCallBack(this);
}
複製程式碼

當然,上面只是一個MAT分析工具使用的示例,其他的記憶體洩露都可以藉助MAT分析工具解決。

3.3 大圖記憶體最佳化

在Android開發中,經常會遇到載入大圖導致記憶體洩露的問題,對於這種場景,有一個通用的解決方案,即使用ARTHook對不合理圖片進行檢測。我們知道,獲取Bitmap佔用的記憶體主要有兩種方式:

  • 透過getByteCount方法,但是需要在執行時獲取
  • width  * height  * 一個畫素所佔記憶體  * 圖片所在資源目錄壓縮比

透過ARTHook方法可以優雅的獲取不合理圖片,侵入性低,但是因為相容性問題一般線上下使用。使用ARTHook需要安裝以下依賴:

implementation 'me.weishu:epic:0.3.6'複製程式碼

然後自定義實現Hook方法,如下所示。

public class CheckBitmapHook extends XC_MethodHook {    @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable {        super.afterHookedMethod(param);
        ImageView imageView = (ImageView)param.thisObject;
        checkBitmap(imageView,imageView.getDrawable());
    }    private static void checkBitmap(Object o,Drawable drawable) {        if(drawable instanceof BitmapDrawable && o instanceof View) {            final Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();            if(bitmap != null) {                final View view = (View)o;                int width = view.getWidth();                int height = view.getHeight();                if(width > 0 && height > 0) {                    if(bitmap.getWidth() > (width <<1) && bitmap.getHeight() > (height << 1)) {
                        warn(bitmap.getWidth(),bitmap.getHeight(),width,height,                                new RuntimeException("Bitmap size is too large"));
                    }
                } else {                    final Throwable stacktrace = new RuntimeException();
                    view.getViewTreeObserver().addOnPreDrawListener(                            new ViewTreeObserver.OnPreDrawListener() {                                @Override public boolean onPreDraw() {                                    int w = view.getWidth();                                    int h = view.getHeight();                                    if(w > 0 && h > 0) {                                        if (bitmap.getWidth() >= (w << 1)
                                                && bitmap.getHeight() >= (h << 1)) {
                                            warn(bitmap.getWidth(), bitmap.getHeight(), w, h, stacktrace);
                                        }
                                        view.getViewTreeObserver().removeOnPreDrawListener(this);
                                    }                                    return true;
                                }
                            });
                }
            }
        }
    }    private static void warn(int bitmapWidth, int bitmapHeight, int viewWidth, int viewHeight, Throwable t) {
        String warnInfo = new StringBuilder("Bitmap size too large: ")
                .append("\n real size: (").append(bitmapWidth).append(',').append(bitmapHeight).append(')')
                .append("\n desired size: (").append(viewWidth).append(',').append(viewHeight).append(')')
                .append("\n call stack trace: \n").append(Log.getStackTraceString(t)).append('\n')
                .toString();
        LogUtils.i(warnInfo);
複製程式碼

最後,在Application初始化時注入Hook。

DexposedBridge.hookAllConstructors(ImageView.class, new XC_MethodHook() {    @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable {        super.afterHookedMethod(param);
        DexposedBridge.findAndHookMethod(ImageView.class,"setImageBitmap", Bitmap.class,                new CheckBitmapHook());
    }
});
複製程式碼

3.4 線上監控

3.4.1 常規方案

方案一 在特定場景中獲取當前佔用記憶體大小,如果當前記憶體大小超過系統最大記憶體80%,對當前記憶體進行一次Dump(Debug.dumpHprofData()),選擇合適時間將hprof檔案進行上傳,然後透過MAT工具手動分析該檔案。

缺點:

  • Dump檔案比較大,和使用者使用時間、物件樹正相關。
  • 檔案較大導致上傳失敗率較高,分析困難。

方案二 將LeakCannary帶到線上,新增預設懷疑點,對懷疑點進行記憶體洩露監控,發現記憶體洩露回傳到服務端。

缺點:

  • 通用性較低,需要預設懷疑點,對沒有預設懷疑點的地方監控不到。
  • LeakCanary分析比較耗時、耗記憶體,有可能會發生OOM。

3.4.2 LeakCannary改造

改造主要涉及以下幾點:

  • 將需要預設懷疑點改為自動尋找懷疑點,自動將前記憶體中所佔記憶體較大的物件類中設定懷疑點。
  • LeakCanary分析洩露鏈路比較慢,改造為只分析Retain size大的物件。
  • 分析過程會OOM,是因為LeakCannary分析時會將分析物件全部載入到記憶體當中,我們可以記錄下分析物件的個數和佔用大小,對分析物件進行裁剪,不全部載入到記憶體當中。

完成的改造步驟如下:

  1. 監控常規指標:待機記憶體、重點模組佔用記憶體、OOM率
  2. 監控APP一個生命週期內和重點模組介面的生命週期內的GC次數、GC時間等
  3. 將定製的LeakCanary帶到線上,自動化分析線上的記憶體洩露

4,網路最佳化

4.1 網路最佳化的影響

App的網路連線對於使用者來說, 影響很多, 且多數情況下都很直觀, 直接影響使用者對這個App的使用體驗. 其中較為重要的幾點:  流量 :App的流量消耗對使用者來說是比較敏感的, 畢竟流量是花錢的嘛. 現在大部分人的手機上都有安裝流量監控的工具App, 用來監控App的流量使用. 如果我們的App這方面沒有控制好, 會給使用者不好的使用體驗。  電量 :電量相對於使用者來說, 沒有那麼明顯. 一般使用者可能不會太注意. 但是如電量最佳化中的那樣, 網路連線(radio)是對電量影響很大的一個因素. 所以我們也要加以注意。  使用者等待 :也就是使用者體驗, 良好的使用者體驗, 才是我們留住使用者的第一步. 如果App請求等待時間長, 會給使用者網路卡, 應用反應慢的感覺, 如果有對比, 有替代品, 我們的App很可能就會被使用者無情拋棄。

4.2 網路分析工具

網路分析可以藉助的工具有Monitor、代理工具等。

4.2.1 Network Monitor

Android Studio內建的Monitor工具提供了一個Network Monitor,可以幫助開發者進行網路分析, 下面是一個典型的Network Monitor示意圖:

2021年面試【騰訊+阿里+華為】必問的Android效能最佳化解讀
  • Rx --- R(ecive) 表示下行流量,即下載接收。
  • Tx --- T(ransmit) 表示上行流量,即上傳傳送。

Network Monitor實時跟蹤選定應用的資料請求情況。 我們可以連上手機,選定除錯應用程式, 然後在App上操作我們需要分析的頁面請求。

4.2.2 代理工具

網路代理工具有兩個作用,一個是截獲網路請求響應包, 分析網路請求;另一個設定代理網路, 移動App開發中一般用來做不同網路環境的測試, 例如Wifi/4G/3G/弱網等。

現在,可以使用的代理工具有很多, 諸如Wireshark, Fiddler, Charles等。

4.3 網路最佳化方案

對於網路最佳化來說,主要從兩個方面進行著手進行最佳化:

  1. 減少活躍時間:減少網路資料獲取的頻次,從而就減少了radio的電量消耗以及控制電量使用。
  2. 壓縮資料包的大小:壓縮資料包可以減少流量消耗,也可以讓每次請求更快, 。

基於上面的方案,可以得到以下一些常見的解決方案:

4.3.1 介面設計

1,API設計 App與伺服器之間的API設計要考慮網路請求的頻次,資源的狀態等。以便App可以以較少的請求來完成業務需求和介面的展示。

例如, 註冊登入. 正常會有兩個API, 註冊和登入, 但是設計API時我們應該給註冊介面包含一個隱式的登入. 來避免App在註冊後還得請求一次登入介面。

2,使用Gzip壓縮

使用Gzip來壓縮request和response, 減少傳輸資料量, 從而減少流量消耗。使用Retrofit等網路請求框架進行網路請求時,預設進行了Gzip的壓縮。

3,使用Protocol Buffer 以前,我們傳輸資料使用的是XML, 後來使用JSON代替了XML, 很大程度上也是為了可讀性和減少資料量。而在遊戲開發中,為了保證資料的準確和及時性,Google推出了Protocol Buffer資料交換格式。

4,依據網路情況獲取不同解析度的圖片 我們使用淘寶或者京東的時候,會看到應用會根據網路情況,獲取不同解析度的圖片,避免流量的浪費以及提升使用者的體驗。

4.3.2 合理使用網路快取

適當的使用快取, 不僅可以讓我們的應用看起來更快, 也能避免一些不必要的流量消耗,帶來更好的使用者體驗。

1,打包網路請求

當介面設計不能滿足我們的業務需求時。例如,可能一個介面需要請求多個介面,或是網路良好,處於Wifi狀態下時我們想獲取更多的資料等。這時就可以打包一些網路請求, 例如請求列表的同時, 獲取Header點選率較高的的item項的詳情資料。

2,監聽裝置狀態 為了提升使用者體驗,我們可以對裝置的使用狀態進行監聽,然後再結合JobScheduler來執行網路請求.。比方說Splash閃屏廣告圖片, 我們可以在連線到Wifi時下載快取到本地; 新聞類的App可以在充電,Wifi狀態下做離線快取。

4.3.3 弱網測試&最佳化

1,弱網測試 有幾種方式來模擬弱網進行測試:

Android Emulator 通常,我們建立和啟動Android模擬器可以設定網路速度和延遲, 如下圖所示:

2021年面試【騰訊+阿里+華為】必問的Android效能最佳化解讀

然後,我們在啟動時使用的emulator命令如下。

$emulator -netdelay gprs -netspeed gsm -avd Nexus_5_API_22
複製程式碼

2,網路代理工具 使用網路代理工具也可以模擬網路情況。以Charles為例,保持手機和PC處於同一個區域網, 在手機端wifi設定高階設定中設定代理方式為手動, 代理ip填寫PC端ip地址, 埠號預設8888。

2021年面試【騰訊+阿里+華為】必問的Android效能最佳化解讀

5,耗電最佳化

事實上,如果我們的應用需要播放影片、需要獲取 GPS 資訊,亦或者是遊戲應用,耗電都是比較嚴重的。如何判斷哪些耗電是可以避免,或者是需要去最佳化的呢?我們可以開啟手機自帶的耗電排行榜,發現“王者榮耀”使用了 7 個多小時,這時使用者對“王者榮耀”的耗電是有預期的。

2021年面試【騰訊+阿里+華為】必問的Android效能最佳化解讀

5.1 最佳化方向

假設這個時候發現某個應用他根本沒怎麼使用,但是耗電卻非常多,那麼就會被系統無情的殺掉。 所以耗電最佳化的第一個方向是最佳化應用的後臺耗電。

知道了系統是如何計算耗電的,我們也就可以知道應用在後臺不應該做什麼,例如長時間獲取 WakeLock、WiFi 和藍芽的掃描等,以及後臺服務。為什麼說耗電最佳化第一個方向就是最佳化應用後臺耗電,因為大部分廠商預裝專案要求最嚴格的正是應用後臺待機耗電。

2021年面試【騰訊+阿里+華為】必問的Android效能最佳化解讀


2021年面試【騰訊+阿里+華為】必問的Android效能最佳化解讀


當然前臺耗電我們不會完全不管,但是標準會放鬆很多。再來看看下面這張圖,如果系統對你的應用彈出這個對話方塊,可能對於微信來說,使用者還可以忍受,但是對其他大多數的應用來說,可能很多使用者就直接把你加入到後臺限制的名單中了。

耗電最佳化的第二個方向是符合系統的規則,讓系統認為你耗電是正常的。

而 Android P 及以上版本是透過 Android Vitals 監控後臺耗電,所以我們需要符合 Android Vitals 的規則, 目前它的具體規則如下:

2021年面試【騰訊+阿里+華為】必問的Android效能最佳化解讀

可以看到,Android系統目前比較關心是後臺 Alarm 喚醒、後臺網路、後臺 WiFi 掃描以及部分長時間 WakeLock 阻止系統後臺休眠,因為這些都有可能導致耗電問題。

5.2 耗電監控

5.2.1 Android Vitals

Android Vitals 的幾個關於電量的監控方案與規則,可以幫助我們進行耗電監測。

  • Alarm Manager wakeup 喚醒過多
  • 頻繁使用區域性喚醒鎖
  • 後臺網路使用量過高
  • 後臺 WiFi Scans 過多
    在使用了一段時間之後,我發現它並不是那麼好用。以 Alarm wakeup 為例,Vitals 以每小時超過 10 次作為規則。由於這個規則無法做修改,很多時候我們可能希望針對不同的系統版本做更加細緻的區分。其次跟 Battery Historian 一樣,我們只能拿到 wakeup 的標記的元件,拿不到申請的堆疊,也拿不到當時手機是否在充電、剩餘電量等資訊。 下圖是wakeup拿到的資訊。
2021年面試【騰訊+阿里+華為】必問的Android效能最佳化解讀

對於網路、WiFi scans 以及 WakeLock 也是如此。雖然 Vitals 幫助我們縮小了排查的範圍,但是依然沒辦法確認問題的具體原因。

5.3 如何監控耗電

前面說過,Android Vitals並不是那麼好用,而且對於國內的應用來說其實也根本無法使用。那我們的耗電監控系統應該監控哪些內容,又應該如何做呢?首先,我們看一下耗電監控具體應該怎麼做呢?

  • 監控資訊:簡單來說系統關心什麼,我們就監控什麼,而且應該以後臺耗電監控為主。類似 Alarm wakeup、WakeLock、WiFi scans、Network 都是必須的,其他的可以根據應用的實際情況。如果是地圖應用,後臺獲取 GPS 是被允許的;如果是計步器應用,後臺獲取 Sensor 也沒有太大問題。
  • 現場資訊:監控系統希望可以獲得完整的堆疊資訊,比如哪一行程式碼發起了 WiFi scans、哪一行程式碼申請了 WakeLock 等。還有當時手機是否在充電、手機的電量水平、應用前臺和後臺時間、CPU 狀態等一些資訊也可以幫助我們排查某些問題。
  • 提煉規則:最後我們需要將監控的內容抽象成規則,當然不同應用監控的事項或者引數都不太一樣。 由於每個應用的具體情況都不太一樣,可以用來參考的簡單規則。
2021年面試【騰訊+阿里+華為】必問的Android效能最佳化解讀
2021年面試【騰訊+阿里+華為】必問的Android效能最佳化解讀

5.3.2 Hook方案

明確了我們需要監控什麼以及具體的規則之後,接下來我們來看一下電量監控的技術方案。這裡首先來看一下Hook 方案。Hook 方案的好處在於使用者接入非常簡單,不需要去修改程式碼,接入的成本比較低。下面我以幾個比較常用的規則為例,看看如何使用 Java Hook 達到監控的目的。

1,WakeLock WakeLock 用來阻止 CPU、螢幕甚至是鍵盤的休眠。類似 Alarm、JobService 也會申請 WakeLock 來完成後臺 CPU 操作。WakeLock 的核心控制程式碼都在PowerManagerService中,實現的方法非常簡單,如下所示。

// 代理 PowerManagerServiceProxyHook().proxyHook(context.getSystemService(Context.POWER_SERVICE), "mService", this);@Overridepublic void beforeInvoke(Method method, Object[] args) {    // 申請 Wakelock
    if (method.getName().equals("acquireWakeLock")) {        if (isAppBackground()) {            // 應用後臺邏輯,獲取應用堆疊等等     
         } else {            // 應用前臺邏輯,獲取應用堆疊等等
         }    // 釋放 Wakelock
    } else if (method.getName().equals("releaseWakeLock")) {       // 釋放的邏輯    
    }
}
複製程式碼

2,Alarm Alarm 用來做一些定時的重複任務,它一共有四個型別,其中ELAPSED_REALTIME_WAKEUP和RTC_WAKEUP型別都會喚醒裝置。同樣,Alarm 的核心控制邏輯都在AlarmManagerService中,實現如下。

// 代理 AlarmManagerServicenew ProxyHook().proxyHook(context.getSystemService
(Context.ALARM_SERVICE), "mService", this);public void beforeInvoke(Method method, Object[] args) {    // 設定 Alarm
    if (method.getName().equals("set")) {        // 不同版本引數型別的適配,獲取應用堆疊等等
    // 清除 Alarm
    } else if (method.getName().equals("remove")) {        // 清除的邏輯
    }
}
複製程式碼

除了WakeLock和Alarm外,對於後臺 CPU,我們可以使用卡頓監控相關的方法;對於後臺網路,同樣我們可以透過網路監控相關的方法;對於 GPS 監控,我們可以透過 Hook 代理LOCATION_SERVICE;對於 Sensor,我們透過 Hook SENSOR_SERVICE中的“mSensorListeners”,可以拿到部分資訊。

最後,我們將申請資源到的堆疊資訊儲存起來。當我們觸發某個規則上報問題的時候,可以將收集到的堆疊資訊、電池是否充電、CPU 資訊、應用前後臺時間等輔助資訊上傳到後臺即可。

5.3.3 插樁法

使用 Hook 方式雖然簡單,但是某些規則可能不太容易找到合適的 Hook 點,而且在 Android P 之後,很多的 Hook 點都不支援了。出於相容性考慮,我首先想到的是插樁法。以 WakeLock 為例:

public class WakelockMetrics {
    // Wakelock 申請
    public void acquire(PowerManager.WakeLock wakelock) {
        wakeLock.acquire();        // 在這裡增加 Wakelock 申請監控邏輯
    }    // Wakelock 釋放
    public void release(PowerManager.WakeLock wakelock, int flags) {
        wakelock.release();        // 在這裡增加 Wakelock 釋放監控邏輯
    }
}
複製程式碼

如果你對電量消耗又研究,那麼肯定知道Facebook 的耗電監控的開源庫Battery-Metrics,它監控的資料非常全,包括 Alarm、WakeLock、Camera、CPU、Network 等,而且也有收集電量充電狀態、電量水平等資訊。不過,遺憾的是Battery-Metrics 只是提供了一系列的基礎類,在實際使用時開發者仍然需要修改大量的原始碼。

6,安裝包最佳化

現在市面上的App,小則幾十M,大則上百M。安裝包越小,下載時省流量,使用者好的體驗,下載更快,安裝更快。那麼對於安裝包,我們可以從哪些方面著手進行最佳化呢?

6,1 常用的最佳化策略

1,清理無用資源 在android打包過程中,如果程式碼有涉及資源和程式碼的引用,那麼就會打包到App中,為了防止將這些廢棄的程式碼和資源打包到App中,我們需要及時地清理這些無用的程式碼和資源來減小App的體積。清理的方法是,依次點選android Studio的【Refactor】->【Remove unused Resource】, 如下圖所示:

2021年面試【騰訊+阿里+華為】必問的Android效能最佳化解讀

2,使用Lint工具

Lint工具還是很有用的,它給我們需要最佳化的點:

  • 檢測沒有用的佈局並且刪除
  • 把未使用到的資源刪除
  • 建議String.xml有一些沒有用到的字元也刪除掉

3,開啟shrinkResources去除無用資源 在build.gradle 裡面配置shrinkResources true,在打包的時候會自動清除掉無用的資源,但經過實驗發現打出的包並不會,而是會把部分無用資源用更小的東西代替掉。注意,這裡的“無用”是指呼叫圖片的所有父級函式最終是廢棄程式碼,而shrinkResources true 只能去除沒有任何父函式呼叫的情況。

    android {
        buildTypes {
            release {
                shrinkResources true
            }
        }
    }
複製程式碼

除此之外,大部分應用其實並不需要支援幾十種語言的國際化支援,還可以刪除語言支援檔案。

6.2 資源壓縮

在android開發中,內建的圖片是很多的,這些圖片佔用了大量的體積,因此為了縮小包的體積,我們可以對資源進行壓縮。常用的方法有:

  1. 使用壓縮過的圖片:使用壓縮過的圖片,可以有效降低App的體積。
  2. 只用一套圖片:對於絕大對數APP來說,只需要取一套設計圖就足夠了。
  3. 使用不帶alpha值的jpg圖片:對於非透明的大圖,jpg將會比png的大小有顯著的優勢,雖然不是絕對的,但是通常會減小到一半都不止。
  4. 使用tinypng有失真壓縮:支援上傳PNG圖片到官網上壓縮,然後下載儲存,在保持alpha通道的情況下對PNG的壓縮可以達到1/3之內,而且用肉眼基本上分辨不出壓縮的損失。
  5. 使用webp格式:webp支援透明度,壓縮比比,佔用的體積比JPG圖片更小。從Android 4.0+開始原生支援,但是不支援包含透明度,直到Android 4.2.1+才支援顯示含透明度的webp,使用的時候要特別注意。
  6. 使用svg:向量圖是由點與線組成,和點陣圖不一樣,它再放大也能保持清晰度,而且使用向量圖比點陣圖設計方案能節約30~40%的空間。
  7. 對打包後的圖片進行壓縮:使用7zip壓縮方式對圖片進行壓縮,可以直接使用微信開源的AndResGuard壓縮方案。
    apply plugin: 'AndResGuard'
    buildscript {
        dependencies {
            classpath 'com.tencent.mm:AndResGuard-gradle-plugin:1.1.7'
        }
    }
    andResGuard {
        mappingFile = null
        use7zip = true
        useSign = true
        keepRoot = false
        // add <your_application_id>.R.drawable.icon into whitelist.
        // because the launcher will get thgge icon with his name
        def packageName = <your_application_id>
                whiteList = [        //for your icon
        packageName + ".R.drawable.icon",                //for fabric
                packageName + ".R.string.com.crashlytics.*",                //for umeng update
                packageName + ".R.string.umeng*",
                packageName + ".R.string.UM*",
                packageName + ".R.string.tb_*",
                packageName + ".R.layout.umeng*",
                packageName + ".R.layout.tb_*",
                packageName + ".R.drawable.umeng*",
                packageName + ".R.drawable.tb_*",
                packageName + ".R.anim.umeng*",
                packageName + ".R.color.umeng*",
                packageName + ".R.color.tb_*",
                packageName + ".R.style.*UM*",
                packageName + ".R.style.umeng*",
                packageName + ".R.id.umeng*"
        ]
        compressFilePattern = [        "*.png",                "*.jpg",                "*.jpeg",                "*.gif",                "resources.arsc"
        ]
        sevenzip {
            artifact = 'com.tencent.mm:SevenZip:1.1.7'
            //path = "/usr/local/bin/7za"
        }
    }
複製程式碼

6.3 資源動態載入

在前端開發中,動態載入資源可以有效減小apk的體積。除此之外,只提供對主流架構的支援,比如arm,對於mips和x86架構可以考慮不支援,這樣可以大大減小APK的體積。

當然,除了上面提到的場景的最佳化場景外,Android App的最佳化還包括儲存最佳化、多執行緒最佳化以及奔潰處理等方面。
更多Android技術分享可以關注@我,也可以加入QQ群號:Android進階學習群:345659112,一起學習交流。
作者:xiangzhihong
連結:
來源:稀土掘金
著作權歸作者所有。商業轉載請聯絡作者獲得授權,非商業轉載請註明出處。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69983917/viewspace-2795185/,如需轉載,請註明出處,否則將追究法律責任。

相關文章