對於Android開發者來講,懂得基本的應用開發技能每每是不夠,由於無論是工做仍是面試,都須要開發者懂得大量的性能優化,這對提高應用的體驗是很是重要的。對於Android開發來講,性能優化主要圍繞以下方面展開:啓動優化、渲染優化、內存優化、網絡優化、卡頓檢測與優化、耗電優化、安裝包體積優化、安全問題等。html
一個應用的啓動快慢是可以直接影響用戶的使用體驗的,若是啓動較慢可能會致使用戶卸載放棄該應用程序。前端
對於Android應用程序來講,根據啓動方式能夠分爲冷啓動,熱啓動和溫啓動三種。java
能夠看到,熱啓動是啓動最快的,溫啓動則是介於冷啓動和熱啓動之間的一種啓動方式。下而冷啓動則是最慢的,由於它會涉及不少進程的建立,下面是冷啓動相關的任務流程:python
在冷啓動模式下,系統會啓動三個任務:android
一旦系統建立應用程序進程,應用程序進程就會進入下一階段,並完成以下的一些事情。web
應用程序進程完成第一次繪製後,系統進程會交換當前顯示的背景窗口,將其替換爲主活動。此時,用戶能夠開始使用該應用程序了。由於App應用進程的建立過程是由手機的軟硬件決定的,因此咱們只能在這個建立過程當中進行一些視覺優化。面試
在冷啓動的時候,當應用程序進程被建立後,就須要設置啓動窗口的主題。目前,大部分的 應用在啓動會都會先進入一個閃屏頁(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>
設置主題的方式只能應用在要求不是很高的場景,而且這種優化治標不治本,關鍵還在於代碼的優化。爲了進行優化,咱們須要掌握一些基本的數據。編程
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。
日誌方式
埋點方式是另外一種統計線上時間的方式,這種方式經過記錄啓動時的時間和結束的時間,而後取兩者差值便可。首先,須要定義一個統計時間的工具類:
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") } }
在作啓動優化的時候,能夠藉助三方工具來幫助咱們理清各個階段的方法或者線程、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
這裏能夠你們普及下各個字端的含義:
Systrace開銷較小,屬於輕量級的工具,而且能夠直觀反映CPU的利用率。
Android系統每隔16ms就會從新繪製一次Activity,所以,咱們的應用必須在16ms內完成屏幕刷新的所有邏輯操做,每一幀只能停留16ms,不然就會出現掉幀現象。Android應用卡頓與否與UI渲染有直接的關係。
對於大多數手機的屏幕刷新頻率是60hz,也就是若是在1000/60=16.67ms內沒有把這一幀的任務執行完畢,就會發生丟幀的現象,丟幀是形成界面卡頓的直接緣由,渲染操做一般依賴於兩個核心組件:CPU與GPU。CPU負責包括Measure,Layout等計算操做,GPU負責Rasterization(柵格化)操做。
所謂柵格化,就是將矢量圖形轉換爲位圖的過程,手機上顯示是按照一個個像素來顯示的,好比將一個Button、TextView等組件拆分紅一個個像素顯示到手機屏幕上。而UI渲染優化的目的就是減輕CPU、GPU的壓力,除去沒必要要的操做,保證每幀16ms之內處理完全部的CPU與GPU的計算、繪製、渲染等等操做,使UI順滑、流暢的顯示出來。
UI渲染優化的第一步就是找到Overdraw(過分繪製),即描述的是屏幕上的某個像素在同一幀的時間內被繪製了屢次。在重疊的UI佈局中,若是不可見的UI也在作繪製的操做或者後一個控件將前一個控件遮擋,會致使某些像素區域被繪製了屢次,從而增長了CPU、GPU的壓力。
那麼如何找出佈局中Overdraw的地方呢?很簡單,就是打開手機裏開發者選項,而後將調試GPU過分繪製的開關打開便可,而後就能夠看到應用的佈局是否被Overdraw,以下圖所示。
藍色、淡綠、淡紅、深紅表明了4種不一樣程度的Overdraw狀況,1x、2x、3x和4x分別表示同一像素上同一幀的時間內被繪製了屢次,1x就表示一次(最理想狀況),4x表示4次(最差的狀況),而咱們須要消除的就是3x和4x。
咱們知道,自定義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方法,繪製以前裁剪出一個區域,這樣繪製的時候只在這區域內繪製,超出部分不會繪製出來。從新運行上面的代碼,效果以下圖所示。
Hierarchy Viewer 是 Android Device Monitor 中內置的一種工具,可以讓開發者測量佈局層次結構中每一個視圖的佈局速度,以及幫助開發者查找視圖層次結構致使的性能瓶頸。Hierarchy Viewer能夠經過紅、黃、綠三種不一樣的顏色來區分佈局的Measure、Layout、Executive的相對性能表現狀況。
打開
提高佈局性能的關鍵點是儘可能保持佈局層級的扁平化,避免出現重複的嵌套佈局。若是咱們寫的佈局層級比較深會嚴重增長CPU的負擔,形成性能的嚴重卡頓,關於Hierarchy Viewer的使用能夠參考:使用 Hierarchy Viewer 分析佈局。
在咱們優化過view的樹形結構和overdraw以後,可能仍是感受本身的app有卡頓和丟幀,或者滑動慢等問題,咱們就要查看一下是否存在內存抖動狀況了。所謂內存抖動,指的是內存頻繁建立和GC形成的UI線程被頻繁阻塞的現象。
Android有自動管理內存的機制,可是對內存的不恰當使用仍然容易引發嚴重的性能問題。在同一幀裏面建立過多的對象是件須要特別引發注意的事情,在同一幀裏建立大量對象可能引發GC的不停操做,執行GC操做的時候,全部線程的任何操做都會須要暫停,直到GC操做完成。大量不停的GC操做則會顯著佔用幀間隔時間。若是在幀間隔時間裏面作了過多的GC操做,那麼就會形成頁面卡頓。
在Android開發中,致使GC頻繁操做有兩個主要緣由:
Android的內存抖動可使用Android Studio的Profiler進行檢測。
而後,點擊record記錄內存信息,查找發生內存抖動位置,固然也可直接經過Jump to Source定位到代碼位置。
爲了不發生內存抖動,咱們須要避免在for循環裏面分配對象佔用內存,須要嘗試把對象的建立移到循環體以外,自定義View中的onDraw方法也須要引發注意,每次屏幕發生繪製以及動畫執行過程當中,onDraw方法都會被調用到,避免在onDraw方法裏面執行復雜的操做,避免建立對象。對於那些沒法避免須要建立對象的狀況,咱們能夠考慮對象池模型,經過對象池來解決頻繁建立與銷燬的問題,可是這裏須要注意結束使用以後,須要手動釋放對象池中的對象。
在前面Java基礎環節,咱們對Java的內存管理模型也作了基本的介紹,參考連接:Android 面試之必問Java基礎
在Java的內存模型中,將內存區域劃分爲方法區、堆、程序計數器、本地方法棧、虛擬機棧五個區域,以下圖。
方法區
堆
虛擬機棧
本地方法棧
程序計數器
標記清除算法
標記清除算法主要分爲有兩個階段,首先標記出須要回收的對象,而後咋標記完成後統一回收全部標記的對象;
缺點:
複製算法
將可用內存按空間分爲大小相同的兩小塊,每次只使用其中的一塊,等這塊內存使用完了將還存活的對象複製到另外一塊內存上,而後將這塊內存區域對象總體清除掉。每次對整個半區進行內存回收,不會致使碎片問題,實現簡單且效率高效。
缺點:
須要將內存縮小爲原來的一半,空間代價過高。
標記整理算法
標記整理算法標記過程和標記清除算法同樣,但清除過程並非對可回收對象直接清理,而是將全部存活對象像一端移動,而後集中清理到端邊界之外的內存。
分代回收算法
當代虛擬機垃圾回收算法都採用分代收集算法來收集,根據對象存活週期不一樣將內存劃分爲新生代和老年代,再根據每一個年代的特色採用最合適的回收算法。
所謂內存泄露,指的是內存中存在的沒有用的確沒法回收的對象。表現的現象是會致使內存抖動,可用內存減小,進而致使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分析工具解決。
在Android開發中,常常會遇到加載大圖致使內存泄露的問題,對於這種場景,有一個通用的解決方案,即便用ARTHook對不合理圖片進行檢測。咱們知道,獲取Bitmap佔用的內存主要有兩種方式:
*
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()); } });
方案一
在特定場景中獲取當前佔用內存大小,若是當前內存大小超過系統最大內存80%,對當前內存進行一次Dump(Debug.dumpHprofData()),選擇合適時間將hprof文件進行上傳,而後經過MAT工具手動分析該文件。
缺點:
方案二
將LeakCannary帶到線上,添加預設懷疑點,對懷疑點進行內存泄露監控,發現內存泄露回傳到服務端。
缺點:
改造主要涉及如下幾點:
完成的改造步驟以下:
App的網絡鏈接對於用戶來講, 影響不少, 且多數狀況下都很直觀, 直接影響用戶對這個App的使用體驗. 其中較爲重要的幾點:
流量 :App的流量消耗對用戶來講是比較敏感的, 畢竟流量是花錢的嘛. 如今大部分人的手機上都有安裝流量監控的工具App, 用來監控App的流量使用. 若是咱們的App這方面沒有控制好, 會給用戶很差的使用體驗。
電量 :電量相對於用戶來講, 沒有那麼明顯. 通常用戶可能不會太注意. 可是如電量優化中的那樣, 網絡鏈接(radio)是對電量影響很大的一個因素. 因此咱們也要加以注意。
用戶等待 :也就是用戶體驗, 良好的用戶體驗, 纔是咱們留住用戶的第一步. 若是App請求等待時間長, 會給用戶網絡卡, 應用反應慢的感受, 若是有對比, 有替代品, 咱們的App極可能就會被用戶無情拋棄。
網絡分析能夠藉助的工具備Monitor、代理工具等。
Android Studio內置的Monitor工具提供了一個Network Monitor,能夠幫助開發者進行網絡分析,下面是一個典型的Network Monitor示意圖。
Network Monitor實時跟蹤選定應用的數據請求狀況。 咱們能夠連上手機,選定調試應用進程, 而後在App上操做咱們須要分析的頁面請求。
網絡代理工具備兩個做用,一個是截獲網絡請求響應包, 分析網絡請求;另外一個設置代理網絡, 移動App開發中通常用來作不一樣網絡環境的測試, 例如Wifi/4G/3G/弱網等。
如今,可使用的代理工具備不少, 諸如Wireshark, Fiddler, Charles等。
對於網絡優化來講,主要從兩個方面進行着手進行優化:
基於上面的方案,能夠獲得如下一些常見的解決方案:
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,依據網絡狀況獲取不一樣分辨率的圖片
咱們使用淘寶或者京東的時候,會看到應用會根據網絡狀況,獲取不一樣分辨率的圖片,避免流量的浪費以及提高用戶的體驗。
適當的使用緩存, 不只可讓咱們的應用看起來更快, 也能避免一些沒必要要的流量消耗,帶來更好的用戶體驗。
1,打包網絡請求
當接口設計不能知足咱們的業務需求時。例如,可能一個界面須要請求多個接口,或是網絡良好,處於Wifi狀態下時咱們想獲取更多的數據等。這時就能夠打包一些網絡請求, 例如請求列表的同時, 獲取Header點擊率較高的的item項的詳情數據。
2,監聽設備狀態
爲了提高用戶體驗,咱們能夠對設備的使用狀態進行監聽,而後再結合JobScheduler來執行網絡請求.。比方說Splash閃屏廣告圖片, 咱們能夠在鏈接到Wifi時下載緩存到本地; 新聞類的App能夠在充電,Wifi狀態下作離線緩存。
1,弱網測試
有幾種方式來模擬弱網進行測試:
Android Emulator
一般,咱們建立和啓動Android模擬器能夠設置網絡速度和延遲,以下圖所示。
而後,咱們在啓動時使用的emulator命令以下。
$emulator -netdelay gprs -netspeed gsm -avd Nexus_5_API_22
2,網絡代理工具
使用網絡代理工具也能夠模擬網絡狀況。以Charles爲例,保持手機和PC處於同一個局域網, 在手機端wifi設置高級設置中設置代理方式爲手動, 代理ip填寫PC端ip地址, 端口號默認8888。
事實上,若是咱們的應用須要播放視頻、須要獲取 GPS 信息,亦或者是遊戲應用,耗電都是比較嚴重的。如何判斷哪些耗電是能夠避免,或者是須要去優化的呢?咱們能夠打開手機自帶的耗電排行榜,發現「王者榮耀」使用了 7 個多小時,這時用戶對「王者榮耀」的耗電是有預期的。
假設這個時候發現某個應用他根本沒怎麼使用,可是耗電卻很是多,那麼就會被系統無情的殺掉。因此耗電優化的第一個方向是優化應用的後臺耗電。
知道了系統是如何計算耗電的,咱們也就能夠知道應用在後臺不該該作什麼,例如長時間獲取 WakeLock、WiFi 和藍牙的掃描等,以及後臺服務。爲何說耗電優化第一個方向就是優化應用後臺耗電,由於大部分廠商預裝項目要求最嚴格的正是應用後臺待機耗電。
固然前臺耗電咱們不會徹底無論,可是標準會放鬆不少。再來看看下面這張圖,若是系統對你的應用彈出這個對話框,可能對於微信來講,用戶還能夠忍受,可是對其餘大多數的應用來講,可能不少用戶就直接把你加入到後臺限制的名單中了。
耗電優化的第二個方向是符合系統的規則,讓系統認爲你耗電是正常的。
而 Android P 及以上版本是經過 Android Vitals 監控後臺耗電,因此咱們須要符合 Android Vitals 的規則,目前它的具體規則以下。
能夠看到,Android系統目前比較關心是後臺 Alarm 喚醒、後臺網絡、後臺 WiFi 掃描以及部分長時間 WakeLock 阻止系統後臺休眠,由於這些都有可能致使耗電問題。
Android Vitals 的幾個關於電量的監控方案與規則,能夠幫助咱們進行耗電監測。
在使用了一段時間以後,我發現它並非那麼好用。以 Alarm wakeup 爲例,Vitals 以每小時超過 10 次做爲規則。因爲這個規則沒法作修改,不少時候咱們可能但願針對不一樣的系統版本作更加細緻的區分。其次跟 Battery Historian 同樣,咱們只能拿到 wakeup 的標記的組件,拿不到申請的堆棧,也拿不到當時手機是否在充電、剩餘電量等信息。 下圖是wakeup拿到的信息。
對於網絡、WiFi scans 以及 WakeLock 也是如此。雖然 Vitals 幫助咱們縮小了排查的範圍,可是依然沒辦法確認問題的具體緣由。
前面說過,Android Vitals並非那麼好用,並且對於國內的應用來講其實也根本沒法使用。那咱們的耗電監控系統應該監控哪些內容,又應該如何作呢?首先,咱們看一下耗電監控具體應該怎麼作呢?
明確了咱們須要監控什麼以及具體的規則以後,接下來咱們來看一下電量監控的技術方案。這裏首先來看一下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 信息、應用先後臺時間等輔助信息上傳到後臺便可。
使用 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 只是提供了一系列的基礎類,在實際使用時開發者仍然須要修改大量的源碼。
如今市面上的App,小則幾十M,大則上百M。安裝包越小,下載時省流量,用戶好的體驗,下載更快,安裝更快。那麼對於安裝包,咱們能夠從哪些方面着手進行優化呢?
1,清理無用資源
在android打包過程當中,若是代碼有涉及資源和代碼的引用,那麼就會打包到App中,爲了防止將這些廢棄的代碼和資源打包到App中,咱們須要及時地清理這些無用的代碼和資源來減少App的體積。清理的方法是,依次點擊android Studio的【Refactor】->【Remove unused Resource】,以下圖所示。
2,使用Lint工具
Lint工具仍是頗有用的,它給咱們須要優化的點:
3,開啓shrinkResources去除無用資源
在build.gradle 裏面配置shrinkResources true,在打包的時候會自動清除掉無用的資源,但通過實驗發現打出的包並不會,而是會把部分無用資源用更小的東西代替掉。注意,這裏的「無用」是指調用圖片的全部父級函數最終是廢棄代碼,而shrinkResources true 只能去除沒有任何父函數調用的狀況。
android { buildTypes { release { shrinkResources true } } }
除此以外,大部分應用其實並不須要支持幾十種語言的國際化支持,還能夠刪除語言支持文件。
在android開發中,內置的圖片是不少的,這些圖片佔用了大量的體積,所以爲了縮小包的體積,咱們能夠對資源進行壓縮。經常使用的方法有:
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" } }
在前端開發中,動態加載資源能夠有效減少apk的體積。除此以外,只提供對主流架構的支持,好比arm,對於mips和x86架構能夠考慮不支持,這樣能夠大大減少APK的體積。
固然,除了上面提到的場景的優化場景外,Android App的優化還包括存儲優化、多線程優化以及奔潰處理等方面。