Android性能優化實踐

在這裏插入圖片描述

2019年5月30號:
更新內存泄漏相關內容,新增使用系統服務引起的內存泄漏相關內容。
更新內存泄漏未關閉資源對象內存泄露,新增WebView擴展,介紹WebView的內存分配並提出解決方案。
2019年5月29號:
更新內存優化相關內容,新增內存管理介紹、內存抖動。
2019年5月28號:
用戶zhangkai2811指出Fresco拼寫錯誤,現已修改完畢。java


繪製優化

繪製原理

View的繪製流程有3個步驟,分別是measure、layout和draw,它們主要運行在系統的應用框架層,而真正將數據渲染到屏幕上的則是系統Native層的SurfaceFlinger服務來完成的。android

繪製過程主要由CPU來進行Measure、Layout、Record、Execute的數據計算工做,GPU負責柵格化、渲染。CPU和GPU是經過圖形驅動層來進行鏈接的,圖形驅動層維護了一個隊列,CPU將display list添加到該隊列中,這樣GPU就能夠從這個隊列中取出數據進行繪製。git

Android系統每隔16ms發出VSYNC信號,觸發對UI進行渲染,若是每次渲染都成功,這樣就可以達到流暢的畫面所須要的60fps,VSYNC是Vertical Synchronization(垂直同步)的縮寫,是一種定時中斷,一旦收到VSYNC信號,CPU就開始處理各幀數據。若是某個操做要花費30ms,這樣系統在獲得VSYNC信號時沒法進行正常的渲染,會發生丟幀。github

產生卡頓緣由有不少,主要有如下幾點:shell

  • 佈局Layout過於複雜,沒法在16ms內完成渲染。
  • 同一時間動畫執行的次數過多,致使CPU和GPU負載太重。
  • View過渡繪製,致使某些像素在同一幀時間內被繪製屢次。
  • 在UI線程中作了稍微耗時的操做。
  • GC回收時暫停時間過長或者頻繁的GC產生大量的暫停時間。

工具篇

一、Profile GPU Rendering數據庫

Profile GPU Rendering是Android 4.1系統提供的開發輔助功能,能夠在開發者選項中打開這一功能,以下圖:編程

單擊Profile GPU Rendering選項並開啓Profile GPU Rendering功能,以下圖:後端

上面的彩色的圖的橫軸表明時間,縱軸表示某一幀的耗時。綠色的橫線爲警惕線,超過這條線則意味着時長超過了16m,儘可能要保證垂直的彩色柱狀圖保持在綠線下面。這些垂直的彩色柱狀圖表明着一幀,不一樣顏色的彩色柱狀圖表明不一樣的含義:數組

  • 橙色表明處理的時間,是CPU告訴GPU渲染一幀的地方,這是一個阻塞調用,由於CPU會一直等待GPU發出接到命令的回覆,若是橙色柱狀圖很高,則代表GPU很繁忙。
  • 紅色表明執行的時間,這部分是Android進行2D渲染 Display List的時間。若是紅色柱狀圖很高,多是由從新提交了視圖而致使的。還有複雜的自定義View也會致使紅的柱狀圖變高。
  • 藍色表明測量繪製的時間,也就是須要多長時間去建立和更新DisplayList。若是藍色柱狀圖很高,多是須要從新繪製,或者View的onDraw方法處理事情太多。

在Android 6.0中,有更多的顏色被加了進來,以下圖所示:緩存

下面來分別介紹它們的含義:

  • Swap Buffers:表示處理的時間,和上面講到的橙色同樣。
  • Command Issue:表示執行的時間,和上面講到的紅色同樣。
  • Sync & Upload:表示的是準備當前界面上有待繪製的圖片所耗費的時間,爲了減小該段區域的執行時間,咱們能夠減小屏幕上的圖片數量或者是縮小圖片的大小。
  • Draw:表示測量和繪製視圖列表所須要的時間,和上面講到的藍色同樣。
  • Measure/Layout:表示佈局的onMeasure與onLayout所花費的時間,一旦時間過長,就須要仔細檢查本身的佈局是否是存在嚴重的性能問題。
  • Animation:表示計算執行動畫所須要花費的時間,包含的動畫有ObjectAnimator,ViewPropertyAnimator,Transition等。一旦這裏的執行時間過長,就須要檢查是否是使用了非官方的動畫工具或者是檢查動畫執行的過程當中是否是觸發了讀寫操做等等。
  • Input Handling:表示系統處理輸入事件所耗費的時間,粗略等於對事件處理方法所執行的時間。一旦執行時間過長,意味着在處理用戶的輸入事件的地方執行了複雜的操做。
  • Misc Time/Vsync Delay:表示在主線程執行了太多的任務,致使UI渲染跟不上VSYNC的信號而出現掉幀的狀況。

Profile GPU Rendering能夠找到渲染有問題的界面,可是想要修復的話,只依賴Profile GPU Rendering是不夠的,能夠用另外一個工具Hierarchy Viewer來查看佈局層次和每一個View所花的時間。

二、Systrace

Systrace是Android4.1中新增的性能數據採樣和分析工具。它可幫助開發者收集Android關鍵子系統(SurfaceFlinger、WindowManagerService等Framework部分關鍵模塊、服務,View體系系統等)的運行信息。Systrace的功能包括跟蹤系統的I/O操做、內核工做隊列、CPU負載以及Android各個子系統的運行情況等。對於UI顯示性能,好比動畫播放不流暢、渲染卡頓等問題提供了分析數據。在android-sdk/tools/目錄的命令行中輸入‘monitor’,會打開Android Device Monitor。

三、Traceview

TraceView是Android SDK中自帶的數據採集和分析工具。通常來講,經過TraceView咱們能夠獲得如下兩種數據:

  • 單次執行耗時的方法。
  • 執行次數多的方法。

在android-sdk/tools/目錄的命令行中輸入‘monitor’,會打開Android Device Monitor,選擇相應的進程,並單擊Start Method Profiling按鈕,對應用中須要監控的點進行操做,單擊Stop Method Profiling按鈕,會自動跳到TraceView視圖。

也能夠代碼中添加TraceView監控語句,代碼以下所示。

Debug.startMethodTracing();
...
Debug.stopMethodTracing();
複製代碼

在開始監控的地方調用startMethodTracing方法,在須要結束監控的地方調用stopMethodTracing方法。系統會在SD卡中生成trace文件,將trace文件導出並用SDK中的Traceview打開便可。固然不要忘了在manifest中加入

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>權限。
複製代碼

四、GPU過分繪製

Android手機上面的開發者選項提供了工具來檢測過分繪製,能夠按以下步驟來打開:

開發者選項->調試GPU過分繪製->顯示過分繪製區域

以下圖所示:

能夠看到,界面上出現了一堆紅綠藍的區域,咱們來看下這些區域表明什麼意思:

須要注意的是,有些過分繪製是沒法避免的。所以在優化界面時,應該儘可能讓大部分的界面顯示爲真彩色(即無過分繪製)或者爲藍色(僅有 1 次過分繪製)。儘可能避免出現粉色或者紅色。

優化建議

  1. 使用include標籤來進行佈局的複用
  2. 使用merge標籤去除多餘層級
  3. 使用ViewStub提升加載速度(延遲加載)
  4. 移除控件中不須要的背景
  5. 將layout層級扁平化,推薦使用ConstraintLayout
  6. 使用嵌套少的佈局,合理運用LinearLayout和RelativeLayout
  7. onDraw()中不要建立新的局部變量以及不要作耗時操做

內存優化

知識掃盲

OOM:

系統分配給app的堆內存是有上限的,不是系統空閒多少內存app就能夠用多少,getMemoryClass()能夠獲取到這個值。 能夠在manifest文件中設置largeHeap爲true,這樣會增大堆內存上限,getLargeMemoryClass()能夠獲取到這個值。 超出虛擬機堆內存上限會形成OOM。

Low Memory Killer:

android內存管理使用了分頁(paging)和內存映射(memory-mapping)技術,可是沒有使用swap,而是使用Low Memory Killer策略來提高物理內存的利用率 ,致使除了gc和殺死進程回收物理內存以外沒有其餘方式來利用已經被佔用的內存。 當前臺應用切換到後臺後,系統並不結束它的進程,而是把它緩存起來,供下次啓動。當系統內存不足時,按最近最少使用+優先釋放內存使用密集的策略釋放緩存進程。

GC:

內存使用的多也會形成GC速度變慢,形成卡頓。 內存佔用太高,在建立對象時內存不足,很容易形成觸發GC影響APP性能。

圖片相關

圖片的格式

目前 Android 端支持的圖片格式有JPEG、GIF、PNG、BMP、WebP,可是在 Android中可以使用編解碼使用的只有其中的三種:JPEG、PNG、WebP。

  • JPEG:是普遍使用的有損壓縮圖像標準格式,它不支持透明和多幀動畫
  • PNG:是一種無損壓縮圖片格式,它支持完整的透明通道,因爲是無損壓縮,因此它的佔用空間通常比較大。
  • GIF:它支持多幀動畫
  • WebP:它支持有損和無損壓縮,支持完整的透明通道也支持多幀動畫,是一種比較理想的圖片格式。
圖片壓縮工具
  • 無損壓縮 ImageOption ImageOption 是一個無損的壓縮工具,它經過優化PNG 的壓縮參數,移除冗餘元數據以及非必須的顏色配置文件等方式,在不犧牲圖片質量的前提下,既減少了PNG圖片佔用的空間,又提升了加載的速度。
  • 有損壓縮 ImageAlpha ImageAlpha 是 ImageOptions 做者開發的一個有損的 PNG 壓縮工具,相比較而言,圖片大小獲得極大的下降,固然圖片質量同時也會受到必定程度的影響,通過該工具壓縮的圖片,須要通過設計師的檢視才能上線,不然可能會影響到整個 APP 的視覺效果。
  • 使用有損壓縮工具 TinyPNG 等。
  • PNG/JPEG 轉換爲 WebP。
  • 儘可能使用 .9格式的PNG 圖,由於它體積小,拉伸不變形可以適配 Android 各類機型。
代碼壓縮

Android中Bitmap所佔內存大小計算方式:圖片長度 x 圖片寬度 x 一個像素點佔用的字節數

影響Bitmap佔用內存的因素:

  • 圖片最終加載的分辨率;
  • 圖片的格式(PNG/JPEG/BMP/WebP);
  • 圖片所存放的drawable目錄;
  • 圖片屬性設置的色彩模式;
  • 設備的屏幕密度;

一、Bitmap的Compress方法(質量壓縮):

public boolean compress(CompressFormat format, int quality, OutputStream stream) 複製代碼

參數format:表示圖像的壓縮格式,目前有CompressFormat.JPEG、CompressFormat.PNG、CompressFormat.WEBP。

參數quality: 圖像壓縮率,0-100。 0 壓縮100%,100意味着不壓縮。

參數stream: 寫入壓縮數據的輸出流。

經常使用的用法:

public static Bitmap compress(Bitmap bitmap){

    ByteArrayOutputStream baos = new ByteArrayOutputStream();

    bitmap.compress(Bitmap.CompressFormat.JPEG, 90, baos);

    byte[] bytes = baos.toByteArray();

    return BitmapFactory.decodeByteArray(bytes, 0, bytes.length);

}
複製代碼

上面方法中經過bitmap的compress方法對bitmap進行質量壓縮,10%壓縮,90%不壓縮。

圖片的大小是沒有變的,由於質量壓縮不會減小圖片的像素,它是在保持像素的前提下改變圖片的位深及透明度等,來達到壓縮圖片的目的,這也是爲何該方法叫質量壓縮方法。圖片的長,寬,像素都不變,那麼bitmap所佔內存大小是不會變的。

quality值越小壓縮後的baos越小(使用場景:在微信分享時,須要對圖片的字節數組大小進行限制,這時可使用bitmap的compress方法對圖片進行質量壓縮)。

二、BitmapFactory.Options的inJustDecodeBounds和inSampleSize參數(採樣率壓縮):

inJustDecodeBounds:當inJustDecodeBounds設置爲true的時候,BitmapFactory經過decodeXXXX解碼圖片時,將會返回空(null)的Bitmap對象,這樣能夠避免Bitmap的內存分配,可是它能夠返回Bitmap的寬度、高度以及MimeType。

inSampleSize: 當它小於1的時候,將會被當作1處理,若是大於1,那麼就會按照比例(1 / inSampleSize)縮小bitmap的寬和高、下降分辨率,大於1時這個值將會被處置爲2的倍數。例如,width=100,height=100,inSampleSize=2,那麼就會將bitmap處理爲,width=50,height=50,寬高降爲1 / 2,像素數降爲1 / 4。

經常使用用法:

public static Bitmap inSampleSize(byte[] data,int reqWidth,int reqHeight){

    final BitmapFactory.Options options = new BitmapFactory.Options();

    options.inJustDecodeBounds = true;

    BitmapFactory.decodeByteArray(data, 0, data.length, options);

    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

    options.inJustDecodeBounds = false;

    return BitmapFactory.decodeByteArray(data, 0, data.length, options);

}

public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {

    final int picheight = options.outHeight;

    final int picwidth = options.outWidth;

    int targetheight = picheight;

    int targetwidth = picwidth;

    int inSampleSize = 1;

    if (targetheight > reqHeight || targetwidth > reqWidth) {

        while (targetheight >= reqHeight

                && targetwidth >= reqWidth) {

            inSampleSize += 1;

            targetheight = picheight / inSampleSize;

            targetwidth = picwidth / inSampleSize;

        }

    }

    return inSampleSize;

}
}
複製代碼

inSampleSize方法中先將inJustDecodeBounds設置爲false,在經過BitmapFactory的decodeXXXX方法解碼圖片,返回空(null)的Bitmap對象,同時獲取了bitmap的寬高,再經過calculateInSampleSize方法根據原bitmap的 寬高和目標寬高計算出合適的inSampleSize,最後將inJustDecodeBounds設置爲true,經過BitmapFactory的decodeXXXX方法解碼圖片(使用場景:好比讀取本地圖片時,防止Bitmap過大致使內存溢出)。

三、經過Matrix壓縮圖片

Matrix matrix = new Matrix();

matrix.setScale(0.5f, 0.5f);

bm = Bitmap.createBitmap(bit, 0, 0, bit.getWidth(),bit.getHeight(), matrix, true);

}
複製代碼

使用場景:自定義View時,對圖片進行縮放、旋轉、位移以及傾斜等操做,常見的就是對圖片進行縮放處理,以及圓角圖片等。

其餘知識點

inBitmap: 若是設置,在加載Bitmap的時候會嘗試去重用這塊內存(內存複用),不能重用的時候會返回null,不然返回bitmap。

複用內存:BitmapFactory.Options 參數inBitmap的使用。inMutable設置爲true,而且配合SoftReference軟引用使用(內存空間足夠,垃圾回收器就不會回收它;若是內存空間不足了,就會回收這些軟引用對象的內存)。有一點要注意Android4.4如下的平臺,須要保證inBitmap和即將要獲得decode的Bitmap的尺寸規格一致,Android4.4及其以上的平臺,只須要知足inBitmap的尺寸大於要decode獲得的Bitmap的尺寸規格便可。

圖片加載和緩存

常見的圖片加載緩存庫有 Picasso、Glide、Fresco。

  • Picasso 是 Square 公司開源的圖片加載庫,它實現圖片的下載和二級緩存緩存功能,庫文件 120KB
  • Glide 是 Google 推薦的用於 Android 平臺上的圖片加載和緩存庫,庫文件 475KB
  • Fresco 是 Facebook 開源的功能強大的圖片加載庫,如對圖片顯示要求很高可選擇該庫。該庫最顯著的特色是實現了三級緩存,兩級內存緩存一級磁盤緩存。庫文件 3.4MB

根據 App 對圖片顯示和緩存的需求從低到高的選擇順序:Picasso < Glide < Fresco

Bitmap其餘方案

一、使用完畢後釋放圖片資源

Android編程中,每每最容易出現OOM的地方就是在圖片處理的時候,咱們先上個數據:一個像素的顯示須要4字節(R、G、B、A各佔一個字節),因此一個1080x720像素的手機一個滿屏幕畫面就須要近3M內存,而開發一個輕量應用的安裝包大小也差很少就3M左右,因此說圖片很佔內存。在Android中,圖片的資源文件叫作Drawable,存儲在硬盤上,不耗內存,但咱們並沒有法對其進行處理,最多隻能進行展現。而若是想對該圖片資源進行處理,咱們須要把這個Drawable解析爲Bitmap形式裝載入內存中。其中Android的不一樣版本對Bitmap的存儲方式還有所不一樣。下面是Android官方文檔中對此描述的一段話

On Android 2.3.3 (API level 10) and lower, the backing pixel data for a bitmap is stored in native memory. It is separate from the bitmap itself, which is stored in the Dalvik heap. The pixel data in native memory is not released in a predictable manner, potentially causing an application to briefly exceed its memory limits and crash. As of Android 3.0 (API level 11), the pixel data is stored on the Dalvik heap along with the associated bitmap.

bitmap分紅兩個部分,一部分爲bitmap對象,用以存儲此圖片的長、寬、透明度等信息;另外一部分爲bitmap數據,用以存儲bitmap的(A)RGB字節數據。在2.3.3及之前版本中bitmap對象和bitmap數據是存儲在不一樣的內存空間上的,bitmap數據部分存儲在native內存中,GC沒法涉及。因此以前咱們須要調用bitmap的recycle方法來顯示的告訴系統此處內存可回收,而在3.0版本開始,bitmap的的這兩部分都存儲在了Dalvik堆中,能夠被GC機制統一處理,也就無需用recycle了。 關於bitmap優化,不一樣版本方法也不相同,2.3.3版本及之前,就要作到及時調用recycle來回收不在使用的bitmap,而3.0開始可使用BitmapFactory.Options.inBitmap這個選項,設置一個可複用的bitmap,這樣之後新的bitmap且大小相同的就能夠直接使用這塊內存,而無需重複申請內存。4.4以後解決了對大小的限制,不一樣大小也能夠複用該塊空間。

注:若調用了Bitmap.recycle()後,再繪製Bitmap,則會出現Canvas: trying to use a recycled bitmap錯誤

二、根據分辨率適配 & 縮放圖片

若 Bitmap 與 當前設備的分辨率不匹配,則會拉伸Bitmap,而Bitmap分辨率增長後,所佔用的內存也會相應增長

由於Bitmap 的內存佔用 根據 x、y的大小來增長的

三、按需 選擇合適的解碼方式

不一樣的圖片解碼方式 對應的 內存佔用大小 相差很大,具體以下

使用參數:BitmapFactory.inPreferredConfig 設置 默認使用解碼方式:ARGB_8888

四、設置 圖片緩存

重複加載圖片資源耗費太多資源(CPU、內存 & 流量)

內存泄漏

內存泄露,即Memory Leak,指程序中再也不使用到的對象因某種緣由從而沒法被GC正常回收。發生內存泄露,會致使一些再也不使用到的對象沒有及時釋放,這些對象佔用了寶貴的內存空間,很容易致使後續須要分配內存的時候,內存空間不足而出現OOM(內存溢出)。

一、靜態變量致使的內存泄露

靜態變量的生命週期與應用的生命週期一致,該對象會一直被引用直到應用結束。

例子1:

public class MainActivity extends Activity {
    public static Context context;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        context=this;
    }
}
複製代碼

上述代碼在MainActivity中context爲靜態變量,並持有Context,當Activity退出後,因爲Activity被context一直引用着,致使Activity沒法被回收,所以形成了內存泄漏。上述代碼比較明顯,通常不會犯這種錯誤。

例子2:

public class MainActivity extends Activity {
    public static Out mOut;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mOut = new Out(this);
    }
}
//外部Out類
public class Out {
    Out(Context context) {

    }
}
複製代碼

上述代碼與例子1相似,mOut爲靜態變量,生命週期與應用一致,傳入的MainActivity也被一直引用,致使Activity沒法被回收,形成內存泄漏。

解決方案:

一、在不使用靜態變量時,置空。

二、可使用Application的Context。

三、經過弱引用和軟引用來引用Activity。

  • 強引用:直接的對象引用。
  • 軟引用:當一個對象只有軟引用存在時,系統內存不足時此對象會被GC回收。
  • 弱引用:當一個對象只有弱引用存在時,此對象隨時被GC回收。

例子3:

單例模式在Android開發中會常常用到,可是若是使用不當就會致使內存泄露。由於單例的靜態特性使得它的生命週期同應用的生命週期同樣長,若是一個對象已經沒有用處了,可是單例還持有它的引用,那麼在整個應用程序的生命週期它都不能正常被回收,從而致使內存泄露。

public class Singleton {
   private static Singleton singleton = null;
   private Context mContext;

   public Singleton(Context mContext) {
      this.mContext = mContext;
   }

   public static Singleton getSingleton(Context context){
    if (null == singleton){
      singleton = new Singleton(context);
    }
    return singleton;
  }
}
複製代碼

像上面代碼中這樣的單例,若是咱們在調用getInstance(Context context)方法的時候傳入的context參數是Activity、Service等上下文,就會致使內存泄露。

當咱們退出Activity時,該Activity就沒有用了,可是由於singleton做爲靜態單例(在應用程序的整個生命週期中存在)會繼續持有這個Activity的引用,致使這個Activity對象沒法被回收釋放,這就形成了內存泄露。

二、非靜態內部類致使內存泄露

非靜態內部類(包括匿名內部類)默認就會持有外部類的引用,當非靜態內部類對象的生命週期比外部類對象的生命週期長時,就會致使內存泄露。

非靜態內部類致使的內存泄露在Android開發中有一種典型的場景就是使用Handler,不少開發者在使用Handler是這樣寫的:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        start();
    }

    private void start() {
        Message msg = Message.obtain();
        msg.what = 1;
        mHandler.sendMessage(msg);
    }

    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            if (msg.what == 1) {
                // 作相應邏輯
            }
        }
    };
}
複製代碼

當Activity退出後,msg可能仍然存在於消息對列MessageQueue中未處理或者正在處理,那麼這樣就會致使Activity沒法被回收,以至發生Activity的內存泄露。 一般在Android開發中若是要使用內部類,但又要規避內存泄露,通常都會採用靜態內部類+弱引用的方式。

public class MainActivity extends AppCompatActivity {

    private Handler mHandler;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mHandler = new MyHandler(this);
        start();
    }

    private void start() {
        Message msg = Message.obtain();
        msg.what = 1;
        mHandler.sendMessage(msg);
    }

    private static class MyHandler extends Handler {

        private WeakReference<MainActivity> activityWeakReference;

        public MyHandler(MainActivity activity) {
            activityWeakReference = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = activityWeakReference.get();
            if (activity != null) {
                if (msg.what == 1) {
                    // 作相應邏輯
                }
            }
        }
    }
}
複製代碼

mHandler經過弱引用的方式持有Activity,當GC執行垃圾回收時,遇到Activity就會回收並釋放所佔據的內存單元。這樣就不會發生內存泄露了。 上面的作法確實避免了Activity致使的內存泄露,發送的msg再也不已經沒有持有Activity的引用了,可是msg仍是有可能存在消息隊列MessageQueue中,因此更好的是在Activity銷燬時就將mHandler的回調和發送的消息給移除掉。

@Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null);
    }
    
複製代碼

非靜態內部類形成內存泄露還有一種狀況就是使用Thread或者AsyncTask。 好比在Activity中直接new一個子線程Thread:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 模擬相應耗時邏輯
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

複製代碼

或者直接新建AsyncTask異步任務:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        new AsyncTask<Void, Void, Void>() {
            @Override
            protected Void doInBackground(Void... params) {
                // 模擬相應耗時邏輯
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return null;
            }
        }.execute();
    }
}
複製代碼

這種方式新建的子線程Thread和AsyncTask都是匿名內部類對象,默認就隱式的持有外部Activity的引用,致使Activity內存泄露。要避免內存泄露的話仍是須要像上面Handler同樣使用靜態內部類+弱應用的方式(代碼就不列了,參考上面Hanlder的正確寫法)。

三、集合類內存泄露

集合類添加元素後,將會持有元素對象的引用,致使該元素對象不能被垃圾回收,從而發生內存泄漏。

四、未關閉資源對象內存泄露

  • 註銷廣播:若是廣播在Activity銷燬後不取消註冊,那麼這個廣播會一直存在系統中,因爲廣播持有了Activity的引用,所以會致使內存泄露。
  • 關閉輸入輸出流等:在使用IO、File流等資源時要及時關閉。這些資源在進行讀寫操做時一般都使用了緩衝,若是不及時關閉,這些緩衝對象就會一直被佔用而得不到釋放,以至發生內存泄露。所以咱們在不須要使用它們的時候就應該及時關閉,以便緩衝能獲得釋放,從而避免內存泄露。
  • 回收Bitmap:Bitmap對象比較佔內存,當它再也不被使用的時候,最好調用Bitmap.recycle()方法主動進行回收。
  • 中止動畫:屬性動畫中有一類無限動畫,若是Activity退出時不中止動畫的話,動畫會一直執行下去。由於動畫會持有View的引用,View又持有Activity,最終Activity就不能給回收掉。只要咱們在Activity退出把動畫停掉便可。
  • 銷燬WebView:WebView在加載網頁後會長期佔用內存而不能被釋放,所以咱們在Activity銷燬後要調用它的destory()方法來銷燬它以釋放內存。或是把使用了 WebView 的 Activity (或者 Service) 放在單獨的進程裏

WebView擴展:

WebView 解析網頁時會申請Native堆內存用於保存頁面元素,當頁面較複雜時會有很大的內存佔用。若是頁面包含圖片,內存佔用會更嚴重。而且打開新頁面時,爲了能快速回退,以前頁面佔用的內存也不會釋放。有時瀏覽十幾個網頁,都會佔用幾百兆的內存。這樣加載網頁較多時,會致使系統不堪重負,最終強制關閉應用,也就是出現應用閃退或重啓。
因爲佔用的都是Native 堆內存,因此實際佔用的內存大小不會顯示在經常使用的 DDMS Heap 工具中( DMS Heap 工具看到的只是Java虛擬機分配的內存,即便Native堆內存已經佔用了幾百兆,這裏顯示的還只是幾兆或十幾兆)。只有使用 adb shell 中的一些命令好比 dumpsys meminfo 包名,或者在程序中使用 Debug.getNativeHeapSize()才能看到 Native 堆內存信息。

五、使用系統服務引起的內存泄漏

爲了方便咱們使用一些常見的系統服務,Activity 作了一些封裝。好比說,能夠經過 getPackageManager在 Activtiy 中獲取 PackageManagerService,可是,裏面實際上調用了 Activity 對應的 ContextImpl 中的 getPackageManager 方法

ContextWrapper#getPackageManager

@Override
public PackageManager getPackageManager() {
    return mBase.getPackageManager();
}
ContextImpl#getPackageManager

@Override
public PackageManager getPackageManager() {
    if (mPackageManager != null) {
        return mPackageManager;
    }
    IPackageManager pm = ActivityThread.getPackageManager();
    if (pm != null) {
        // Doesn't matter if we make more than one instance.
        return (mPackageManager = new ApplicationPackageManager(this, pm));//建立 ApplicationPackageManager
    }
    return null;
}
複製代碼

ApplicationPackageManager#ApplicationPackageManager

ApplicationPackageManager(ContextImpl context,
                          IPackageManager pm) {
    mContext = context;//保存 ContextImpl 的強引用
    mPM = pm;
}

private UserManagerService(Context context, PackageManagerService pm,
        Object packagesLock, File dataDir) {
    mContext = context;//持有外部 Context 引用
    mPm = pm;
     //代碼省略
}
PackageManagerService#PackageManagerService

public class PackageManagerService extends IPackageManager.Stub {
    static UserManagerService sUserManager;//持有 UMS 靜態引用
    public PackageManagerService(Context context, Installer installer,
        boolean factoryTest, boolean onlyCore) {
          sUserManager = new UserManagerService(context, this, mPackages);//初始化 UMS
        }
}
複製代碼

遇到的內存泄漏問題是由於在 Activity 中調用了 getPackageManger 方法獲取 PMS ,該方法調用的是 ContextImpl,此時若是ContextImpl 中 PackageManager 爲 null,就會建立一個 PackageManger(ContextImpl 會將本身傳遞進去,而 ContextImpl 的 mOuterContext 爲 Activity),建立 PackageManager 實際上會建立 PackageManagerService(簡稱 PMS),而 PMS 的構造方法中會建立一個 UserManger(UserManger 初始化以後會持有 ContextImpl 的強引用)。 只要 PMS 的 class 未被銷燬,那麼就會一直引用着 UserManger ,進而致使其關聯到的資源沒法正常釋放。

解決辦法:

將getPackageManager()改成 getApplication()#getPackageManager() 。這樣引用的就是 Application Context,而非 Activity 了。

內存泄漏工具

一、leakcanary

leakcanary是square開源的一個庫,可以自動檢測發現內存泄露,其使用也很簡單: 在build.gradle中添加依賴:

dependencies {
  debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.1'
  releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.1'

  //可選項,若是使用了support包中的fragments
  debugImplementation 'com.squareup.leakcanary:leakcanary-support-fragment:1.6.1'
}
複製代碼

根目錄下的build.gradle添加mavenCentral()便可,以下:

allprojects {
    repositories {
        google()
        jcenter()
        mavenCentral()
    }
}
複製代碼

而後在自定義的Application中調用如下代碼就能夠了。

public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        if (LeakCanary.isInAnalyzerProcess(this)) {
            return;
        }
        LeakCanary.install(this);

        //正常初始化代碼
    }
}

複製代碼

若是檢測到有內存泄漏,通知欄會有提示,以下圖;若是沒有內存泄漏,則沒有提示。

二、Memory Profiler

Memory Profiler 是 Android Profiler 中的一個組件,能夠幫助你分析應用卡頓,崩潰和內存泄露等等問題。

打開 Memory Profiler後便可看到一個相似下圖的視圖。

上面的紅色數字含義以下:

1.用於強制執行垃圾回收事件的按鈕。
2.用於捕獲堆轉儲的按鈕。
3.用於記錄內存分配狀況的按鈕。 此按鈕僅在鏈接至運行 Android 7.1 或更低版本的設備時纔會顯示。
4.用於放大/縮小/還原時間線的按鈕。
5.用於跳轉至實時內存數據的按鈕。
6.Event 時間線,其顯示 Activity 狀態、用戶輸入 Event 和屏幕旋轉 Event。
7.內存使用量時間線,其包含如下內容:
    一個顯示每一個內存類別使用多少內存的堆疊圖表,如左側的 y 軸以及頂部的彩色鍵所示。
    虛線表示分配的對象數,如右側的 y 軸所示。
    用於表示每一個垃圾回收事件的圖標。
複製代碼

如何Memory Profiler分析內存泄露,按如下步驟來便可:

1.使用Memory Profiler監聽要分析的應用進程
2.旋轉幾回要分析的Activity。(這是由於旋轉Activity後會從新建立)
3.點擊捕獲堆轉儲按鈕去捕獲堆轉儲
4.在捕獲結果中搜索要分析的類。(這裏是MainActivity)
5.點擊要分析的類,右邊會顯示這個類建立對象的數量。
複製代碼

以下圖:

內存抖動

內存抖動的緣由:

內存抖動通常是瞬間建立了大量對象,會在短期內觸發屢次GC,產生卡頓。

內存抖動的在分析工具上的表現:

解決方案:

最簡單的作法就是把以前的主線程操做放到子線程去,雖然內存抖動依然存在,可是卡頓問題能夠大大緩解。

對於內存抖動自己:

儘可能避免在循環體內建立對象,應該把對象建立移到循環體外。 須要大量使用Bitmap和其餘大型對象時,儘可能嘗試複用以前建立的對象。

網絡優化

客戶端請求流程以下:

相關分析工具

分析網絡狀況的方式能夠經過Wireshark, Fiddler, Charlesr等抓包工具,也能夠經過Android Studio的Network Profiler

窗口頂部顯示的是 Event 時間線以及 1 無線裝置功耗狀態(低/高)與 WLAN 的對比。 在時間線上,您能夠 2點擊並拖動選擇時間線的一部分來檢查網絡流量。

下方的3窗口會顯示在時間線的選定片斷內收發的文件,包括文件名稱、大小、類型、狀態和時間。 您能夠點擊任意列標題爲此列表排序。

同時,您還能夠查看時間線選定片斷的明細數據,顯示每一個文件的發送或接收時間。

點擊網絡鏈接的名稱便可查看 4 有關所發送或接收的選定文件的詳細信息。 點擊各個標籤可查看響應數據、標題信息或調用堆棧。

注: 必須啓用高級分析才能從時間線中選擇要檢查的片斷,查看發送和接收的文件列表,或查看有關所發送或接收的選定文件的詳細信息。 要啓用高級分析,請參閱啓用高級分析。

啓用高級分析須要點擊Run Configuration:

打開Run/Debug Configurations,左側選擇你的應用,右側在Profiling中勾選Enable advanced profiling。

經過以上這些工具能夠查看某個時間段內網絡請求的具體狀況,從而進行網絡優化的相關工做。

優化建議

一、後端API設計

後端設計API時須要考慮網絡請求的頻次、資源狀態,在某些狀況下能夠合併多個接口以知足客戶端業務需求。

二、Gzip壓縮

使用Gzip來壓縮request和response, 減小傳輸數據量, 從而減小流量消耗。同時能夠考慮使用Protocol Buffer代替JSON,protobuf會比JSON數據量小不少.

三、圖片大小優化

  • 請求圖片時告訴服務器須要的圖片的寬高。(好比使用七牛時,能夠在url後面添加質量、格式、寬高等等來獲取合適的圖片資源)
  • 列表採用縮略圖。
  • 使用Webp格式:安卓系統從Android4.0(API 14)添加了有損耗的WebP support而且在Android4.2(API 17)對無損的,清晰的WebP提供了支持。使用WebP格式;一樣的照片,採用WebP格式可大幅節省流量,相對於JPG格式的圖片,流量能節省將近 25% 到 35 %;相對於PNG格式的圖片,流量能夠節省將近80%。最重要的是使用WebP以後圖片質量也沒有改變。
  • 使用第三方圖片加載框架
  • 網絡緩存
  • 監聽網絡狀態,非WiFi下能夠顯示無圖頁面,WiFi或4G狀況下才顯示有圖頁面。
  • IP直連與HttpDns:DNS解析的失敗率佔聯網失敗中很大一種,並且首次域名解析通常須要幾百毫秒。針對此,咱們能夠不用域名,才用IP直連省去 DNS 解析過程,節省這部分時間。HttpDNS基於Http協議的域名解析,替代了基於DNS協議向運營商Local DNS發起解析請求的傳統方式,能夠避免Local DNS形成的域名劫持和跨網訪問問題,解決域名解析異常帶來的困擾。

電量優化

電量分析工具

一、Batterystats & bugreport

Android 5.0及以上的設備, 容許咱們經過adb命令dump出電量使用統計信息.

由於電量統計數據是持續的, 會很是大, 統計咱們的待測試App以前先reset下, 連上設備,命令行執行:

$ adb shell dumpsys batterystats --reset
Battery stats reset.
複製代碼

斷開測試設備, 操做咱們的待測試App,從新鏈接設備, 使用adb命令導出相關統計數據:

// 此命令持續記錄輸出, 想要中止記錄時按Ctrl+C退出.
$ adb bugreport > bugreport.txt
複製代碼

導出的統計數據存儲到bugreport.txt, 此時咱們能夠藉助以下工具來圖形化展現電池的消耗狀況。

二、Battery Historian

Google提供了一個開源的電池歷史數據分析工具

Battery Historian連接

耗電緣由

  • 網絡請求
  • 使用WakeLock:WakeLock會保持CPU運行,或是防止屏幕變暗/關閉,讓手機能夠在用戶不操做時依然運行。CPU會一直得不到休眠, 而大大增長耗電.
  • GPS
  • 藍牙傳輸

建議: 根據具體業務需求,嚴格限制應用位於後臺時是否禁用某些數據傳輸,儘可能可以避免無效的數據傳輸。 數據傳輸的頻度問題,如網絡請求能夠壓縮合並,如本地數據上傳,能夠選擇恰當的時機上傳。

JobScheduler組件

經過不停的喚醒CPU(經過後天常駐的Service)來達到一些功能的使用,這樣會形成電量資源的消耗,好比後臺日誌的上報,按期更新數據等等,在Android 5.0提供了一個JobScheduler組件,經過設置一系列的預置條件,當條件知足時,才執行對應的操做,這樣既能省電,有保證了功能的完整性。

JobScheduler的適用場景:

  • 重要不緊急的任務,能夠延遲執行,好比按期數據庫數據更新和數據上報
  • 耗電量較大的任務,好比充電時才執行的備份數據操做。
  • 不緊急能夠不執行的網絡任務,好比在Wi-Fi環境下預加載數據。
  • 能夠批量執行的任務
  • ......等等

JobScheduler的使用

private Context mContext;
    private JobScheduler mJobScheduler;

    public JobSchedulerManager(Context context){
        this.mContext=context;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            this.mJobScheduler= (JobScheduler) mContext.getSystemService(Context.JOB_SCHEDULER_SERVICE);
        }
    }
複製代碼

經過getSystemService()方法獲取一個JobSchedule的對象。

public boolean addTask(int taskId) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            JobInfo.Builder builder = new JobInfo.Builder(taskId,
                    new ComponentName("com.apk.administrator.loadapk",
                            JobScheduleService.class.getName()));
            switch (taskId) {
                case 1:
                    //每隔1秒執行一次
                    builder.setPeriodic(1000);
                    break;
                case 2:
                    //設備重啓後,再也不執行該任務
                    builder.setPersisted(false);
                    break;
                default:
                    break;
            }
            if (null != mJobScheduler) {
                return mJobScheduler.schedule(builder.build()) > 0;
            } else {
                return false;
            }
        } else {
            return true;
        }
    }
複製代碼

建立一個JobInfo對象時傳入兩個參數,第一個參數是任務ID,能夠對不一樣的任務ID作不一樣的觸發條件,執行任務時根據任務ID執行具體的任務;第二個參數是JobScheduler任務的服務,參數爲進程名和服務類名。

JobInfo支持如下幾種觸發條件:

  • setMinimumLatency(long minLatencyMillis):設置任務的延遲時間(單位是ms),須要注意的是,setMinimumLatency與setPeriodic(long time)方法不兼容,同時調用會引發異常。
  • setOverrideDeadline(long maxExecutionDelayMillis):設置任務最晚的延遲時間。若是到了規定時間,其它條件還未知足,這個任務也會被啓動。與setMinimumLatency(long time)同樣,setOverriddeDeadline與setPeriodic(long time)同時調用會引發異常。
  • setPersisted(boolean isPersisted):設置重啓以後,任務是否還要繼續執行。
  • setRequiredNetworkType(int networkType):只有知足指定的網絡條件時,纔會被執行。有三種網絡條件,JobInfo.NETWORK_TYPE_NONE無論是否有網絡,這個任務都會被執行(若是未設置,這個參數就是默認參數);JobInfo.NETWORK_TYPE_ANY只有在有網絡的狀況下,任務纔會執行,和網絡類型無關;JobInfo.NETWORK_TYPE_UNMETERED非運營商網絡(好比在Wi-Fi鏈接時),任務纔會被執行。
  • setRequiresCharging(boolean requiresCharging):只有當設備在充電時,這個任務纔會被執行。 setRequiresDeviceIdle(boolean requiresDeviceIdle):只有當用戶沒有在使用該設備且有一段時間沒有使用時,纔會啓動該任務。
public class JobScheduleService extends JobService {
    @Override
    public boolean onStartJob(JobParameters params) {
        return false;
    }

    @Override
    public boolean onStopJob(JobParameters params) {
        return false;
    }
}
複製代碼

JobService運行在主線程,若是是耗時任務,使用ThreadHandler或者一個異步任務來運行耗時的任務,防止阻塞主線程。

JobScheduleService繼承JobService,實現兩個方法onStartJob和onStopJob。 任務開始時,執行onStartJob方法,當任務執行完畢後,須要調用jobFinished方法來通知系統;任務執行完成後,調用jobFinished方法通知JobScheduler;當系統接受到一個取消請求時,調用onStopJob方法取消正在等待執行的任務。若是系統在接受到一個取消請求時,實際任務隊列中已經沒有正在運行的任務,onStopJob不會被調用。 最後在AndroidManifest中配置下:

<service android:name=".JobScheduleService" android:permission="android.permission.BIND_JOB_SERVICE" />
複製代碼

APK體積優化

一、從圖片入手:.9圖、壓縮或採用Webp。
二、使用Lint刪除無用資源
三、經過Gradle配置,過濾無用資源和.so文件
四、第三方庫慎重使用,能夠只提取使用到的代碼
五、資源混淆:方案有:美團和微信,前者是經過修改AAPT在處理資源文件相關的源碼達到資源名的替換,後者經過直接修改resources.arsc文件來達到資源文件名的混淆。
六、插件化
複製代碼

整合網上相關資料,不按期更新此文。

相關文章
相關標籤/搜索