Android性能優化概覽

前言

最近自己對安卓有一些感慨,有時候也會質疑自己,在這裏抒發一下,有興趣的可以看看,趕時間的就直接看正文啦。

對目前大部分Android開發人員來說,當然這裏說的是各種琳琅滿目的小公司的大部分Android開發人員們,(一說我就想起上半年春招的時候,我居然才知道有個東西叫面經,所以目前還沒能去大公司擰螺絲or造核彈暫且是擰螺絲吧,也只是聽別人說說,但是至少我目前是懷揣着一顆造核彈的心),大多數時候我們的工作可能都是寫寫佈局,有點經驗的弄個酷炫的自定義view(然後滿滿成就感),再然後就是寫寫業務邏輯,根據邏輯寫一寫數據的展示等,可以說這兩件事已經涵蓋了%90 上述範圍的Android開發人員的工作,再加上現在技術的發展,很多佈局上的難題(各種滑動view的嵌套,懸浮佈局,側拉,導航,引導頁)已經有了比較成熟的解決方案,這也是必然趨勢,再比如業務上的處理,各種成熟的框架已經席捲並深入整個Android開發的每個項目中(Glide、retrofit、okhttp、RxJava等),基本只要你熟練,再對框架有一定理解(MVP、MVVM等),再對業務的分塊分層明確,你甚至可以完全一個人完成一個app的開發,也就是說APP的開發難度相比之前已經大幅降低了,話說回來,那這些人剩下的%10的工作在幹什麼呢,這個,咳咳,因人而異吧,努力的人就學習學習新知識(dart、flutter),滿足於現狀的就在刷抖音微博了,每每看到後者,我自己都會有一種….(想了很久,不知道應該填什麼詞了)的感覺,但是我相信你看到我這篇文章的時候,你肯定是一個十分愛學習的人,不甘於平凡,有一腔熱血隨時揮灑,對技術的熱愛,對完美的追求,對自己所向往事物的執着,但是偶爾你可能也會和我一樣,看看周圍的人和事,有一些自己的體會和感想,對前路的否定,對技術的不可控,對google開發人員目前在開發的技術彷彿永遠也追趕不上,被google開發牽着在走,作爲技術人員中平凡的一粒,技術日新月異的速度,我總有一種賽跑的感覺,但是當我們年輕的時候,能否成爲領跑者,這不正是我們期待和值得付出的嗎!

目錄

1.性能優化的入手點
2.可以使用的工具
3.瑣碎的優化點

正文

其實本來想說說爲什麼要做性能優化的,但是這不是廢話嗎,/捂臉,就跳過啦。

Android性能優化的入手點

其實說起性能優化,本身就是一個很寬泛的概念,大到架構的設計優化,小到一個bitmap的及時回收。所以爲了知道性能優化的方方面面,我們有必要知道總共可以通過哪些方式來優化我們APP的性能。

1.佈局優化

佈局優化其實也有很多種優化,咱們一個個來看。
如果你對這個不瞭解的話,先放幾個Android提供的原生標籤感受一下,includemergeViewStub ,怎麼樣,都認識幾個,include應該都是很熟悉的,所以第一個優化佈局的方式就是使用include 標籤來實現佈局的複用,這樣我們就不用去每次都在各個不同的Activity或者Fragment 裏寫重複的佈局,但是後面兩個是來幹嘛的呢,舉個栗子,比如我們平常可能會寫一個標題佈局,然後用include 標籤將它包括在其它的佈局中,這時候你的include標籤包裹的內容假設如下

<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
    android:layout_height="60dp">
    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" 
        android:layout_alignParentLeft="true"
        android:layout_centerVertical="true"
        android:text="返回"/>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" 
        android:layout_centerInParent="true"
        android:text="標題"/>
</RelativeLayout>

然後我們使用時

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_alignParentTop="true"
        android:layout_height="wrap_content">
        <include
            layout="@layout/layout_title"/>
    </RelativeLayout>

其實include實現的效果就是將包裹的佈局給替換過來,如果我們將上述第一個佈局中的內容替換到下面佈局的話,是不是發現多了一個RelativeLayout 的層級,但是這個層級好像又沒有辦法去除,因爲如果採用這種include的方式,必須用一個基礎的佈局來包裹他,否則在複雜的場景中,沒法確定它的位置。這個時候,Merge就來了,怎麼使用呢,很簡單,我們只需要將title佈局的根標籤換爲Merge 即可,這樣最後加載的時候,merge這一層就相於會被忽略掉,也就是減少一層佈局的層級,帶來繪製上的優化。

接下來咱們再看ViewStub,相信看到Stub這個後綴應該都能猜到,這個是用來懶加載的,怎麼使用呢?非常簡單,就拿上面的佈局來說,我們可以將主佈局中的include 標籤替換爲ViewStub,如下

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <ViewStub
        android:id="@+id/viewstub"
        android:layout="@layout/layout_title"   //注意這裏有前綴android:
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="Hello World!" />

</RelativeLayout>

這時我們再運行程序,發現標題欄並沒有顯示出來,然後我們在代碼中再去加載它

    private ViewStub viewStub;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        viewStub=findViewById(R.id.viewstub);
        viewStub.inflate();
    }

再次運行,我們發現標題欄顯示了,說明什麼問題,說明我們初次運行程序,這個佈局是不會被加載的,然後我們可以再需要的時候,去控制它來動態加載,這樣的好處就是可以提升界面的初次加載速度。但是它也有一些侷限性,比如包含的資源不能用merge 標籤,同時這個加載過後就不能再次控制它隱藏了,也就是說這個inflate() 操作是一次性的,無法實現顯示->隱藏->顯示 的效果。

說完了這兩個標籤,咱們再說一些佈局邏輯上的優化,這個就比較抽象了,首先我們對比一下常用佈局的性能:
RelativeLayout默認是測量兩次,這個可以在RelativeLayoutonMeasure 源碼中看到,那爲什麼要測量兩次呢?因爲在RelativeLayout 中是沒有一個明確的垂直排列和水平排列的概念的,它的子view必須要先經過水平方向的測量,然後再經過垂直方向上的測量,以此來達到“相對佈局”的效果,但是LinearLayout 就不同了,因爲你可以爲它指定明確的方向,到底是水平還是垂直,所以在LinearLayout中,你可以看到它的onmeasure 方法中根據水平和垂直作了不同的測量處理,所以只會測量一次,但是有一個要特別注意的是,如果爲LinearLayout 的子view設置了weight 屬性時,那麼對於這部分子view也還是要測量兩次的,爲什麼呢?因爲每個子view要根據weight 值來分配剩餘空間時,所以第一遍測量需要計算出剩餘空間,也就是測量沒有設置weight 屬性的子view,然後第二遍測量再根據weight 屬性的值分配餘下空間。

ok,我們掌握上面的知識後,然後可以比較直觀的看到,LinearLayout 的性能是比RelativeLayout 要好的,但是設置weight 屬性會導致測量兩次,所以我們要儘量避免設置weight 屬性,還有比較新的佈局ConstraintLayout 它的onmeasure 也是對子view要測量兩次,但是它的測量耗時要比RelativeLayout 要低,因爲測量兩次,所以比LinearLayout 要耗時,但是它的約束佈局的方式能最大化程度的較少佈局的層級,即便是對於一個複雜的佈局,可能一次佈局嵌套都不用,直接一個ConstraintLayout 做根視圖就可以完成。

那說了這麼多,我們在實際使用中如何去合理運用這些佈局來寫出性能最優的佈局呢?我將如何選擇佈局來提升性能總結了兩個原則:

1.最優選讓佈局層級最少的佈局
2.在不改變佈局層級的情況下,這三者中選擇LinearLayout 最優

這裏對第二點稍作解釋,比如我有一個佈局裏只有一個控件,所以這個時候,不管是用RelativeLayout還是LinearLayout都可以達到效果,但是明顯應該選擇LinearLayout,因爲二者佈局層級一樣!

佈局這裏其實還有很多零碎的細節可以優化,比如,避免不必要的background 屬性設置,能用drawable或者shape等xml代碼實現的最好就不要選擇使用圖片等等。

最後,咱們再來總結一下佈局這塊,可以優化的點

1、使用include 標籤來實現佈局複用,同時使用merge 標籤減少include 使用時多出來的一層佈局
2、使用ViewStub 標籤來懶加載佈局,提高界面初次加載的速度
3、合理的運用佈局來最大化減少佈局層級,在不改變佈局層級的情況下,選擇LinearLayout 最優
4、不要給控件設置不必要的background屬性,合理使用圖片資源,能用xml代碼實現的就不要用圖片

2.容器的選擇

大家都知道在Java中,最常用的容器就是HashMap了,所以我們大部分開發者在平常的開發中,需要用到容器時,可能想都不會想,需要用到鍵值對形式的存儲時,直接敲出HashMap,彷彿這是一個默認的標配一樣,其實不然,細節決定成敗,我們看看這樣一些類,SparseArray , SparseLongArrayArrayMapArraySet,怎麼樣,都認識嗎,首先簡單介紹一下它們,SparseArrayArrayMap是安卓官方爲安卓量身打造的存儲容器,專門用於安卓這種數據量不大,但是操作頻繁的場景,那這二者的區別又是什麼呢,該怎麼使用呢?

這二者的使用方法非常簡單,可以說幾乎與HashMap無異,但是SparseArray 可以做到避免裝箱和拆箱,因爲它的鍵只能爲int,如果覺得int類型的範圍不夠,還有LongSparseArray,他和SparseArray 唯一的區別就是,它的鍵爲 long 類型,同時,SparseArrayvalueObject 類型,然後針對value的不同, 還有SparseBooleanArray,表示value 值只能爲boolean 類型,SparseIntArray,表示value 值只能爲int 類型,SparseLongArray,表示value 值只能爲long 類型,如下

SparseArray          <int, Object>
LongSparseArray      <long, Object>
SparseBooleanArray   <int, boolean>
SparseIntArray       <int, int>
SparseLongArray      <int, long>

HashMap 相比,HashMap 的鍵值對採用的都是泛型,所以就不可避免的需要自動拆箱和裝箱,而裝箱和拆箱是非常耗時的,但是因爲設計原理的不同,SparseArray 查找是二分查找,效率自然沒有HashMap高,但是在數據量很少的情況下,這個效率帶來的損耗相對裝箱拆箱帶來的損耗是可以忽略不計的,所以在安卓這種少數據量的情況下,如果允許int類型作鍵,那麼就毫不猶豫的選擇SparseArray 吧.

這時候你可能會說,那要是鍵不能爲int 類型呢,沒關係,咱們還有ArrayMap ,ArrayMap也是安卓官方爲安卓量身打造的容器,它和HashMap 一樣,鍵值對是採用的泛型,但是相對HashMap 來說,在數據量很少的情況下,它在設計時採用的空間緩存機制使得內存利用率極高,同時它支持使用索引來迭代,而HashMap 只能使用迭代器來迭代,迭代效率要比HashMap 高很多。

但是上述說的兩種情況都是在數據量很少的情況下SparseArrayArrayMap 所具有的優點,但是安卓中大部分場景都是數據量很少的情況,所以我們在符合要求的情況下要儘量根據具體情況選用這兩個容器來提高性能,而不是籠統的一概選擇HashMap

然後我們也總結一下容器選擇這塊

1.允許使用int 作爲鍵時,選用SparseArray 可以避免裝箱拆箱,性能最佳
2.不允許使用int 作爲鍵時,選用ArrayMap 提高內存利用率,提高迭代效率
3.數據量很大時(大於1000),還是使用HashMap較優

有關SparseArrayArrayMap 的具體實現原理可以看我的這篇文章:
Android集合框架之SparseArray、ArrayMap超級詳解

3.處理內存泄露

說到內存泄露,很難去以一個完整具體的方法去完全解決,只能是通過編碼習慣和在常見的內存泄露的場景多加留心以避免,內存優化中,最老生常談的一個問題就是內存泄露,我們首先來看看內存泄露。

什麼叫內存泄露?當一個對象已經不需要再使用了,本該被回收時,而有另外一個正在使用的對象持有它的引用,從而就導致對象不能被回收。這種導致了本該被回收的對象不能被回收而停留在堆內存中的現象,就叫做內存泄漏

其實內存泄露也是一個很大的概念,要舉出具體的例子也有很多,但是如果不掌握分析內存泄露的方法,以及不知道爲什麼會內存泄露,就很難在實際項目中去避免它, 或者簡單點,也就是需要掌握大部分場景中內存泄露的原理,下面舉一個具體的實例來說明
我們在之前使用Handler 傳遞消息時,可能會這樣寫:

public class MainActivity extends AppCompatActivity {

    private Handler handler=new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            Log.d("Main",(int)msg.obj+"");
        }
    };

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

    public void send(View view){
        Message message=Message.obtain();
        message.obj=1;
        handler.sendMessage(message);
    }
}

ok,這樣寫在使用上沒有任何問題,編譯也能通過,也能運行,也能正確接收到消息得到結果。
但是細心一點的話,你會發現AndroidStudio非常智能友好的提示警告,具體警告信息爲:handler應該是靜態的,否則可能會發生內存泄露。什麼意思呢?我們現在來分析一下,爲什麼會發生內存泄露,首先在Java中,非靜態的內部類和非靜態的匿名內部類都會隱式的持有一份對外部類的引用,而靜態的內部類則不會包含對外部類的引用,這裏我們聲明的handler 對象其實就是一個匿名內部類聲明的對象,所以handler 此時會持有外部類的引用

handler 持有外部類引用說明什麼呢,因爲我們這裏是在主線程使用的,主線程中每一個Message 對象都會由LooperMessageQueue取出,最後傳遞給handler,然後執行handleMessage 方法。好了,現在我們假設當前Activity需要被回收,但是handler 卻還有message沒有處理完(可能爲延遲發送的message導致,或者message太多等等原因),所以這個handler 對象不會被回收,而handler 持有當前Activity 的引用,導致Activity 也沒法被正常回收,此時,就發生Activity泄露啦!

ok,我們知道了泄露的原因,就應該學會根據原因找到解決辦法,我們這裏導致泄露的原因就是:hander 持有了外部類對象,也就是Activity 的引用,那麼我們的解決思路就是只需要消除這個引用就行,但是這在Java規範中是不允許的,但是有一個解決方案是可以達到和消除他們之間的引用有異曲同工之妙的效果的,那就是讓handler持有外部類的弱引用

java的引用一共分爲四種,強引用、弱引用、軟引用、虛引用,不懂的童鞋可以百度啦

handler持有的外部類引用爲弱引用時,在上述情況中,當Activity再次回收時,因爲handler 持有的是弱引用,所以此時並不會阻礙Activity被回收。
ok,我們現在找到了解決辦法,來看看是如何用代碼解決的吧

public class MainActivity extends AppCompatActivity {

    private Handler handler;

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

    public void send(View view){
        Message message=Message.obtain();
        message.obj=1;
        handler.sendMessage(message);
    }

    private static class MyHandler extends Handler {
        private final WeakReference<MainActivity> mActivity;

        MyHandler(MainActivity activity) {
            mActivity = new WeakReference<>(activity);
        }
        @Override
        public void handleMessage(Message msg)
        {
            MainActivity activity = mActivity.get();
            if (activity != null) {
                Log.d("Main",(int)msg.obj+"");
            }
        }
    }
}

核心就在於靜態內部類中將MainActivity的對象設置爲了WeakReference,同時設置MyHandler 爲靜態內部類。

OK, 我們通過上面這個比較典型的例子應該能直觀感受到內存泄露的查找以及解決的整個過程,當然在實際開發中,這種潛在的內存泄露就需要我們用一雙慧眼去識別了,當然爲了更好的處理這樣一個複雜的問題,我們可以對一些常見的容易泄露的場景做一些總結,這樣當我們遇到這些場景的時候,就要提醒自己格外小心了。目前我知道的主要如下:

  • 非靜態內部類和非靜態的匿名內部類會持有外部類的引用
  • 使用Handler(其實就是上面的非靜態的匿名內部類持有外部類引用的情況)
  • bitmap及時回收
  • 自定義view獲取自定義屬性後,TypedArray 及時回收
  • 靜態view(比如private static Button btn)會持有Activity的引用
  • 廣播等註冊之後沒有取消註冊,當然還有各種Manager,一般有register方法的,都會有unregister方法,所以要記得在合適的時機調用
  • WebView也容易泄露(這個接觸的比較少,不過作爲提醒,還是記錄下來)

當然了,如果要完全羅列出來所有的情況是不可能的,而且也沒有這個必要,我們只要學會在遇到內存泄露了怎麼分析,怎麼根據原因解決問題,那就基本ok了,最後再提醒一下,泄露中最容易發生的就是Activity和Bitmap的泄露。

4.緩存優化處理

緩存優化處理其實在Android中已經非常常見了,你可能不知道,但是你一定悄無聲息的使用過,比如最簡單的使用Glide 加載圖片時,就使用到了緩存處理,但是這是別人已經封裝好了,但是這畢竟是別人幫咱們做的,那要是下次遇到需要緩存其它對象的時候,你真的知道該怎麼做嗎

首先,我們都知道Android的內存是非常有限的,所以這就導致一個問題,沒有足夠的空間讓我們緩存,緩存的時候必須要有取捨,所以這就涉及到一個策略問題,Android其實已經給我們提供了一個策略,這個策略其實就是計算機系統中的一個策略,叫最近最少使用算法,什麼意思呢?就是當緩存容量達到閾值的時候,必須要去除掉已緩存的某一個對象,然後加入新的需要緩存的對象,而選取的原則就是該對象是在最近的某段時間內,是訪問最少的,其實概念有點難懂,說白話一點就是要從緩存中刪除的對象是我目前最不需要的對象,而判斷標準就是我最後訪問它的時間距離現在的時間最長。

知道了最近最少使用算法,我們在緩存的時候就可以使用這個策略,那麼如何實現呢?當然我們知道該策略的原理之後,完全可以自己手動實現一個,但是Android早已考慮到了這一點,提供了原生的類LruCache 供我們使用,我們只需要簡單的put、get即可實現採取最近最少使用策略的一個緩存,基本用法和hashmap很像,LruCache的鍵值對採用泛型,構造方法中的參數代表緩存的最大數量

LruCache<String,Bitmap> lruCache=new LruCache<>(5);
lruCache.put("1",bitmap);
Bitmap bitmap=lruCache.get("1");

當然我們在使用的時候,一定要記得在合適的地方去釋放緩存,這些小細節一定要注意,而且多線程的同步問題,也要記得去控制。

當然了,這裏的LruCache 是用於內存的,那麼用於本地緩存的有沒有現成的可以使用的呢?當然有,就是DiskLruCache 這個是JakeWharton大神的一個開源庫,比較久了,但是使用起來還是OK的,具體使用我就不贅述啦,放上地址:DiskLruCache

掌握了緩存的策略之後,我們還需要知道緩存應該緩存在哪個地方,這時候就冒出來了另一個東東——三級緩存

什麼是三級緩存?很簡單,取對象的時候,首先從內存中取,若內存中沒有,再從本地取,若本地還是沒有,才從網絡上取。我們很容易理解,這樣做的效果其實就相當於最大化程度實現對象的快速獲取,因爲這三者在執行時需要的時間是:從內存取 < 從本地文件取 < 請求網絡取。對象的獲取速度快了,那麼界面展現的時間就很快,自然帶給用戶的體驗就非常流暢。

三級緩存的常用代碼如下(bitmap舉例):

Bitmap bitmap = null;
//首先從內存取
bitmap = mLruCacheUtils.getBitmapFromMemory(url);
if(bitmap != null)
{
    imageview.setImageBitmap(bitmap);
    return;
}
//再從本地取
bitmap = mLocalCacheUtils.getBitmapFromLocal(url);
if(bitmap != null)
{
    imageview.setImageBitmap(bitmap);
    mMemoryCacheUtils.setBitmapToMemory(url, bitmap);   //記得將圖片保存在內存
    return;
}
//最後才走網絡
bitmap = mNetCacheUtils.getBitmapFromNet(url);
imageview.setImageBitmap(bitmap);
mMemoryCacheUtils.setBitmapToMemory(url, bitmap);//保存在內存
mLocalCacheUtils.setBitmapToLocal(url, bitmap);//保存在內存

基本在緩存這裏,採用三級緩存+Lru,是我目前知道比較常用的方式,當然爲了達到理想的性能,僅僅使用緩存是不夠的,還是會存在諸多不足,所以實際使用既要處理好緩存,同時還要處理好其他的細節,比如合理使用三方庫,及時釋放,壓縮處理,延遲刪除,佔位設置(針對圖片),異步加載,懶加載等等,媽耶,一口氣說了這麼多,讓我喝口水先。

5.啓動頁優化

首先我們擴展一下視野,啓動這裏有一些專有名詞挺有意思的,它們是,冷啓動、熱啓動。
什麼是冷啓動?當應用啓動時,後臺沒有該應用的進程。
什麼是熱啓動?當應用啓動時,後臺有該應用的進程。

我們關心和主要優化的也就是冷啓動,那在冷啓動的時候,安卓系統發生了什麼呢?
首先從Zygote進程中fork出一個新的進程給應用,然後創建初始化Application,然後創建初始化MainActivity,然後是熟悉的oncreate -> onStart -> onresume 然後再measure -> layout -> draw 到這裏爲止,我們就看到了我們的畫面。

那再回到啓動頁優化,最容易碰到的問題就是經典的白屏黑屏問題,具體的原因主要就是我們在applicaton 創建和初始化中做了過多的耗時操作,一般都是各種三方框架的初始化,所以我們在這裏可以優化一下用戶的體驗,具體的解決方案其實準確來說是不算優化的,只能說是一個視覺上的優化,將這個白屏的界面設置爲透明或者我們自己想要展現給用戶的圖片等等,達到“欺騙”用戶視覺的目的。

具體的操作方法爲在styles文件中重新聲明一個繼承系統主題的style,然後我們在這裏設置透明或者我們要展示給用戶的圖片,然後將啓動頁Activitytheme 屬性設置爲我們聲明的style, 代碼如下
如果想設置爲透明的話,代碼如下

    //啓動頁的theme
    <style name="WelcomeTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="android:windowIsTranslucent">true</item>
        <item name="android:windowNoTitle">true</item>
    </style>

如果想指定具體的圖片的話,就這樣設置

    //啓動頁的theme
    <style name="WelcomeTheme" parent="Theme.AppCompat.Light.NoActionBar">
        <item name="android:windowBackground">@mipmap/welcome_bg</item>
        <item name="android:windowNoTitle">true</item>
    </style>

然後在AndroidManifest文件中給相應的Activity 設置上去

<activity android:name=".WelcomeActivity"
    android:theme="@style/WelcomeTheme">
......

ok,除了這個之外,我們要想真正解決啓動慢的問題,還是要靠我們優化代碼邏輯,比如我們在Applciation 中要儘量少的進行耗時操作,越“精簡”越好,但是我們可以WelcomeActivity 中來做一些數據的初始化等耗時操作,因爲我們的WelcomeActivity 不管什麼情況下都是要經過三秒或者兩秒才進入到主界面,所以把這個時間利用起來豈不是美滋滋。

然後還有另外一種場景需要我們注意,假設用戶不小心誤點了我們的APP。正常情況下,用戶可能並沒有耐心等到三秒之後進入主界面,而是果斷按back鍵直接在WelcomeActivity 退出,如果這裏沒有做處理的話,用戶過了幾秒鐘會在瀏覽其它應用的時候,突然跳轉到我們的應用,顯然這是極不友好的,那麼爲什麼會有這樣的現象呢,

我這裏就放一下我當時的代碼

public class WelcomeActivity extends AppCompatActivity{
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_welcome);
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                startActivity(new Intent(WelcomeActivity.this,MainActivity.class));
            }
        },3000);
    }
}

咋一看,確實沒啥問題,功能也都實現了,現在我們結合問題的現象來分析,用戶點擊back鍵退出Activity 的時候,我們此時的handler 還沒有完成它的使命,因爲消息延遲3秒才處理,所以三秒後,即便按back鍵退出了WelcomeActivity ,此時並沒有銷燬activity,只是處於onstop 狀態,所以仍然需要執行意圖跳轉到主界面的activity,那我們知道原因了,如何解決呢

如下,我們只需要在onstop方法中移除handler的runnable回調即可

    @Override
    protected void onStop() {
        super.onStop();
        handler.removeCallbacks(runnable);
    }

或者換一種簡單暴力的思維,直接禁止返回按鍵,重寫onBackPressed 方法也可以解決。

可以使用的工具

這裏主要講一下我所知道的可以通過哪些方式或者工具來檢測問題。

首先我們來了解一下有一個東東叫嚴苛模式,我們可以通過開啓這個嚴苛模式來檢查問題,它主要用來檢測線程策略和VM策略,線程策略檢測的內容包括網絡請求、磁盤讀寫、以及其它在線程中執行的邏輯耗時操作,VM策略檢測的內容包括Activity泄露、資源未關閉、各種對象泄露等等。

嚴苛模式在Android中對應的類就是StrickMode ,好了,我們現在以一個簡單的例子來看一下它的使用

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                .detectActivityLeaks()//檢測Activity泄露
                .penaltyLog()//在Logcat中打印相關日誌
                .build());
        MyThread thread=new MyThread();
        thread.start();

    }
    class MyThread extends Thread {
        @Override
        public void run() {
            try {
                Thread.sleep(60 * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

很明顯這裏存在一個MainActivity泄露,願意就是非靜態內部類MyThread定義的對象thread 持有MainActivity 的引用,導致MainActivity 無法釋放。當然解決辦法就是將MyThread 改爲靜態的即可。
現在我們運行上述代碼,然後不斷的旋轉手機來頻繁地創建和銷燬Activity,然後,來看看StrictMode 到底會打印什麼,我們通過日誌觀察,發現打印如下

08-05 22:32:28.928 7856-7856/com.hq.testperformance E/StrictMode: class com.hq.testperformance.MainActivity; instances=2; limit=1  //instance表示MainActivity的實例個數,現在是2個,表示已經泄露了
    android.os.StrictMode$InstanceCountViolation: class com.hq.testperformance.MainActivity; instances=2; limit=1
        at android.os.StrictMode.setClassInstanceLimit(StrictMode.java:1)
08-05 22:32:30.540 7856-7856/com.hq.testperformance E/StrictMode: class com.hq.testperformance.MainActivity; instances=3; limit=1//3個
    android.os.StrictMode$InstanceCountViolation: class com.hq.testperformance.MainActivity; instances=3; limit=1
        at android.os.StrictMode.setClassInstanceLimit(StrictMode.java:1)
08-05 22:32:32.093 7856-7856/com.hq.testperformance E/StrictMode: class com.hq.testperformance.MainActivity; instances=4; limit=1
    android.os.StrictMode$InstanceCountViolation: class com.hq.testperformance.MainActivity; instances=4; limit=1
        at android.os.StrictMode.setClassInstanceLimit(StrictMode.java:1)
08-05 22:32:33.745 7856-7856/com.hq.testperformance E/StrictMode: class com.hq.testperformance.MainActivity; instances=5; limit=1
    android.os.StrictMode$InstanceCountViolation: class com.hq.testperformance.MainActivity; instances=5; limit=1
        at android.os.StrictMode.setClassInstanceLimit(StrictMode.java:1)

我們可以很清晰地看到,隨着我們不斷的旋轉屏幕以創建銷燬MainActivity ,發現實例個數在一直增加,這也正好驗證了我們上述的預測。

通過開啓嚴苛模式,你還可以做很多其他的事,這裏我就不一一舉例了,只要使用得當,還是一個非常好用的輔助工具。

接下來我們再看看Android官方給我們設計的檢測內存的親兒子Android Profiler,我們可以在AndroidStudio的地步工具欄中找到它,我們現在再次運行上述程序代碼,同樣的旋轉屏幕的操作,然後打開Android Profiler工具欄,我們依然可以在這裏直觀形象的看到內存情況,在右側,我們可以看到MainActivity的實例個數,很明顯泄露了
這裏寫圖片描述
不得不說這個工具真的非常強大,而且圖形界面非常的友好,想檢測什麼只需要在對應的操作欄點擊對應的條目即可。具體的使用我就不細說啦,留給大家去慢慢發掘吧!

其實AndroidStudio還有另外一個好東西,那就是代碼檢測,lint工具,具體的位置如下圖
這裏寫圖片描述
點擊之後會彈出提示讓你選擇檢測的代碼範圍,可以是整個項目,也可以是一個Module,也可以是一個具體的Activity,功能也是非常的強大,具體檢測的結果會分的非常詳細,一目瞭然,點擊就可以查看具體的不規範信息,如下
這裏寫圖片描述
這個雖然不是專門很對內存的,但是它針對的是代碼優化也可以在無形之中幫助我們改善代碼的結構,去除一些隱患,間接的提高我們的程序性能!

介紹完了官方的一些工具,再介紹一個開源的神器—-LeakCanary,之所以說它是神器,其實一點也不爲過,畢竟人家的強大功能在那裏,問題的層級分析給你分析的明明白白,先放上官方地址LeakCanary,具體的配置非常簡單,如下

public class MyApplication extends Application {
    @Override public void onCreate() {
        super.onCreate();
        if (LeakCanary.isInAnalyzerProcess(this)) {//1
            // This process is dedicated to LeakCanary for heap analysis.
            // You should not init your app in this process.
            return;
        }
        LeakCanary.install(this);
    }
}

然後我們測試用例同樣使用上面的例子,不斷的旋轉屏幕以銷燬和創建MainActivity ,最終會在發生泄露的時候彈出一個提示,然後你會發現同時你的手機狀態欄多了一些推送,點擊進去,如下,可以查看到具體的泄露信息

通過這些信息,我們再來定位我們的錯誤,再找解決的辦法。

其它瑣碎的優化點

這個主要就是列舉一下我所知道的其它各種優化小細節,當做參考

  • 1.對於service,如果業務邏輯允許,並且佔用內存特別大的話,可以爲service 新開一個進程,因爲內存的分配是以進程爲單位的,而一個app就是一個進程,一個進程內存有限,所以新開進程不僅讓service的內存充足,也可以爲app主進程省下內存空間。
  • 2.在自定義view中或者其它manager 類的單例實現時,不要籠統的使用Activitycontext對象,所以儘量使用Applcaitioncontext,爲什麼呢,因爲一旦這個context超過了它本身的生命週期,就會導致泄露,典型的就是Activity泄露
  • 3.自定義view時,不要在onMeasureonLayoutonDraw裏創建對象,尤其是onDraw,否則會發生內存抖動
  • 4.有意識的避免裝箱和拆箱
  • 5.如果需要開啓多個線程,使用線程池來完成
  • 6.正則表達式很耗性能
  • 7.浮點運算比整數運算慢,能用 int 解決的就不要再麻煩float
  • 8.圖片的webp格式是一個你需要知道的格式
  • 9.如果可以,儘量將網絡請求合併,減傷網絡請求的次數
  • 10.同步時,減少鎖個數、減小鎖範圍
  • 未完待續

小結

我們現在再來總結一下正文中的內容,首先是關於性能優化的入手點,如下

  • 佈局優化
  • 容器的選擇
  • 處理內存泄露
  • 緩存優化處理
  • 啓動頁優化

其實還有很多其他的方面,但是限於本人水平,就沒有給大家深入講解,然後是幾個輔助工具的使用

  • StrickMode模式的開啓和使用
  • Android Profiler的使用
  • Android Lint工具的使用
  • LeakCanary開源庫的使用

如果真正深入分析的話,上述每一個小點都可以單獨拆分出來寫一篇文章,具體深入的話,是可以有很多東西的,本身性能優化就是一個龐大而雜的工作,需要耐心和技術,只有不斷的打磨和沉澱纔可以鍛造出一個性能優越的App,好啦,就到這啦,要是有什麼疑問的話,歡迎留言交流!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章