Android 性能優化最佳實踐

本文由玉剛說寫做平臺提供寫做贊助html

原做者:Mr.Sjava

版權聲明:本文版權歸微信公衆號玉剛說全部,未經許可,不得以任何形式轉載android

什麼是性能git

這張圖很好詮釋了什麼性能

快,穩,省,小,這四點很形象的表明了性能的四個方面,同時也讓咱們知道咱們App如今是不是款性能良好的APP,若是有一項不達標,那麼說明咱們的應用有待優化。程序員

不少時候咱們注重功能實現,保證能用,可是咱們會發現,這樣的應用很難拿的出手,裏面的槽點太多了,性能不好,可是又不知道從哪裏下手進行優化,那麼咱們就一步一步來,看看咱們到底應該怎麼優化咱們的APP。github

1 、佈局優化web

和UI相關的首先就是佈局,特別是在開發一些複雜界面的時候,一般咱們都是採用佈局嵌套的方法,每一個人的佈局思路不太同樣,寫出的也不太同樣,,因此就可能形成嵌套的層級過多。面試

官方 屏幕上的某個像素在同一幀的時間內被繪製了屢次。在多層次的UI結構裏面,若是不可見的UI也在作繪製的操做,這就會致使某些像素區域被繪製了屢次。這就浪費大量的CPU以及GPU資源。算法

白話 顯示一個佈局就比如咱們蓋一個房子,首先咱們要測量房子的大小,還要測量房間裏面各個傢俱的大小,和位置,而後進行擺放同時也要對房子進行裝修,若是咱們是一層,都在明面上,幹起活來敞亮也輕鬆,但是有的人的房子,喜歡各類隔斷,分紅一個一個的大隔斷間,每一個大隔斷間裏還有小隔斷間,小隔斷間裏有小小隔斷間,還有小小小隔斷間。。。N層隔斷間。數據庫

看到這些頭皮發麻吧,並且是一個大隔斷間裏面全部的小隔斷,小小隔斷等等都測量完擺放好,才能換另一個大隔斷,天呢,太浪費時間了,不能都直接都放外面嗎?也好擺放啊,這麼搞我怎麼擺,每一個隔斷間都要裝修一遍,太浪費時間了啊。

咱們的Android虛擬機也會這麼抱怨,我們家原本就不富裕,什麼都要省着用,你這麼搞,確定運轉有問題啊,那麼多嵌套的小隔斷間須要處理,都會佔用cpu計算的時間和GPU渲染的時間。顯示GPU過分繪製,分層以下如所示:

分層顏色.png
經過顏色咱們能夠知道咱們應用是否有多餘層次的繪製,若是一路飄紅,那麼咱們就要相應的處理了。

因此咱們有了第一個優化版本:

優化 1.0

  1. 若是父控件有顏色,也是本身須要的顏色,那麼就沒必要在子控件加背景顏色
  2. 若是每一個自控件的顏色不太同樣,並且能夠徹底覆蓋父控件,那麼就不須要再父控件上加背景顏色
  3. 儘可能減小沒必要要的嵌套
  4. 能用LinearLayout和FrameLayout,就不要用RelativeLayout,由於RelativeLayout控件相對比較複雜,測繪也想要耗時。

作到了以上4點只能說恭喜你,入門級優化已經實現了。

針對嵌套佈局,谷歌也是陸續出了一些新的方案。對就是includemergeViewStub三兄弟。

include能夠提升佈局的複用性,大大方便咱們的開發,有人說這個沒有減小布局的嵌套吧,對,include確實沒有,可是include和merge聯手搭配,效果那是槓槓滴。

merge的佈局取決於父控件是哪一個佈局,使用merge至關於減小了自身的一層佈局,直接採用父include的佈局,固然直接在父佈局裏面使用意義不大,因此會和include配合使用,既增長了佈局的複用性,用減小了一層佈局嵌套。

ViewStub它能夠按需加載,什麼意思?用到他的時候喊他一下,再來加載,不須要的時候像空氣同樣,在一邊靜靜的呆着,不吃你的米,也不花你家的錢。等須要的時候ViewStub中的佈局才加載到內存,多節儉持家啊。對於一些進度條,提示信息等等八百年才用一次的功能,使用ViewStub是極其合適的。這就是不用不知道,一用戒不了。

咱們開始進化咱們的優化

優化 1.1

  1. 使用include和merge增長複用,減小層級
  2. ViewStub按需加載,更加輕便

可能又有人說了:背景複用了,嵌套已經很精簡了,再精簡就實現了不了複雜視圖了,但是仍是一路飄紅,這個怎麼辦?面對這個問題谷歌給了咱們一個新的佈局ConstraintLayout

ConstraintLayout能夠有效地解決佈局嵌套過多的問題。ConstraintLayout使用約束的方式來指定各個控件的位置和關係的,它有點相似於 RelativeLayout,但遠比RelativeLayout要更強大(照抄隔壁IOS的約束佈局)。因此簡單佈局簡單處理,複雜佈局ConstraintLayout很好使,提高性能從佈局作起。

再次進化:

優化 1.2

  1. 複雜界面可選擇ConstraintLayout,可有效減小層級

二、繪製優化

咱們把佈局優化了,可是和佈局息息相關的還有繪製,這是直接影響顯示的兩個根本因素。

其實佈局優化了對於性能提高影響不算很大,可是是咱們最容易下手,最直接接觸的優化,因此無論能提高多少,哪怕只有百分之一的提高,咱們也要作,由於影響性能的地方太多了,每一個部分都提高一點,咱們應用就能夠提高不少了。

咱們平時感受的卡頓問題最主要的緣由之一是由於渲染性能,由於愈來愈複雜的界面交互,其中可能添加了動畫,或者圖片等等。咱們但願創造出愈來愈炫的交互界面,同時也但願他能夠流暢顯示,可是每每卡頓就發生在這裏。

這個是Android的渲染機制形成的,Android系統每隔16ms發出VSYNC信號,觸發對UI進行渲染,可是渲染未必成功,若是成功了那麼表明一切順利,可是失敗了可能就要延誤時間,或者直接跳過去,給人視覺上的表現,就是要麼卡了一會,要麼跳幀。

View的繪製頻率保證60fps是最佳的,這就要求每幀繪製時間不超過16ms(16ms = 1000/60),雖然程序很難保證16ms這個時間,可是儘可能下降onDraw方法中的複雜度老是切實有效的。

這個正常狀況下,每隔16ms draw()一下,很整齊,很流暢,很完美。

image.png
每每會發生以下圖的狀況,有個便祕的傢伙霸佔着,一幀畫面拉的時間那麼長,這一下可不就卡頓了嘛。把後面的時間給佔用了,後面只能延後,或者直接略過了。

image.png
既然問題找到了,那麼咱們確定要有相應的解決辦法,根本作法是 減輕onDraw()的負擔。

因此

第一點: onDraw方法中不要作耗時的任務,也不作過多的循環操做,特別是嵌套循環,雖然每次循環耗時很小,可是大量的循環勢必霸佔CPU的時間片,從而形成View的繪製過程不流暢。

第二點: 除了循環以外,onDraw()中不要建立新的局部對象,由於onDraw()方法通常都會頻繁大量調用,就意味着會產生大量的零時對象,不進佔用過的內存,並且會致使系統更加頻繁的GC,大大下降程序的執行速度和效率。

其實這兩點在android的UI線程中都適用。

升級進化:

優化2.0

  1. onDraw中不要建立新的局部對象
  2. onDraw方法中不要作耗時的任務

其實從渲染優化裏咱們也牽扯出了另外一個優化,那就是內存優化。

三、內存優化

內存泄漏指的是那些程序再也不使用的對象沒法被GC識別,這樣就致使這個對象一直留在內存當中,佔用了沒來就很少的內存空間。

內存泄漏是一個緩慢積累的過程,一點一點的給你,溫水煮青蛙通常,咱們每每很難直觀的看到,只能最後內存不夠用了,程序奔潰了,才知道里面有大量的泄漏,可是究竟是那些地方?估計是狼煙遍地,千瘡百孔,都不知道如何下手。怎麼辦?最讓人難受的是內存泄漏狀況那麼多,記不住,理解也不容易,關鍵是老會忘記。怎麼辦呢?老這麼下去也不是事,總不能面試的時候突擊,作項目的時候不知所措吧。因此必定要記住瞭解GC原理,這樣才能夠更準確的理解內存泄漏的場景和緣由。不懂GC原理的能夠先看一下這個JVM初探:內存分配、GC原理與垃圾收集器

原本GC的誕生是爲了讓java程序員更加輕鬆(這一點隔壁C++痛苦的一匹),java虛擬機會自動幫助咱們回收那些再也不須要的內存空間。經過引用計數法,可達性分析法等等方法,確認該對象是否沒有引用,是否能夠被回收。

有人會說真麼強悍的功能看起來無懈可擊啊,對,理論上能夠達到消除內存泄漏,可是不少人不按常理出牌啊,每每不少時候,有的對象還保持着引用,但邏輯上已經不會再用到。就是這一類對象,遊走於GC法律的邊緣,我沒用了,可是你又不知道我沒用了,就是這麼賴着不走,空耗內存。

由於有內存泄漏,因此內存被佔用愈來愈多,那麼GC會更容易被觸發,GC會愈來愈頻發,可是當GC的時候全部的線程都是暫停狀態的,須要處理的對象數量越多耗時越長,因此這也會形成卡頓。

image.png

那麼什麼狀況下會出現這樣的對象呢? 基本能夠分爲如下四大類: 一、集合類泄漏 二、單例/靜態變量形成的內存泄漏 三、匿名內部類/非靜態內部類 四、資源未關閉形成的內存泄漏

一、集合類泄漏

集合類添加元素後,仍引用着集合元素對象,致使該集合中的元素對象沒法被回收,從而致使內存泄露。

舉個栗子:

static List<Object> mList = new ArrayList<>();
   for (int i = 0; i < 100; i++) {
       Object obj = new Object();
      mList.add(obj);
       obj = null;
    }
複製代碼

當mList沒用的時候,咱們若是不作處理的話,這就是典型的佔着茅坑不拉屎,mList內部持有者衆多集合元素的對象,不泄露天理難容啊。解決這個問題也超級簡單。把mList清理掉,而後把它的引用也給釋放掉。

mList.clear();
  mList = null;
複製代碼

二、單例/靜態變量形成的內存泄漏

單例模式具備其 靜態特性,它的生命週期 等於應用程序的生命週期,正是由於這一點,每每很容易形成內存泄漏。 先來一個小栗子:

public class SingleInstance {

    private static SingleInstance mInstance;
    private Context mContext;

    private SingleInstance(Context context){
        this.mContext = context;
    }

    public static SingleInstance newInstance(Context context){
        if(mInstance == null){
            mInstance = new SingleInstance(context);
        }
        return sInstance;
    }
}

複製代碼

當咱們在Activity裏面使用這個的時候,把咱們Acitivty的context傳進去,那麼,這個單例就持有這個Activity的引用,當這個Activity沒有用了,須要銷燬的時候,由於這個單例還持有Activity的引用,因此沒法GC回收,因此就出現了內存泄漏,也就是生命週期長的持有了生命週期短的引用,形成了內存泄漏。

因此咱們要作的就是生命週期長的和生命週期長的玩,短的和短的玩。就比如你去商場,原本就是傳個話的,話說完就要走了,忽然保安過來非要拉着你的手,說要和你天長地久。只要商場在一天,他就要陪你一天。天呢?太可怕了。叔叔咱們不約,我有個人小夥伴,我還要上學呢,你趕忙找你的保潔阿姨去吧。你在商場的生命週期原本可能就是1分鐘,而保安的生命週期那是要和商場開關門一致的,因此不一樣生命週期的最好別一塊兒玩的好。

解決方案也很簡單:

public class SingleInstance {

    private static SingleInstance mInstance;
    private Context mContext;

    private SingleInstance(Context context){
        this.mContext = context.getApplicationContext();
    }

    public static SingleInstance newInstance(Context context){
        if(mInstance == null){
            mInstance = new SingleInstance(context);
        }
        return sInstance;
    }
}

複製代碼

還有一個經常使用的地方就是Toast。你應該知道和誰玩了吧。

三、匿名內部類/非靜態內部類

這裏有一張寶圖:

image.png
非靜態內部類他會持有他外部類的引用,從圖咱們能夠看到非靜態內部類的生命週期可能比外部類更長,這就是二樓的狀況一致了,若是非靜態內部類的周明週期長於外部類,在加上自動持有外部類的強引用,個人乖乖,想不泄漏都難啊。

咱們再來舉個栗子:

public class TestActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        new MyAscnyTask().execute();
    }

    class MyAscnyTask extends AsyncTask<Void, Integer, String>{
        @Override
        protected String doInBackground(Void... params) {
            try {
                Thread.sleep(100000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "";
        }
    }
}

複製代碼

咱們常常會用這個方法去異步加載,而後更新數據。貌似很日常,咱們開始學這個的時候就是這麼寫的,沒發現有問題啊,可是你這麼想想,MyAscnyTask是一個非靜態內部類,若是他處理數據的時間很長,極端點咱們用sleep 100秒,在這期間Activity可能早就關閉了,原本Activity的內存應該被回收的,可是咱們知道非靜態內部類會持有外部類的引用,因此Activity也須要陪着非靜態內部類MyAscnyTask一塊兒天荒地老。好了,內存泄漏就造成了。

怎麼辦呢?

既然MyAscnyTask的生命週期可能比較長,那就把它變成靜態,和Application玩去吧,這樣MyAscnyTask就不會再持有外部類的引用了。二者也相互獨立了。

public class TestActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        new MyAscnyTask().execute();
    }
//改了這裏 注意一下 static
   static  class MyAscnyTask extends AsyncTask<Void, Integer, String>{
        @Override
        protected String doInBackground(Void... params) {
            try {
                Thread.sleep(100000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return "";
        }
    }
}
複製代碼

說完非靜態內部類,我再來看看匿名內部類,這個問題很常見,匿名內部類和非靜態內部類有一個共同的地方,就是會只有外部類的強引用,因此這哥倆本質是同樣的。可是處理方法有些不同。可是思路絕對同樣。換湯不換藥。

舉個灰常熟悉的栗子:

public class TestActivity extends Activity {
private TextView mText;
    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
//do something
mText.setText(" do someThing");
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
mText = findVIewById(R.id.mText);
        //  匿名線程持有 Activity 的引用,進行耗時操做
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(100000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

    
        mHandler. sendEmptyMessageDelayed(0, 100000);
    }

複製代碼

想必這兩個方法是咱們常常用的吧,很熟悉,也是這麼學的,沒感受不對啊,老師就是這麼教的,經過咱們上面的分析,還這麼想嗎?關鍵是 耗時時間過長,形成內部類的生命週期大於外部類,對弈非靜態內部類,咱們能夠靜態化,至於匿名內部類怎麼辦呢?同樣把它變成靜態內部類,也就是說盡可能不要用匿名內部類。完事了嗎?不少人不注意這麼一件事,若是咱們在handleMessage方法裏進行UI的更新,這個Handler靜態化了和Activity沒啥關係了,可是好比這個mText,怎麼說?全寫是activity.mText,看到了吧,持有了Activity的引用,也就是說Handler費勁心思變成靜態類,自認爲不持有Activity的引用了,準確的說是不自動持有Activity的引用了,可是咱們要作UI更新的時候勢必會持有Activity的引用,靜態類持有非靜態類的引用,咱們發現怎麼又開始內存泄漏了呢?到處是坑啊,怎麼辦呢?咱們這裏就要引出弱引用的概念了。

引用分爲強引用,軟引用,弱引用,虛引用,強度一次遞減。

強引用 咱們平時不作特殊處理的通常都是強引用,若是一個對象具備強引用,GC寧肯OOM也毫不會回收它。看出多強硬了吧。

軟引用(SoftReference) 若是內存空間足夠,GC就不會回收它,若是內存空間不足了,就會回收這些對象的內存。

弱引用(WeakReference) 弱引用要比軟引用,更弱一個級別,內存不夠要回收他,GC的時候無論內存夠不夠也要回收他,簡直是弱的一匹。不過GC是一個優先級很低的線程,也不是太頻繁進行,因此弱引用的生活還過得去,沒那麼提心吊膽。

虛引用 用的甚少,我沒有用過,若是想了解的朋友,能夠自行谷歌百度。

因此咱們用弱引用來修飾Activity,這樣GC的時候,該回收的也就回收了,不會再有內存泄漏了。很完美。

public class TestActivity extends Activity {
    private TextView mText;
    private MyHandler myHandler = new MyHandler(TestActivity.this);
    private MyThread myThread = new MyThread();

    private static class MyHandler extends Handler {

        WeakReference<TestActivity> weakReference;

        MyHandler(TestActivity testActivity) {
            this.weakReference = new WeakReference<TestActivity>(testActivity);

        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            weakReference.get().mText.setText("do someThing");

        }
    }

    private static class MyThread extends Thread {

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

            try {
                sleep(100000);

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        mText = findViewById(R.id.mText);
        myHandler.sendEmptyMessageDelayed(0, 100000);
        myThread.start();
    }
//最後清空這些回調 
    @Override
    protected void onDestroy() {
        super.onDestroy();
        myHandler.removeCallbacksAndMessages(null);
    }
複製代碼

四、資源未關閉形成的內存泄漏

  • 網絡、文件等流忘記關閉
  • 手動註冊廣播時,退出時忘記 unregisterReceiver()
  • Service 執行完後忘記 stopSelf()
  • EventBus 等觀察者模式的框架忘記手動解除註冊

這些須要記住又開就有關,具體作法也很簡單就不一一贅述了。給你們介紹幾個很好用的工具: 一、leakcanary傻瓜式操做,哪裏有泄漏自動給你顯示出來,很直接很暴力。 二、咱們平時也要多使用Memory Monitor進行內存監控,這個分析就有些難度了,能夠上網搜一下具體怎麼使用。 三、Android Lint 它能夠幫助咱們發現代碼機構 / 質量問題,同時提供一些解決方案,內存泄露的會飄黃,用起來很方便,具體使用方法上網學習,這裏很少作說明了。

so

優化3.0

  1. 解決各個狀況下的內存泄漏,注意平時代碼的規範。

四、啓動速度優化

不知道你們有沒有細心發現,咱們的應用啓動要比別的大廠的要慢,要花費更多的時間,明明他們的包體更大,app更復雜,怎麼啓動時間反而比咱們的短呢?

可是這塊的優化關注的人不多,由於App經常伴有閃屏頁,因此這個問題看起來就不是問題了,可是一款好的應用是絕對不容許這樣的,我加閃屏頁是個人事,啓動速度慢絕對不能夠。

app啓動分爲冷啓動(Cold start)、熱啓動(Hot start)和溫啓動(Warm start)三種。

冷啓動(Cold start)

冷啓動是指應用程序從頭開始:系統的進程在此開始以前沒有建立應用程序。冷啓動發生在諸如自設備啓動以來首次啓動應用程序或自系統終止應用程序以來。

在冷啓動開始時,系統有三個任務。這些任務是: 一、加載並啓動應用程序 二、啓動後當即顯示應用程序的空白啓動窗口 三、建立應用程序進程

當系統爲咱們建立了應用進程以後,開始建立應用程序對象。

一、啓動主線程 二、建立主Activity 三、加載佈局 四、屏幕布局 五、執行初始繪製

應用程序進程完成第一次繪製後,系統進程會交換當前顯示的背景窗口,將其替換爲主活動。此時,用戶能夠開始使用該應用程序。至此啓動完成。

image.png

Application建立

當Application啓動時,空白的啓動窗口將保留在屏幕上,直到系統首次完成繪製應用程序。此時,系統進程會交換應用程序的啓動窗口,容許用戶開始與應用程序進行交互。這就是爲何咱們的程序啓動時會先出現一段時間的黑屏(白屏)。

若是咱們有本身的Application,系統會onCreate()在咱們的Application對象上調用該方法。以後,應用程序會生成主線程(也稱爲UI線程),並經過建立主要活動來執行任務。

從這一點開始,App就按照他的 應用程序生命週期階段進行

Activity建立

應用程序進程建立活動後,活動將執行如下操做:

  1. 初始化值。
  2. 調用構造函數。
  3. 調用回調方法,例如 Activity.onCreate(),對應Activity的當前生命週期狀態。

一般,該 onCreate()方法對加載時間的影響最大,由於它以最高的開銷執行工做:加載和膨脹視圖,以及初始化活動運行所需的對象。

熱啓動(Hot start)

應用程序的熱啓動比冷啓動要簡單得多,開銷也更低。在一個熱啓動中,系統都會把你的Activity帶到前臺。若是應用程序的Activity仍然駐留在內存中,那麼應用程序能夠避免重複對象初始化、佈局加載和渲染。

熱啓動顯示與冷啓動方案相同的屏幕行爲:系統進程顯示空白屏幕,直到應用程序完成呈現活動。

溫啓動(Warm start)

溫啓動包含了冷啓動時發生的一些操做,與此同時,它表示的開銷比熱啓動少,有許多潛在的狀態能夠被認爲是溫暖的開始。

場景:

  • 用戶退出您的應用,但隨後從新啓動它。該過程可能已繼續運行,但應用程序必須經過調用從頭開始從新建立Activity 的onCreate()
  • 系統將您的應用程序從內存中逐出,而後用戶從新啓動它。須要從新啓動進程和活動,可是在調用onCreate()的時候能夠從Bundle(savedInstanceState)獲取數據。

瞭解完啓動過程,咱們就知道哪裏會影響咱們啓動的速度了。在建立應用程序和建立Activity期間均可能會出現性能問題。

這裏是慢的定義:

  • 啓動須要5秒或更長時間。
  • 啓動須要2秒或更長時間。
  • 啓動須要1.5秒或更長時間。

不管何種啓動,咱們的優化點都是: Application、Activity建立以及回調等過程

谷歌官方給的建議是: 一、利用提早展現出來的Window,快速展現出來一個界面,給用戶快速反饋的體驗; 二、避免在啓動時作密集沉重的初始化(Heavy app initialization); 三、避免I/O操做、反序列化、網絡操做、佈局嵌套等。

具體作法:

針對1:利用提早展現出來的Window,快速展現出來一個界面

使用Activity的windowBackground主題屬性來爲啓動的Activity提供一個簡單的drawable。

Layout XML file:

<layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque">
  <!-- The background color, preferably the same as your normal theme -->
  <item android:drawable="@android:color/white"/>
  <!-- Your product logo - 144dp color version of your app icon -->
  <item>
    <bitmap
      android:src="@drawable/product_logo_144dp"
      android:gravity="center"/>
  </item>
</layer-list>
複製代碼

Manifest file:

<activity ...
android:theme="@style/AppTheme.Launcher" />
複製代碼

這樣在啓動的時候,會先展現一個界面,這個界面就是Manifest中設置的Style,等Activity加載完畢後,再去加載Activity的界面,而在Activity的界面中,咱們將主題從新設置爲正常的主題,從而產生一種快的感受。其實就是個障眼法而已,提早讓你看到了假的界面。也算是一種不錯的方法,可是治標不治本。

針對2:避免在啓動時作密集沉重的初始化

咱們審視一下咱們的MyApplication裏面的操做。初始化操做有友盟,百度,bugly,數據庫,IM,神策,圖片加載庫,網絡請求庫,廣告sdk,地圖,推送,等等,這麼多須要初始化,Application的任務過重了,啓動不慢纔怪呢。

怎麼辦呢?這些還都是必要的,不能不去初始化啊,那就只能異步加載了。可是並非全部的均可以進行異步處理。這裏分狀況給出一些建議: 一、好比像友盟,bugly這樣的業務非必要的能夠的異步加載。 二、好比地圖,推送等,非第一時間須要的能夠在主線程作延時啓動。當程序已經啓動起來以後,在進行初始化。 三、對於圖片,網絡請求框架必須在主線程裏初始化了。

同時由於咱們通常會有閃屏頁面,也能夠把延時啓動的地圖,推進的啓動在這個時間段裏,這樣合理安排時間片的使用。極大的提升了啓動速度。

針對3:避免I/O操做、反序列化、網絡操做、佈局嵌套等。

這個不用多說了,你們應該知道如何去作了,有些上文也有說明。

so

優化4.0

  1. 利用提早展現出來的Window,快速展現出來一個界面,給用戶快速反饋的體驗;
  2. 避免在啓動時作密集沉重的初始化(Heavy app initialization);
  3. 避免I/O操做、反序列化、網絡操做、佈局嵌套等。

五、包體優化

我作過兩年的海外應用產品,深知包體大小對於產品新增的影響,包體小百分之五,可能新增就增長百分之五。若是產品基數很大,這個提高就更可怕了。無論怎麼說,咱們要減肥,要六塊腹肌,不要九九歸一的大肚子。

既然要瘦身,那麼咱們必須知道APK的文件構成,解壓apk:

image.png

assets文件夾 存放一些配置文件、資源文件,assets不會自動生成對應的 ID,而是經過 AssetManager 類的接口獲取。

res目錄 res 是 resource 的縮寫,這個目錄存放資源文件,會自動生成對應的 ID 並映射到 .R 文件中,訪問直接使用資源 ID。

META-INF 保存應用的簽名信息,簽名信息能夠驗證 APK 文件的完整性。

AndroidManifest.xml 這個文件用來描述 Android 應用的配置信息,一些組件的註冊信息、可以使用權限等。

classes.dex Dalvik 字節碼程序,讓 Dalvik 虛擬機可執行,通常狀況下,Android 應用在打包時經過 Android SDK 中的 dx 工具將 Java 字節碼轉換爲 Dalvik 字節碼。

resources.arsc 記錄着資源文件和資源 ID 之間的映射關係,用來根據資源 ID 尋找資源。

咱們須要從代碼和資源兩個方面去減小響應的大小。

一、首先咱們可使用lint工具,若是有沒有使用過的資源就會打印以下的信息(不會使用的朋友能夠上網看一下)

res/layout/preferences.xml: Warning: The resource R.layout.preferences appears
    to be unused [UnusedResources]
複製代碼

同時咱們能夠開啓資源壓縮,自動刪除無用的資源

android {
    ...
    buildTypes {
        release {
            shrinkResources true
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'),
                    'proguard-rules.pro'
        }
    }
複製代碼

無用的資源已經被刪除了,接下來哪裏能夠在瘦身呢?

二、咱們可使用可繪製對象,某些圖像不須要靜態圖像資源; 框架能夠在運行時動態繪製圖像。Drawable對象(<shape>以XML格式)能夠佔用APK中的少許空間。此外,XML Drawable對象產生符合材料設計準則的單色圖像。

上面的話官方,簡單說來就是,能本身用XML寫Drawable,就本身寫,能不用公司的UI切圖,就別和他們說話,我們本身造,作本身的UI,美滋滋。並且這種圖片佔用空間會很小。

三、重用資源,好比一個三角按鈕,點擊前三角朝上表明收起的意思,點擊後三角朝下,表明展開,通常狀況下,咱們會用兩張圖來切換,咱們徹底能夠用旋轉的形式去改變

<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
    android:drawable="@drawable/ic_thumb_up"
    android:pivotX="50%"
    android:pivotY="50%"
    android:fromDegrees="180" />
複製代碼

好比同一圖像的着色不一樣,咱們能夠用android:tint和tintMode屬性,低版本(5.0如下)可使用ColorFilter

四、壓縮PNG和JPEG文件 您能夠減小PNG文件的大小,而不會丟失使用工具如圖像質量 pngcrushpngquant,或zopflipng。全部這些工具均可以減小PNG文件的大小,同時保持感知的圖像質量。

五、使用WebP文件格式 可使用圖像的WebP文件格式,而不是使用PNG或JPEG文件。WebP格式提供有損壓縮(如JPEG)以及透明度(如PNG),但能夠提供比JPEG或PNG更好的壓縮。

可使用Android Studio將現有的BMP,JPG,PNG或靜態GIF圖像轉換爲WebP格式。

六、使用矢量圖形 可使用矢量圖形來建立與分辨率無關的圖標和其餘可伸縮Image。使用這些圖形能夠大大減小APK大小。一個100字節的文件能夠生成與屏幕大小相關的清晰圖像。

可是,系統渲染每一個VectorDrawable對象須要花費大量時間 ,而較大的圖像須要更長的時間才能顯示在屏幕上。所以,請考慮僅在顯示小圖像時使用這些矢量圖形。

不要把AnimationDrawable用於建立逐幀動畫,由於這樣作須要爲動畫的每一個幀包含一個單獨的位圖文件,這會大大增長APK的大小。

七、代碼混淆 使用proGuard 代碼混淆器工具,它包括壓縮、優化、混淆等功能。這個你們太熟悉了。很少說了。

android {
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile(‘proguard-android.txt'), 'proguard-rules.pro' } } 複製代碼

八、插件化。 好比功能模塊放在服務器上,按需下載,能夠減小安裝包大小。

so

優化5.0

  1. 代碼混淆
  2. 插件化
  3. 資源優化

六、耗電優化

咱們可能對耗電優化不怎麼感冒,沒事,谷歌這方面作得也不咋地,5.0以後纔有像樣的方案,講實話這個優化的優先級沒有前面幾個那麼高,可是咱們也要了解一些避免耗電的坑,至於更細的耗電分析可使用這個Battery Historian

Battery Historian 是由Google提供的Android系統電量分析工具,從手機中導出bugreport文件上傳至頁面,在網頁中生成詳細的圖表數據來展現手機上各模塊電量消耗過程,最後經過App數據的分析制定出相關的電量優化的方法。

咱們來談一下怎麼規避電老虎吧。

谷歌推薦使用JobScheduler,來調整任務優先級等策略來達到下降損耗的目的。JobScheduler能夠避免頻繁的喚醒硬件模塊,形成沒必要要的電量消耗。避免在不合適的時間(例如低電量狀況下、弱網絡或者移動網絡狀況下的)執行過多的任務消耗電量。

具體功能: 一、能夠推遲的非面向用戶的任務(如按期數據庫數據更新); 二、當充電時才但願執行的工做(如備份數據); 三、須要訪問網絡或 Wi-Fi 鏈接的任務(如向服務器拉取配置數據); 四、零散任務合併到一個批次去按期運行; 五、當設備空閒時啓動某些任務; 六、只有當條件獲得知足, 系統纔會啓動計劃中的任務(充電、WIFI...);

同時谷歌針對耗電優化也提出了一個懶惰第一的法則:

減小 你的應用程序能夠刪除冗餘操做嗎?例如,它是否能夠緩存下載的數據而不是重複喚醒無線電以從新下載數據?

推遲 應用是否須要當即執行操做?例如,它能夠等到設備充電才能將數據備份到雲端嗎?

合併 能夠批處理工做,而不是屢次將設備置於活動狀態嗎?例如,幾十個應用程序是否真的有必要在不一樣時間打開收音機發送郵件?在一次喚醒收音機期間,是否能夠傳輸消息?

谷歌在耗電優化這方面確實顯得有些無力,但願之後能夠退出更好的工具和解決方案,否則這方面的優化優先級仍是很低。付出和回報所差太大。

so

優化6.0

  1. 使用JobScheduler調度任務
  2. 使用懶惰法則

六、ListView和 Bitmap優化

針對ListView優化,主要是合理使用ViewHolder。建立一個內部類ViewHolder,裏面的成員變量和view中所包含的組件個數、類型相同,在convertview爲null的時候,把findviewbyId找到的控件賦給ViewHolder中對應的變量,就至關於先把它們裝進一個容器,下次要用的時候,直接從容器中獲取。

如今咱們如今通常使用RecyclerView,自帶這個優化,不過仍是要理解一下原理的好。 而後能夠對接受來的數據進行分段或者分頁加載,也能夠優化性能。

對於Bitmap,這個咱們使用的就比較多了,很容易出現OOM的問題,圖片內存的問題能夠看一下我以前寫的這篇文章一張圖片佔用多少內存

Bitmap的優化套路很簡單,粗暴,就是讓壓縮。 三種壓縮方式: 1.對圖片質量進行壓縮 2.對圖片尺寸進行壓縮 3.使用libjpeg.so庫進行壓縮

對圖片質量進行壓縮

public static Bitmap compressImage(Bitmap bitmap){  
            ByteArrayOutputStream baos = new ByteArrayOutputStream();  
            //質量壓縮方法,這裏100表示不壓縮,把壓縮後的數據存放到baos中  
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos);  
            int options = 100;  
            //循環判斷若是壓縮後圖片是否大於50kb,大於繼續壓縮  
            while ( baos.toByteArray().length / 1024>50) {  
                //清空baos  
                baos.reset();  
                bitmap.compress(Bitmap.CompressFormat.JPEG, options, baos);  
                options -= 10;//每次都減小10  
            }  
            //把壓縮後的數據baos存放到ByteArrayInputStream中  
            ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());  
            //把ByteArrayInputStream數據生成圖片  
            Bitmap newBitmap = BitmapFactory.decodeStream(isBm, null, null);  
            return newBitmap;  
        }  
複製代碼

對圖片尺寸進行壓縮

/**
     * 按圖片尺寸壓縮 參數是bitmap
     * @param bitmap
     * @param pixelW
     * @param pixelH
     * @return
     */
    public static Bitmap compressImageFromBitmap(Bitmap bitmap, int pixelW, int pixelH) {
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, os);
        if( os.toByteArray().length / 1024>512) {//判斷若是圖片大於0.5M,進行壓縮避免在生成圖片(BitmapFactory.decodeStream)時溢出
            os.reset();
            bitmap.compress(Bitmap.CompressFormat.JPEG, 50, os);//這裏壓縮50%,把壓縮後的數據存放到baos中
        }
        ByteArrayInputStream is = new ByteArrayInputStream(os.toByteArray());
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        options.inPreferredConfig = Bitmap.Config.RGB_565;
        BitmapFactory.decodeStream(is, null, options);
        options.inJustDecodeBounds = false;
        options.inSampleSize = computeSampleSize(options , pixelH > pixelW ? pixelW : pixelH ,pixelW * pixelH );
        is = new ByteArrayInputStream(os.toByteArray());
        Bitmap newBitmap = BitmapFactory.decodeStream(is, null, options);
        return newBitmap;
    }


    /**
     * 動態計算出圖片的inSampleSize
     * @param options
     * @param minSideLength
     * @param maxNumOfPixels
     * @return
     */
    public static int computeSampleSize(BitmapFactory.Options options, int minSideLength, int maxNumOfPixels) {
        int initialSize = computeInitialSampleSize(options, minSideLength, maxNumOfPixels);
        int roundedSize;
        if (initialSize <= 8) {
            roundedSize = 1;
            while (roundedSize < initialSize) {
                roundedSize <<= 1;
            }
        } else {
            roundedSize = (initialSize + 7) / 8 * 8;
        }
        return roundedSize;
    }

    private static int computeInitialSampleSize(BitmapFactory.Options options, int minSideLength, int maxNumOfPixels) {
        double w = options.outWidth;
        double h = options.outHeight;
        int lowerBound = (maxNumOfPixels == -1) ? 1 : (int) Math.ceil(Math.sqrt(w * h / maxNumOfPixels));
        int upperBound = (minSideLength == -1) ? 128 :(int) Math.min(Math.floor(w / minSideLength), Math.floor(h / minSideLength));
        if (upperBound < lowerBound) {
            return lowerBound;
        }
        if ((maxNumOfPixels == -1) && (minSideLength == -1)) {
            return 1;
        } else if (minSideLength == -1) {
            return lowerBound;
        } else {
            return upperBound;
        }
    }
複製代碼

使用libjpeg.so庫進行壓縮 能夠參考這篇Android性能優化系列之Bitmap圖片優化: https://blog.csdn.net/u012124438/article/details/66087785)

優化7.0

  1. ListView使用ViewHolder,分段,分頁加載
  2. 壓縮Bitmap

八、響應速度優化

影響響應速度的主要因素是主線程有耗時操做,影響了響應速度。因此響應速度優化的核心思想是避免在主線程中作耗時操做,把耗時操做異步處理。

九、線程優化

線程優化的思想是採用線程池,避免在程序中存在大量的Thread。線程池能夠重用內部的線程,從而避免了現場的建立和銷燬所帶來的性能開銷,同時線程池還能有效地控制線程池的最大併發數,避免大量的線程因互相搶佔系統資源從而致使阻塞現象發生。

《Android開發藝術探索》對線程池的講解很詳細,不熟悉線程池的能夠去了解一下。

  • 優勢: 一、減小在建立和銷燬線程上所花的時間以及系統資源的開銷。 二、如不使用線程池,有可能形成系統建立大量線程而致使消耗完系統內存以及」過分切換」。

  • 須要注意的是: 一、若是線程池中的數量爲達到核心線程的數量,則直接會啓動一個核心線程來執行任務。      二、若是線程池中的數量已經達到或超過核心線程的數量,則任務會被插入到任務隊列中標等待執行。      三、若是(2)中的任務沒法插入到任務隊列中,因爲任務隊列已滿,這時候若是線程數量未達到線程池規定最大值,則會啓動一個非核心線程來執行任務。     四、若是(3)中線程數量已經達到線程池最大值,則會拒絕執行此任務,ThreadPoolExecutor會調用RejectedExecutionHandler的rejectedExecution方法通知調用者。

十、微優化

這些微優化能夠在組合時提升總體應用程序性能,但這些更改不太可能致使顯着的性能影響。選擇正確的算法和數據結構應始終是咱們的首要任務,以提升代碼效率。

  • 編寫高效代碼有兩個基本規則: 一、不要作你不須要作的工做 二、若是能夠避免,請不要分配內存

一、避免建立沒必要要的對象 對象建立永遠不是免費的,雖然每個的代價不是很大,可是總歸是代價的不是嗎?能不建立何須要浪費資源呢?

二、首選靜態(這裏說的是特定情景) 若是您不須要訪問對象的字段,請使您的方法保持靜態。調用速度將提升約15%-20%。這也是很好的作法,由於你能夠從方法簽名中看出,調用方法不能改變對象的狀態

三、對常量使用static final 此優化僅適用於基本類型和String常量,而不適用於 任意引用類型。儘管如此,static final儘量聲明常量是一種好習慣。

四、使用加強的for循環語法 加強for循環(for-each)可用於實現Iterable接口和數組的集合。對於集合,分配一個迭代器來對hasNext()和進行接口調用next()。使用一個 ArrayList,手寫計數循環快約3倍,但對於其餘集合,加強的for循環語法將徹底等效於顯式迭代器用法。

五、避免使用浮點數 根據經驗,浮點數比Android設備上的整數慢約2倍

結尾

本文篇幅有限,性能優化的方面不少,每一項深刻下去,不寫個幾十萬字是結束不了,因此不少都是淺嘗輒止,但願能夠拋磚引玉,用個人拙劣的文章,給你們一些幫助。性能優化須要走的路還很遠,但願能和各位同窗一同前行,一塊兒進步。

參考: Android APP 性能優化的一些思考 https://www.cnblogs.com/cr330326/p/8011523.html

谷歌官方 http://developer.android.com/topic/performance/

Android性能優化系列之Bitmap圖片優化 https://blog.csdn.net/u012124438/article/details/66087785

Android 內存泄漏總結 https://yq.aliyun.com/articles/3009

歡迎關注個人微信公衆號,接收第一手技術乾貨
相關文章
相關標籤/搜索