Android面試之效能優化
前言
本文是為了面試而寫的效能優化。目的不是為了具體的深入而是對於要面試的同學在面試的時候能和麵試官說出的效能優化的方面。在面試的時候基本現在每個面試官都會問一些關於效能優化方法的問題。那麼該怎麼回答呢?面試不同於我們學習新的知識點,要完全學會,要學精,對於面試官這個問題,可以從下面幾個方面來回答,ANR,記憶體溢位,記憶體抖動,記憶體洩漏,UI卡頓,冷啟動優化等方面來回答。
ANR
ANR(Applicatino not responding)是指程式無響應,主要原因為:
- 主執行緒被io操作阻塞(4.0後網路io不允許主執行緒中)。
- 主執行緒做了耗時任務超過 5秒。
- Service做了耗時操作超過20秒,這是由於service預設執行在主執行緒,可以使用IntentService 。
- BroadcastReceiver的onReciver做了耗時操作超過10秒。
解決方式:
- 開一個子執行緒,使用Handler來處理。
- 使用AsyncTask來處理耗時任務。
記憶體溢位
記憶體溢位主要是由於載入大的圖片引起的。解決方式:
- 及時釋放bitmap,呼叫.recycler(Bitmap會佔用java記憶體和c(native)記憶體,java記憶體會自動釋放,c記憶體需要手動釋放)。
- 使用lru 最近最少使用 LruCache來儲存物件put(key,value),使用的使用LinkHashMap()。
- 計算inSampleSize 官方提供的方法,使用BitmapFactory.Options來計算inSampleSize(圖片的縮略比)
- 縮圖 使用Options的inJustDecodeBounds屬性來處理載入縮圖
- 三級快取 記憶體,本地,網路。
記憶體抖動
記憶體抖動是指記憶體在短時間內頻繁地分配和回收,而頻繁的gc會導致卡頓,嚴重時和記憶體洩漏一樣會導致OOM。
常見的記憶體抖動場景:
- 迴圈中建立大量臨時物件;
- onDraw中建立Paint或Bitmap物件等;
記憶體抖動的原因: 瞬間產生大量的物件會嚴重佔用新生代的記憶體區域,當達到閥值,剩餘空間不夠的時候,就會觸發GC。系統花費在GC上的時間越多,進行介面繪製或流音訊處理的時間就越短。即使每次分配的物件佔用了很少的記憶體,但是他們疊加在一起會增加Heap的壓力,從而觸發更多其他型別的GC。這個操作有可能會影響到幀率,並使得使用者感知到效能問題。
記憶體洩漏
記憶體洩漏是指無用物件(不在使用的物件)持續佔有記憶體或無用物件的記憶體得不到及時釋放,從而造成的記憶體空間的浪費稱為記憶體洩漏。
Android記憶體洩漏:
- 單例導致記憶體洩漏
public class SingleInstanceTest {
private static SingleInstanceTest sInstance;
private Context mContext;
private SingleInstanceTest(Context context){
this.mContext = context;
}
public static SingleInstanceTest newInstance(Context context){
if(sInstance == null){
sInstance = new SingleInstanceTest(context);
} return sInstance;
}
}
上面是一個比較簡單的單例模式用法,需要外部傳入一個 Context 來獲取該類的例項,如果此時傳入的 Context 是 Activity 的話,此時單例就有持有該 Activity 的強引用(直到整個應用生命週期結束)。這樣的話,即使該 Activity 退出,該 Activity 的記憶體也不會被回收,這樣就造成了記憶體洩露,特別是一些比較大的 Activity,甚至還會導致 OOM(Out Of Memory)。
解決方式:
public class SingleInstanceTest {
private static SingleInstanceTest sInstance;
private Context mContext;
private SingleInstanceTest(Context context){
his.mContext = context.getApplicationContext();
}
public static SingleInstanceTest newInstance(Context context){
if(sInstance == null){
sInstance = new SingleInstanceTest(context);
} return sInstance;
}
}
可以看到在 SingleInstanceTest 的建構函式中,將 context.getApplicationContext() 賦值給 mContext,此時單例引用的物件是 Application,而 Application 的生命週期本來就跟應用程式是一樣的,也就不存在記憶體洩露。
2.內部類導致記憶體洩漏 非靜態內部類會預設持有外部類的引用。會導致內部類的生命週期過長。 正確的做法就是修改成靜態內部類。
3.Handler 看下面的程式碼
public class HandlerActivity extends AppCompatActivity {
private final static int MESSAGECODE = 1 ;
private final Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
Log.d("mmmmmmmm" , "handler " + msg.what ) ;
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler);
//點選結束Activity
findViewById( R.id.finish ).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
//新建執行緒,內部類
new Thread(new Runnable() {
@Override
public void run() {
handler.sendEmptyMessage( MESSAGECODE ) ;
try {
Thread.sleep( 8000 );
} catch (InterruptedException e) {
e.printStackTrace();
}
//持有物件的引用
handler.sendEmptyMessage( MESSAGECODE ) ;
}
}).start() ;
}
}
這段程式碼執行起來後,立即點選 finish 按鈕,通過檢測,發現 HandlerActivity 出現了記憶體洩漏。
當Activity finish後,延時訊息會繼續存在主執行緒訊息佇列中8秒鐘,然後處理訊息。而該訊息引用了Activity的Handler物件,然後這個Handler又引用了這個Activity。這些引用物件會保持到該訊息被處理完,這樣就導致該Activity物件無法被回收,從而導致了上面說的 Activity洩露。
Handler 是個很常用也很有用的類,非同步,執行緒安全等等。如果有下面這樣的程式碼,會發生什麼呢? handler.postDeslayed ,假設 delay 時間是幾個小時… 這意味著什麼?意味著只要 handler 的訊息還沒有被處理結束,它就一直存活著,包含它的 Activity 就跟著活著。
我們來想辦法修復它,修復的方案是 WeakReference ,也就是所謂的弱引用。垃圾回收器在回收的時候,是會忽視掉弱引用的,所以包含它的 Activity 會被正常清理掉。
解決方式 1.靜態內部類 2.弱引用 3.注意在onDestroy中移除訊息
public class HandlerActivity extends AppCompatActivity {
private final static int MESSAGECODE = 1 ;
private static Handler handler ;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler);
findViewById( R.id.finish ).setOnClickListener(new View.OnClickListener()
@Override
public void onClick(View v) {
finish();
}
});
//建立Handler
handler = new MyHandler( this ) ;
//建立執行緒並且啟動執行緒
new Thread( new MyRunnable() ).start();
}
private static class MyHandler extends Handler {
WeakReference<HandlerActivity> weakReference ;
public MyHandler(HandlerActivity activity ){
weakReference = new WeakReference<HandlerActivity>( activity) ;
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
if ( weakReference.get() != null ){
// update android ui
Log.d("mmmmmmmm" , "handler " + msg.what ) ;
}
}
}
private static class MyRunnable implements Runnable {
@Override
public void run() {
handler.sendEmptyMessage( MESSAGECODE ) ;
try {
Thread.sleep( 8000 );
} catch (InterruptedException e) {
e.printStackTrace();
}
handler.sendEmptyMessage( MESSAGECODE ) ;
}
}
}
UI卡頓
- 在UI執行緒中做輕微耗時操作,會導致UI執行緒卡頓
- 佈局Layout過於複雜,無法再16ms內完成渲染 60fps–>16ms 60ms一幀 每過16ms就會更新一下ui,要達到60ms一幀,否則可能會卡頓
- 同一時間動畫執行的次數過多,導致cpu或gpu負載過重。
- View過度繪製,導致某些畫素在同一時間內被繪製多次,從而導致cpu,gpu負載過重。 overdraw 過度繪製,
- view頻繁的觸發measure。layout,導致measure。layout累計耗時過多以及整個view頻繁的重新渲染
- 記憶體頻繁觸發Gc過多,導致展示阻塞渲染操作
- 屯餘資源及邏輯導致載入和執行緩慢
解決ui卡頓: 1.佈局優化 include,merge,viewsuble 2.背景和圖片等記憶體分配優化
記憶體優化
記憶體管理
- 分配機制 為每一個程序分配一個小額的記憶體,然後根據需要分配更多記憶體。
- 回收機制 Android的目的是儘可能的執行多個程序,這樣可以讓使用者不用每次都重新開啟,而是恢復。當記憶體緊張時會按等級殺死程序。前臺程序>可見程序>服務程序>後臺程序(lru)>空程序。
優化方法:
- 當Service完成任務後,儘量停止它。
- 在UI不可見的時候,釋放掉一些只有UI使用的資源
- 在系統記憶體緊張的時候,儘可能多的釋放掉非重要的資源。
- 避免濫用Bitmap導致的記憶體浪費。
- 儘量使用少的依賴注入框架
冷啟動的優化
冷啟動就是在啟動應用前,系統中沒有該應用的任何程序資訊。 熱啟動就是使用者使用返回鍵退出應用,然後馬上又重新啟動應用。
Application只初始化一次,冷啟動會先建立Application,然後初始化MainActivity,熱啟動會直接初始化MainActivity。
冷啟動流程:
- Zygote程序中fork建立一個新的程序。
- 建立和初始化Application類,建立MainActivity類
- inflate佈局,當onCreate/onStart/onResume方法都走完。
- 呼叫setContetView方法後,將view新增到DecorView中,呼叫view的measuer/layotu/draw顯示到介面上。
減少冷啟動的時間進行優化:
- 減少onCreate方法的工作量 第三方sdk的使用最好使用懶載入方式,當前有些困難
- 不用讓Application參與業務的操作。
- 不用再Application進行耗時操作。
- 不要以靜態變數的方式在Application中儲存資料。
- 減少佈局的深度
效能優化工具
android Studio 中 Android Monitor