Android 面試之必問性能優化

對於Android開發者來講,懂得基本的應用開發技能每每是不夠,由於無論是工做仍是面試,都須要開發者懂得大量的性能優化,這對提高應用的體驗是很是重要的。對於Android開發來講,性能優化主要圍繞以下方面展開:啓動優化、渲染優化、內存優化、網絡優化、卡頓檢測與優化、耗電優化、安裝包體積優化、安全問題等。html

1,啓動優化

一個應用的啓動快慢是可以直接影響用戶的使用體驗的,若是啓動較慢可能會致使用戶卸載放棄該應用程序。前端

1.1 冷啓動、熱啓動和溫啓動的優化

1.1.1 概念

對於Android應用程序來講,根據啓動方式能夠分爲冷啓動,熱啓動和溫啓動三種。java

  • 冷啓動:系統不存在App進程(如APP首次啓動或APP被徹底殺死)時啓動App稱爲冷啓動。
  • 熱啓動:按了Home鍵或其它狀況app被切換到後臺,再次啓動App的過程。
  • 溫啓動:溫啓動包含了冷啓動的一些操做,不過App進程依然存在,這表明着它比熱啓動有更多的開銷。

能夠看到,熱啓動是啓動最快的,溫啓動則是介於冷啓動和熱啓動之間的一種啓動方式。下而冷啓動則是最慢的,由於它會涉及不少進程的建立,下面是冷啓動相關的任務流程:
在這裏插入圖片描述python

1.1.2 視覺優化

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

  • 加載並啓動應用程序。
  • 啓動後當即顯示應用程序空白的啓動窗口。
  • 建立應用程序進程。

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

  • 建立app對象
  • 啓動主線程(main thread)
  • 建立應用入口的Activity對象
  • 填充加載佈局View
  • 在屏幕上執行View的繪製過程.measure -> layout -> draw

應用程序進程完成第一次繪製後,系統進程會交換當前顯示的背景窗口,將其替換爲主活動。此時,用戶能夠開始使用該應用程序了。由於App應用進程的建立過程是由手機的軟硬件決定的,因此咱們只能在這個建立過程當中進行一些視覺優化。面試

1.1.3 啓動主題優化

在冷啓動的時候,當應用程序進程被建立後,就須要設置啓動窗口的主題。目前,大部分的 應用在啓動會都會先進入一個閃屏頁(LaunchActivity) 來展現應用信息,若是在 Application 初始化了其它第三方的服務,就會出現啓動的白屏問題。算法

爲了更順滑無縫銜接咱們的閃屏頁,能夠在啓動 Activity 的 Theme中設置閃屏頁圖片,這樣啓動窗口的圖片就會是閃屏頁圖片,而不是白屏。shell

<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是以圖形的形式展現執行時間、調用棧等信息,信息比較全面,包含全部線程,以下圖所示。
在這裏插入圖片描述
使用TraceView檢測生成生成的結果會放在Andrid/data/packagename/files路徑下。由於Traceview收集的信息比較全面,因此會致使運行開銷嚴重,總體APP的運行會變慢,所以咱們沒法區分是否是Traceview影響了咱們的啓動時間。

SysTrace
Systrace是結合Android內核數據,生成HTML報告,從報告中咱們能夠看到各個線程的執行時間以及方法耗時和CPU執行時間等。

在這裏插入圖片描述
再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,以下圖所示。
在這裏插入圖片描述
藍色、淡綠、淡紅、深紅表明了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的使用。
在這裏插入圖片描述
在下面的代碼中,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));
}

@Override
protected 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開關,效果以下:
在這裏插入圖片描述
能夠看到,淡紅色區域明顯被繪製了三次,是由於圖片的重疊形成的。那怎麼解決這種問題呢?其實,分析能夠發現,最下面的圖片只須要繪製三分之一便可,保證最下面兩張圖片只須要回執其三分之一最上面圖片徹底繪製出來就可。優化後的代碼以下:

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));
}

@Override
protected 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方法,繪製以前裁剪出一個區域,這樣繪製的時候只在這區域內繪製,超出部分不會繪製出來。從新運行上面的代碼,效果以下圖所示。
在這裏插入圖片描述

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 標籤中雙擊應用的軟件包名稱。這會使用應用的視圖層次結構填充相關窗格。

在這裏插入圖片描述
提高佈局性能的關鍵點是儘可能保持佈局層級的扁平化,避免出現重複的嵌套佈局。若是咱們寫的佈局層級比較深會嚴重增長CPU的負擔,形成性能的嚴重卡頓,關於Hierarchy Viewer的使用能夠參考:使用 Hierarchy Viewer 分析佈局

2.5 內存抖動

在咱們優化過view的樹形結構和overdraw以後,可能仍是感受本身的app有卡頓和丟幀,或者滑動慢等問題,咱們就要查看一下是否存在內存抖動狀況了。所謂內存抖動,指的是內存頻繁建立和GC形成的UI線程被頻繁阻塞的現象。

Android有自動管理內存的機制,可是對內存的不恰當使用仍然容易引發嚴重的性能問題。在同一幀裏面建立過多的對象是件須要特別引發注意的事情,在同一幀裏建立大量對象可能引發GC的不停操做,執行GC操做的時候,全部線程的任何操做都會須要暫停,直到GC操做完成。大量不停的GC操做則會顯著佔用幀間隔時間。若是在幀間隔時間裏面作了過多的GC操做,那麼就會形成頁面卡頓。
在這裏插入圖片描述
在Android開發中,致使GC頻繁操做有兩個主要緣由:

  • 內存抖動,所謂內存抖動就是短期產生大量對象又在短期內立刻釋放。
  • 短期產生大量對象超出閾值,內存不夠,一樣會觸發GC操做。

Android的內存抖動可使用Android Studio的Profiler進行檢測。
在這裏插入圖片描述
而後,點擊record記錄內存信息,查找發生內存抖動位置,固然也可直接經過Jump to Source定位到代碼位置。
在這裏插入圖片描述

爲了不發生內存抖動,咱們須要避免在for循環裏面分配對象佔用內存,須要嘗試把對象的建立移到循環體以外,自定義View中的onDraw方法也須要引發注意,每次屏幕發生繪製以及動畫執行過程當中,onDraw方法都會被調用到,避免在onDraw方法裏面執行復雜的操做,避免建立對象。對於那些沒法避免須要建立對象的狀況,咱們能夠考慮對象池模型,經過對象池來解決頻繁建立與銷燬的問題,可是這裏須要注意結束使用以後,須要手動釋放對象池中的對象。

3,內存優化

3.1 內存管理

在前面Java基礎環節,咱們對Java的內存管理模型也作了基本的介紹,參考連接:Android 面試之必問Java基礎

3.1.1 內存區域

在Java的內存模型中,將內存區域劃分爲方法區、堆、程序計數器、本地方法棧、虛擬機棧五個區域,以下圖。
在這裏插入圖片描述
方法區

  • 線程共享區域,用於存儲類信息、靜態變量、常量、即時編譯器編譯出來的代碼數據。
  • 沒法知足內存分配需求時會發生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工具查看內存曲線,發現內存在不斷的上升,以下圖所示。
在這裏插入圖片描述
若是想分析定位具體發生內存泄露位置,咱們能夠藉助MAT工具。首先,使用MAT工具生成hprof文件,點擊dump將當前內存信息轉成hprof文件,須要對生成的文件轉換成MAT可讀取文件。執行一下轉換命令便可完成轉換,生成的文件位於Android/sdk/platorm-tools路徑下。

hprof-conv 剛剛生成的hprof文件 memory-mat.hprof

使用mat打開剛剛轉換的hprof文件,而後使用Android Studio打開hprof文件,以下圖所示。
在這裏插入圖片描述
而後點擊面板的【Historygram】,搜索MemoryLeakActivity,便可查看對應的泄漏文件的相關信息。
在這裏插入圖片描述
而後,查看全部引用對象,並獲得相關的引用鏈,以下圖。
在這裏插入圖片描述
在這裏插入圖片描述
能夠看到GC Roots是CallBackManager
在這裏插入圖片描述
因此,咱們在Activity銷燬時將CallBackManager引用移除便可。

@Override
protected 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示意圖。
在這裏插入圖片描述

  • 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模擬器能夠設置網絡速度和延遲,以下圖所示。
在這裏插入圖片描述
而後,咱們在啓動時使用的emulator命令以下。

$emulator -netdelay gprs -netspeed gsm -avd Nexus_5_API_22

2,網絡代理工具
使用網絡代理工具也能夠模擬網絡狀況。以Charles爲例,保持手機和PC處於同一個局域網, 在手機端wifi設置高級設置中設置代理方式爲手動, 代理ip填寫PC端ip地址, 端口號默認8888。

在這裏插入圖片描述

5,耗電優化

事實上,若是咱們的應用須要播放視頻、須要獲取 GPS 信息,亦或者是遊戲應用,耗電都是比較嚴重的。如何判斷哪些耗電是能夠避免,或者是須要去優化的呢?咱們能夠打開手機自帶的耗電排行榜,發現「王者榮耀」使用了 7 個多小時,這時用戶對「王者榮耀」的耗電是有預期的。
在這裏插入圖片描述

5.1 優化方向

假設這個時候發現某個應用他根本沒怎麼使用,可是耗電卻很是多,那麼就會被系統無情的殺掉。因此耗電優化的第一個方向是優化應用的後臺耗電。

知道了系統是如何計算耗電的,咱們也就能夠知道應用在後臺不該該作什麼,例如長時間獲取 WakeLock、WiFi 和藍牙的掃描等,以及後臺服務。爲何說耗電優化第一個方向就是優化應用後臺耗電,由於大部分廠商預裝項目要求最嚴格的正是應用後臺待機耗電。

在這裏插入圖片描述

在這裏插入圖片描述
固然前臺耗電咱們不會徹底無論,可是標準會放鬆不少。再來看看下面這張圖,若是系統對你的應用彈出這個對話框,可能對於微信來講,用戶還能夠忍受,可是對其餘大多數的應用來講,可能不少用戶就直接把你加入到後臺限制的名單中了。

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

而 Android P 及以上版本是經過 Android Vitals 監控後臺耗電,因此咱們須要符合 Android Vitals 的規則,目前它的具體規則以下。
在這裏插入圖片描述
能夠看到,Android系統目前比較關心是後臺 Alarm 喚醒、後臺網絡、後臺 WiFi 掃描以及部分長時間 WakeLock 阻止系統後臺休眠,由於這些都有可能致使耗電問題。

5.2 耗電監控

5.2.1 Android Vitals

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

在使用了一段時間以後,我發現它並非那麼好用。以 Alarm wakeup 爲例,Vitals 以每小時超過 10 次做爲規則。因爲這個規則沒法作修改,不少時候咱們可能但願針對不一樣的系統版本作更加細緻的區分。其次跟 Battery Historian 同樣,咱們只能拿到 wakeup 的標記的組件,拿不到申請的堆棧,也拿不到當時手機是否在充電、剩餘電量等信息。 下圖是wakeup拿到的信息。

在這裏插入圖片描述
對於網絡、WiFi scans 以及 WakeLock 也是如此。雖然 Vitals 幫助咱們縮小了排查的範圍,可是依然沒辦法確認問題的具體緣由。

5.3 如何監控耗電

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

  • 監控信息:簡單來講系統關心什麼,咱們就監控什麼,並且應該之後臺耗電監控爲主。相似 Alarm wakeup、WakeLock、WiFi scans、Network 都是必須的,其餘的能夠根據應用的實際狀況。若是是地圖應用,後臺獲取 GPS 是被容許的;若是是計步器應用,後臺獲取 Sensor 也沒有太大問題。
  • 現場信息:監控系統但願能夠得到完整的堆棧信息,好比哪一行代碼發起了 WiFi scans、哪一行代碼申請了 WakeLock 等。還有當時手機是否在充電、手機的電量水平、應用前臺和後臺時間、CPU 狀態等一些信息也能夠幫助咱們排查某些問題。
  • 提煉規則:最後咱們須要將監控的內容抽象成規則,固然不一樣應用監控的事項或者參數都不太同樣。 因爲每一個應用的具體狀況都不太同樣,能夠用來參考的簡單規則。

在這裏插入圖片描述
在這裏插入圖片描述

5.3.2 Hook方案

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

1,WakeLock
WakeLock 用來阻止 CPU、屏幕甚至是鍵盤的休眠。相似 Alarm、JobService 也會申請 WakeLock 來完成後臺 CPU 操做。WakeLock 的核心控制代碼都在PowerManagerService中,實現的方法很是簡單,以下所示。

// 代理 PowerManagerService
ProxyHook().proxyHook(context.getSystemService(Context.POWER_SERVICE), "mService", this);

@Override
public 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中,實現以下。

// 代理 AlarmManagerService
new 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】,以下圖所示。
在這裏插入圖片描述
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的優化還包括存儲優化、多線程優化以及奔潰處理等方面。

相關文章
相關標籤/搜索