Android 常見性能優(yōu)化總結(jié)

前言

北京時間2022年8月16日凌晨 Android 13正式版全球也已發(fā)布,android 每次發(fā)新版本都會圍繞“優(yōu)化”這個詞展開,android 13也不例外 個人認(rèn)為他最大的亮點(diǎn)是UI界面優(yōu)化,其實(shí)經(jīng)過這么多年更新迭代 Android系統(tǒng)性能也已經(jīng)非常流暢,可以在體驗(yàn)上完全媲美iOS。但是,到了各大廠商手里,改源碼、自定義系統(tǒng),使得Android原生系統(tǒng)變得魚龍混雜,同時開發(fā)人員技術(shù)水平的參差不齊,即使很多手機(jī)在跑分軟件性能非常高,打開應(yīng)用依然存在卡頓現(xiàn)象。另外,隨著產(chǎn)品內(nèi)容迭代,功能越來越復(fù)雜,UI頁面也越來越豐富,也成為流暢運(yùn)行的一種阻礙。綜上所述,對APP進(jìn)行性能優(yōu)化已成為開發(fā)者該有的一種綜合素質(zhì),也是開發(fā)者能夠完成高質(zhì)量應(yīng)用程序作品的保證。

優(yōu)化點(diǎn)

Android的性能優(yōu)化,主要是從以下幾個方面進(jìn)行優(yōu)化的:


分類

整體優(yōu)化方向:


優(yōu)化總結(jié)

優(yōu)化方案

一:流暢性

1. 響應(yīng)速度:

多線程的方式 包括:AsyncTask、繼承 Thread類、實(shí)現(xiàn) Runnable接口、Handler消息機(jī)制、HandlerThread等
注:實(shí)際開發(fā)中,當(dāng)一個進(jìn)程發(fā)生了ANR后,系統(tǒng)會在 /data/anr目錄下創(chuàng)建一個文件 traces.txt,通過分析該文件可定位出ANR的原因

2. 啟動速度:app啟動(Application )、頁面啟動(activity)、h5(wabView)

app啟動(Application ):app啟動分為冷啟動(Cold start)、熱啟動(Hot start)和溫啟動(Warm start)三種(概念不再贅述,類似博客挺多挺詳細(xì)的)。
無論何種啟動,我們的優(yōu)化點(diǎn)都是: Application、Activity創(chuàng)建以及回調(diào)等過程
谷歌官方給的建議是:
1、利用提前展示出來的Window,快速展示出來一個界面,給用戶快速反饋的體驗(yàn);
2、避免在啟動時做密集沉重的初始化(Heavy app initialization);
3、避免I/O操作、反序列化、網(wǎng)絡(luò)操作、布局嵌套等。

方案1: 治標(biāo)不治本的迷惑性做法:主題設(shè)置圖片資源

<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>

方案2: 避免在啟動時做密集沉重的初始化

application 中init三方SDK耗時優(yōu)化:
1、業(yè)務(wù)非必要的可以的異步加載(線程池)。
2、非第一時間需要的可以在主線程做延時啟動。當(dāng)程序已經(jīng)啟動起來之后,在進(jìn)行初始化。
3、對于圖片,網(wǎng)絡(luò)請求框架必須在主線程里初始化了。
4、啟動圖+開屏廣告(以時間換時間 利用廣告加載的時間初始化sdk)
ps:以上操作也僅僅是在應(yīng)用層能做總有一些能到極限的操作
5、 通過利用多進(jìn)程、修改類加載過程、dex壓縮等方向去實(shí)現(xiàn)

3. 頁面顯示速度

方案1: 頁面布局優(yōu)化

如果父控件有顏色,也是自己需要的顏色,那么就不必在子控件加背景顏色
如果每個自控件的顏色不太一樣,而且可以完全覆蓋父控件,那么就不需要再父控件上加背景顏色
盡量減少不必要的嵌套
能用LinearLayout和FrameLayout,就不要用RelativeLayout,因?yàn)镽elativeLayout控件相對比較復(fù)雜,測繪也想要耗時。
使用include和merge增加復(fù)用,減少層級
ViewStub按需加載,更加輕便
復(fù)雜界面可選擇ConstraintLayout,可有效減少層級
ps:開發(fā)者模式,查看是否存在過度繪制

方案2:繪制優(yōu)化

onDraw中不要創(chuàng)建新的局部對象、也不要做耗時的任務(wù)
ps:我們平時感覺的卡頓問題最主要的原因之一是因?yàn)殇秩拘阅埽驗(yàn)樵絹碓綇?fù)雜的界面交互,其中可能添加了動畫,或者圖片等等。我們希望創(chuàng)造出越來越炫的交互界面,同時也希望他可以流暢顯示,但是往往卡頓就發(fā)生在這里。
這個是Android的渲染機(jī)制造成的,Android系統(tǒng)每隔16ms發(fā)出VSYNC信號,觸發(fā)對UI進(jìn)行渲染,但是渲染未必成功,如果成功了那么代表一切順利,但是失敗了可能就要延誤時間,或者直接跳過去,給人視覺上的表現(xiàn),就是要么卡了一會,要么跳幀。
View的繪制頻率保證60fps是最佳的,這就要求每幀繪制時間不超過16ms(16ms = 1000/60),雖然程序很難保證16ms這個時間,但是盡量降低onDraw方法中的復(fù)雜度總是切實(shí)有效的。

方案3:如果頁面就是比較復(fù)雜,層級再怎么縮減也達(dá)不到預(yù)取效果該怎么辦:

代碼動態(tài)創(chuàng)建View 通過addview的形式添加

方案4:主線程耗時操作排查

開啟strictmode,這樣一來,主線程的耗時操作都將以告警的形式呈現(xiàn)到logcat當(dāng)中
StrictMode,嚴(yán)苛模式,是Android提供的一種運(yùn)行時檢測機(jī)制,用于檢測代碼運(yùn)行時的一些不規(guī)范的操作,最常見的場景是用于發(fā)現(xiàn)主線程的IO操作和網(wǎng)絡(luò)讀寫等耗時的操作。

方案5:viewStub&merge&ViewStub的使用
方案6:頁面刷新

item 刷新局部刷新
復(fù)雜Activity、fragment:利用LiveData等數(shù)據(jù)共享

二:穩(wěn)定性

1. ANR:

以下四個條件都可以造成ANR發(fā)生:
InputDispatching Timeout:5秒內(nèi)無法響應(yīng)屏幕觸摸事件或鍵盤輸入事件
BroadcastQueue Timeout :在執(zhí)行前臺廣播(BroadcastReceiver)的onReceive()函數(shù)時10秒沒有處理完成,后臺為60秒。
Service Timeout :前臺服務(wù)20秒內(nèi),后臺服務(wù)在200秒內(nèi)沒有執(zhí)行完畢。
ContentProvider Timeout :ContentProvider的publish在10s內(nèi)沒進(jìn)行完。
實(shí)際開發(fā)中,當(dāng)一個進(jìn)程發(fā)生了ANR后,系統(tǒng)會在 /data/anr目錄下創(chuàng)建一個文件 traces.txt,通過分析該文件可定位出ANR的原因
ps:總之記住這一點(diǎn):盡量避免在主線程(UI線程)中作耗時操作,耗時操作都放到子線程
多注意一些常用庫出現(xiàn)ANR的情況:如:SharedPreferences等

1. Crash:

1:頻繁GC

內(nèi)存抖動
1短時間內(nèi)創(chuàng)建了大量對象同時又被快速釋放。比如在一個大循環(huán)里去不斷創(chuàng)建對象,會導(dǎo)致頻繁gc;
內(nèi)存泄漏
2內(nèi)存泄漏會導(dǎo)致可用內(nèi)存逐漸變少,而且內(nèi)存碎片加多,這也會增多gc次數(shù),甚至可能發(fā)生OOM
3一次申請?zhí)髢?nèi)存空間
由于內(nèi)存碎片的存在,就算內(nèi)存本身足夠,但由于碎片導(dǎo)致無法找到一塊大空間,這也會觸發(fā)gc;

2:在Android平臺上常見的OOM有如下幾種:

1、使用static修飾Context變量,Context被Hold住了導(dǎo)致Activity無法銷毀。比如 Thread
2、Bitmap沒有及時回收,調(diào)用recycle()函數(shù)并不能立即釋放Bitmap,讀取Bitmap到內(nèi)存的時候沒有做采樣率的設(shè)置;
3、線程數(shù)超限,proc/pid/status中記錄的線程數(shù)超過proc/sys/kernel/threads-max中規(guī)定的最大線程數(shù),場景如隨意創(chuàng)建線程,沒有使用線程池來管理;
4、文件描述符(fd)超限,proc/pid/fd下文件數(shù)目超過proc/pid/limits中的限制,場景如大量的Socket請求或者文件打開操作。
5、Cursor使用問題,使用完之后沒有及時關(guān)閉。
6、Java堆內(nèi)存超限,申請的堆內(nèi)存超過Runtime.getRuntime().maxMemory()。
7、Adapter沒使用緩存的convertView;
8、廣播注冊之后沒有反注冊;
9、WebView沒有銷毀,應(yīng)該調(diào)用destroy()函數(shù)去銷毀;
10、數(shù)組內(nèi)對象過多,沒有及時清理。
11、EventBus 等觀察者模式的框架忘記手動解除注冊
ps:分析工具:LeakCanary、Android Studio自帶工具Profilerd等

3:異常日志搜集:三方的奔潰搜集工具很多,不在贅述,android 崩潰日志收集以及上傳服務(wù)器 -自定義我自己實(shí)現(xiàn)的感興趣的可以看看

三:apk體積

1. 安裝包結(jié)構(gòu):
該圖來自網(wǎng)絡(luò)截圖
2. 優(yōu)化方案:

一:最省(資源)

1. 內(nèi)存:

部分上面OOM分析中已經(jīng)提到了,下面做個簡單補(bǔ)充
1:SparseArray代替HashMap
2:使用Link優(yōu)化項(xiàng)目
3:Bitmap壓縮:一個比較好的壓縮庫,Curzibn/Luban: Luban(魯班)—Image compression with efficiency very close to WeChat Moments/可能是最接近微信朋友圈的圖片壓縮算法 (github.com)
4:推薦使用match_parent,或者固定尺寸
5:線程優(yōu)化(線程池)

2. 網(wǎng)絡(luò):
該圖來自網(wǎng)絡(luò)

Android 性能優(yōu)化之網(wǎng)絡(luò)優(yōu)化 這篇文章寫的挺詳細(xì)不在贅述
ps:網(wǎng)上有很多寫的比較好的網(wǎng)絡(luò)請求框架的封裝可以借鑒一下思路
Retrofit+OkHttp+Kotlin協(xié)程 網(wǎng)絡(luò)請求架構(gòu)等等

1. 耗電量:

Battery Historian
是由Google提供的Android系統(tǒng)電量分析工具,從手機(jī)中導(dǎo)出bugreport文件上傳至頁面,在網(wǎng)頁中生成詳細(xì)的圖表數(shù)據(jù)來展示手機(jī)上各模塊電量消耗過程,最后通過App數(shù)據(jù)的分析制定出相關(guān)的電量優(yōu)化的方法。
谷歌推薦使用JobScheduler,來調(diào)整任務(wù)優(yōu)先級等策略來達(dá)到降低損耗的目的。JobScheduler可以避免頻繁的喚醒硬件模塊,造成不必要的電量消耗。避免在不合適的時間(例如低電量情況下、弱網(wǎng)絡(luò)或者移動網(wǎng)絡(luò)情況下的)執(zhí)行過多的任務(wù)消耗電量。
具體功能:
1、可以推遲的非面向用戶的任務(wù)(如定期數(shù)據(jù)庫數(shù)據(jù)更新);
2、當(dāng)充電時才希望執(zhí)行的工作(如備份數(shù)據(jù));
3、需要訪問網(wǎng)絡(luò)或 Wi-Fi 連接的任務(wù)(如向服務(wù)器拉取配置數(shù)據(jù));
4、零散任務(wù)合并到一個批次去定期運(yùn)行;
5、當(dāng)設(shè)備空閑時啟動某些任務(wù);
6、只有當(dāng)條件得到滿足, 系統(tǒng)才會啟動計劃中的任務(wù)(充電、WIFI...);

實(shí)現(xiàn)一個通過JobScheduler的簡單demo

public class JobSchedulerService extends JobService{
    private String TAG = JobSchedulerService.class.getSimpleName();

    @Override
    public boolean onStartJob(JobParameters jobParameters) {
        Log.d(TAG, "onStartJob:" + jobParameters.getJobId());

        if(true) {
            // JobService在主線程運(yùn)行,如果我們這里需要處理比較耗時的業(yè)務(wù)邏輯需單獨(dú)開啟一條子線程來處理并返回true,
            // 當(dāng)給定的任務(wù)完成時通過調(diào)用jobFinished(JobParameters params, boolean needsRescheduled)告知系統(tǒng)。

            //假設(shè)開啟一個線程去下載文件
            new DownloadTask().execute(jobParameters);

            return true;

        }else {
            //如果只是在本方法內(nèi)執(zhí)行一些簡單的邏輯話返回false就可以了
            return false;
        }
    }

    /**
     * 比如我們的服務(wù)設(shè)定的約束條件為在WIFI狀態(tài)下運(yùn)行,結(jié)果在任務(wù)運(yùn)行的過程中WIFI斷開了系統(tǒng)
     * 就會通過回掉onStopJob()來通知我們停止運(yùn)行,正常的情況下不會回掉此方法
     *
     * @param jobParameters
     * @return
     */
    @Override
    public boolean onStopJob(JobParameters jobParameters) {
        Log.d(TAG, "onStopJob:" + jobParameters.getJobId());

        //如果需要服務(wù)在設(shè)定的約定條件再次滿足時再次執(zhí)行服務(wù)請返回true,反之false
        return true;
    }

    class DownloadTask extends AsyncTask<JobParameters, Object, Object> {
        JobParameters mJobParameters;

        @Override
        protected Object doInBackground(JobParameters... jobParameterses) {
            mJobParameters = jobParameterses[0];

            //比如說我們這里處理一個下載任務(wù)
            //或是處理一些比較復(fù)雜的運(yùn)算邏輯
            //...

            try {
                Thread.sleep(30*1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            return null;
        }

        @Override
        protected void onPostExecute(Object o) {
            super.onPostExecute(o);
            //如果在onStartJob()中返回true的話,處理完成邏輯后一定要執(zhí)行jobFinished()告知系統(tǒng)已完成,
            //如果需要重新安排服務(wù)請true,反之false
            jobFinished(mJobParameters, false);
        }
    }
}

public class MainActivity extends Activity{
    private JobScheduler mJobScheduler;
    private final int JOB_ID = 1;

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

        mJobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE );

        //通過JobInfo.Builder來設(shè)定觸發(fā)服務(wù)的約束條件,最少設(shè)定一個條件
        JobInfo.Builder jobBuilder = new JobInfo.Builder(JOB_ID, new ComponentName(this, JobSchedulerService.class));

        //循環(huán)觸發(fā),設(shè)置任務(wù)每三秒定期運(yùn)行一次
        jobBuilder.setPeriodic(3000);

        //單次定時觸發(fā),設(shè)置為三秒以后去觸發(fā)。這是與setPeriodic(long time)不兼容的,
        // 并且如果同時使用這兩個函數(shù)將會導(dǎo)致拋出異常。
        jobBuilder.setMinimumLatency(3000);

        //在約定的時間內(nèi)設(shè)置的條件都沒有被觸發(fā)時三秒以后開始觸發(fā)。類似于setMinimumLatency(long time),
        // 這個函數(shù)是與 setPeriodic(long time) 互相排斥的,并且如果同時使用這兩個函數(shù),將會導(dǎo)致拋出異常。
        jobBuilder.setOverrideDeadline(3000);

        //在設(shè)備重新啟動后設(shè)置的觸發(fā)條件是否還有效
        jobBuilder.setPersisted(false);

        // 只有在設(shè)備處于一種特定的網(wǎng)絡(luò)狀態(tài)時,它才觸發(fā)。
        // JobInfo.NETWORK_TYPE_NONE,無論是否有網(wǎng)絡(luò)均可觸發(fā),這個是默認(rèn)值;
        // JobInfo.NETWORK_TYPE_ANY,有網(wǎng)絡(luò)連接時就觸發(fā);
        // JobInfo.NETWORK_TYPE_UNMETERED,非蜂窩網(wǎng)絡(luò)中觸發(fā);
        // JobInfo.NETWORK_TYPE_NOT_ROAMING,非漫游網(wǎng)絡(luò)時才可觸發(fā);
        jobBuilder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED);

        //設(shè)置手機(jī)充電狀態(tài)下觸發(fā)
        jobBuilder.setRequiresCharging(true);

        //設(shè)置手機(jī)處于空閑狀態(tài)時觸發(fā)
        jobBuilder.setRequiresDeviceIdle(true);
        
        //得到JobInfo對象
        JobInfo jobInfo = jobBuilder.build();

        //設(shè)置開始安排任務(wù),它將返回一個狀態(tài)碼
        //JobScheduler.RESULT_SUCCESS,成功
        //JobScheduler.RESULT_FAILURE,失敗
        if (mJobScheduler.schedule(jobInfo) == JobScheduler.RESULT_FAILURE) {
            //安排任務(wù)失敗
        }

        //停止指定JobId的工作服務(wù)
        mJobScheduler.cancel(JOB_ID);
        //停止全部的工作服務(wù)
        mJobScheduler.cancelAll();
    }

最主要的一點(diǎn)

記得在Manifest文件內(nèi)配置Service <service android:name=".JobSchedulerService" android:permission="android.permission.BIND_JOB_SERVICE"/>

總結(jié)

作為一個程序員,性能優(yōu)化是常有的事情,不管是桌面應(yīng)用還是web應(yīng)用,不管是前端還是后端,不管是單點(diǎn)應(yīng)用還是分布式系統(tǒng),所以我們應(yīng)該更加去注重性能優(yōu)化的一個使用和技術(shù)上提升,綜上所述,對APP進(jìn)行性能優(yōu)化已成為開發(fā)者該有的一種綜合素質(zhì),也是開發(fā)者能夠完成高質(zhì)量應(yīng)用程序作品的保證。
以下文檔有關(guān)于單向具體優(yōu)化的操作,感興趣的童鞋可以看一下
Android 性能優(yōu)化 | Android 開源項(xiàng)目 | Android Open Source Project (google.cn)
Android深度性能優(yōu)化--啟動優(yōu)化(一篇就夠) - 知乎 (zhihu.com)
(28條消息) Android性能優(yōu)化常見問題,與詳細(xì)解決思路方法!_chuhe1989的博客-CSDN博客

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內(nèi)容