Android性能调优利器StrictMode
2019獨角獸企業重金招聘Python工程師標準>>>
作為Android開發,日常的開發工作中或多或少要接觸到性能問題,比如我的Android程序運行緩慢卡頓,并且常常出現ANR對話框等等問題。既然有性能問題,就需要進行性能優化。正所謂工欲善其事,必先利其器。一個好的工具,可以幫助我們發現并定位問題,進而有的放矢進行解決。本文主要介紹StrictMode 在Android 應用開發中的應用和一些問題。
?
什么是StrictMode
StrictMode意思為嚴格模式,是用來檢測程序中違例情況的開發者工具。最常用的場景就是檢測主線程中本地磁盤和網絡讀寫等耗時的操作。
嚴在哪里
既然叫做嚴格模式,那么又嚴格在哪些地方呢?
在Android中,主線程,也就是UI線程,除了負責處理UI相關的操作外,還可以執行文件讀取或者數據庫讀寫操作(從Android 4.0 開始,網絡操作禁止在主線程中執行,否則會拋出NetworkOnMainThreadException)。使用嚴格模式,系統檢測出主線程違例的情況會做出相應的反應,如日志打印,彈出對話框亦或者崩潰等。換言之,嚴格模式會將應用的違例細節暴露給開發者方便優化與改善。
具體能檢測什么
嚴格模式主要檢測兩大問題,一個是線程策略,即TreadPolicy,另一個是VM策略,即VmPolicy。
ThreadPolicy
線程策略檢測的內容有
- 自定義的耗時調用 使用detectCustomSlowCalls()開啟
- 磁盤讀取操作 使用detectDiskReads()開啟
- 磁盤寫入操作 使用detectDiskWrites()開啟
- 網絡操作 使用detectNetwork()開啟
VmPolicy
虛擬機策略檢測的內容有
- Activity泄露 使用detectActivityLeaks()開啟
- 未關閉的Closable對象泄露 使用detectLeakedClosableObjects()開啟
- 泄露的Sqlite對象 使用detectLeakedSqlLiteObjects()開啟
- 檢測實例數量 使用setClassInstanceLimit()開啟
工作原理
其實StrictMode實現原理也比較簡單,以IO操作為例,主要是通過在open,read,write,close時進行監控。libcore.io.BlockGuardOs文件就是監控的地方。以open為例,如下進行監控。
?
| 1 2 3 4 5 6 7 8 | @Override public FileDescriptor open(String path, int flags, int mode) throws ErrnoException { ? BlockGuard.getThreadPolicy().onReadFromDisk(); ? ? if ((mode & O_ACCMODE) != O_RDONLY) { ? ? ? BlockGuard.getThreadPolicy().onWriteToDisk(); ? ? } ? ? return os.open(path, flags, mode); } |
其中onReadFromDisk()方法的實現,代碼位于StrictMode.java中。
?
| 1 2 3 4 5 6 7 8 9 10 11 | public void onReadFromDisk() { ? ? if ((mPolicyMask & DETECT_DISK_READ) == 0) { ? ? ? return; ? ? } ? ? if (tooManyViolationsThisLoop()) { ? ? ? return; ? ? } ? ? BlockGuard.BlockGuardPolicyException e = new StrictModeDiskReadViolation(mPolicyMask); ? ? e.fillInStackTrace(); ? ? startHandlingViolationException(e); } |
如何使用
關于StrictMode如何使用,最重要的就是如何啟用嚴格模式。
放在哪里
嚴格模式的開啟可以放在Application或者Activity以及其他組件的onCreate方法。為了更好地分析應用中的問題,建議放在Application的onCreate方法中。
簡單啟用
以下的代碼啟用全部的ThreadPolicy和VmPolicy違例檢測
?
| 1 2 3 4 | if (IS_DEBUG && Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) { ? ? StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog().build()); ? StrictMode.setVmPolicy(new VmPolicy.Builder().detectAll().penaltyLog().build()); } |
嚴格模式需要在debug模式開啟,不要在release版本中啟用。
同時,嚴格模式自API 9 開始引入,某些API方法也從 API 11 引入。使用時應該注意 API 級別。
如有需要,也可以開啟部分的嚴格模式。
查看結果
嚴格模式有很多種報告違例的形式,但是想要分析具體違例情況,還是需要查看日志,終端下過濾StrictMode就能得到違例的具體stacktrace信息。
?
| 1 | adb logcat | grep StrictMode |
解決違例
- 如果是主線程中出現文件讀寫違例,建議使用工作線程(必要時結合Handler)完成。
- 如果是對SharedPreferences寫入操作,在API 9 以上 建議優先調用apply而非commit。
- 如果是存在未關閉的Closable對象,根據對應的stacktrace進行關閉。
- 如果是SQLite對象泄露,根據對應的stacktrace進行釋放。
舉個例子
以主線程中的文件寫入為例,引起違例警告的代碼
?
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 | public void writeToExternalStorage() { ? ? File externalStorage = Environment.getExternalStorageDirectory(); ? ? File destFile = new File(externalStorage, "dest.txt"); ? ? try { ? ? ? OutputStream output = new FileOutputStream(destFile, true); ? ? ? ? output.write("droidyue.com".getBytes()); ? ? ? ? output.flush(); ? ? ? ? output.close(); ? ? } catch (FileNotFoundException e) { ? ? ? ? ? e.printStackTrace(); ? ? } catch (IOException e) { ? ? ? e.printStackTrace(); ? ? } } |
引起的警告為
?
| 1 2 3 4 5 6 7 8 | D/StrictMode( 9730): StrictMode policy violation; ~duration=20 ms: android.os.StrictMode$StrictModeDiskReadViolation: policy=31 violation=2 D/StrictMode( 9730):? ? at android.os.StrictMode$AndroidBlockGuardPolicy.onReadFromDisk(StrictMode.java:1176) D/StrictMode( 9730):? ? at libcore.io.BlockGuardOs.open(BlockGuardOs.java:106) D/StrictMode( 9730):? ? at libcore.io.IoBridge.open(IoBridge.java:390) D/StrictMode( 9730):? ? at java.io.FileOutputStream.<init>(FileOutputStream.java:88) D/StrictMode( 9730):? ? at com.example.strictmodedemo.MainActivity.writeToExternalStorage(MainActivity.java:56) D/StrictMode( 9730):? ? at com.example.strictmodedemo.MainActivity.onCreate(MainActivity.java:30) D/StrictMode( 9730):? ? at android.app.Activity.performCreate(Activity.java:4543) |
因為上述屬于主線程中的IO違例,解決方法就是講寫入操作放入工作線程。
?
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | public void writeToExternalStorage() { ? ? new Thread() { ? ? ? @Override ? ? ? public void run() { ? ? ? ? ? super.run(); ? ? ? ? ? File externalStorage = Environment.getExternalStorageDirectory(); ? ? ? ? ? File destFile = new File(externalStorage, "dest.txt"); ? ? ? ? ? try { ? ? ? ? ? ? ? OutputStream output = new FileOutputStream(destFile, true); ? ? ? ? ? ? ? output.write("droidyue.com".getBytes()); ? ? ? ? ? ? ? output.flush(); ? ? ? ? ? ? ? output.close(); ? ? ? ? ? } catch (FileNotFoundException e) { ? ? ? ? ? ? ? e.printStackTrace(); ? ? ? ? ? } catch (IOException e) { ? ? ? ? ? ? ? e.printStackTrace(); ? ? ? ? ? } ? ? ? } ? ? ? }.start(); } |
然而這并非完善,因為OutputStream.write方法可能拋出IOException,導致存在OutputStream對象未關閉的情況,仍然需要改進避免出現Closable對象未關閉的違例。改進如下
?
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | public void writeToExternalStorage() { ? ? new Thread() { ? ? ? @Override ? ? ? ? public void run() { ? ? ? ? ? super.run(); ? ? ? ? ? ? File externalStorage = Environment.getExternalStorageDirectory(); ? ? ? ? ? ? File destFile = new File(externalStorage, "dest.txt"); ? ? ? ? ? ? OutputStream output = null; ? ? ? ? ? ? try { ? ? ? ? ? ? ? ? output = new FileOutputStream(destFile, true); ? ? ? ? ? ? ? ? output.write("droidyue.com".getBytes()); ? ? ? ? ? ? ? ? output.flush(); ? ? ? ? ? ? ? ? output.close(); ? ? ? ? ? ? } catch (FileNotFoundException e) { ? ? ? ? ? ? ? ? e.printStackTrace(); ? ? ? ? ? ? } catch (IOException e) { ? ? ? ? ? ? ? ? e.printStackTrace(); ? ? ? ? ? ? } finally { ? ? ? ? ? ? ? ? if (null != output) { ? ? ? ? ? ? ? ? ? ? try { ? ? ? ? ? ? ? ? ? ? ? output.close(); ? ? ? ? ? ? ? ? ? ? } catch (IOException e) { ? ? ? ? ? ? ? ? ? ? ? ? e.printStackTrace(); ? ? ? ? ? ? ? ? ? ? } ? ? ? ? ? ? ? ? } ? ? ? ? ? ? } ? ? ? ? } ? ? }.start(); } |
檢測內存泄露
通常情況下,檢測內存泄露,我們需要使用MAT對heap dump 文件進行分析,這種操作不困難,但也不容易。使用嚴格模式,只需要過濾日志就能發現內存泄露。
這里以Activity為例說明,首先我們需要開啟對檢測Activity泄露的違例檢測。使用上面的detectAll或者detectActivityLeaks()均可。其次寫一段能夠產生Activity泄露的代碼。
?
| 1 2 3 4 5 6 7 | public class LeakyActivity extends Activity{ ? ? @Override ? ? protected void onCreate(Bundle savedInstanceState) { ? ? ? ? super.onCreate(savedInstanceState); ? ? ? ? MyApplication.sLeakyActivities.add(this); ? ? } } |
MyApplication中關于sLeakyActivities的部分實現
?
| 1 2 3 4 5 | public class MyApplication extends Application { ? public static final boolean IS_DEBUG = true; ? ? public static ArrayList<Activity> sLeakyActivities = new ArrayList<Activity>(); ? } |
當我們反復進入LeakyActivity再退出,過濾StrictMode就會得到這樣的日志
?
| 1 2 3 | E/StrictMode( 2622): class com.example.strictmodedemo.LeakyActivity; instances=2; limit=1 E/StrictMode( 2622): android.os.StrictMode$InstanceCountViolation: class com.example.strictmodedemo.LeakyActivity; instances=2; limit=1 E/StrictMode( 2622):? ? at android.os.StrictMode.setClassInstanceLimit(StrictMode.java:1) |
分析日志,LeakyActivity本應該是只存在一份實例,但現在出現了2個,說明LeakyActivity發生了內存泄露。
嚴格模式除了可以檢測Activity的內存泄露之外,還能自定義檢測類的實例泄露。從API 11 開始,系統提供的這個方法可以實現我們的需求。
?
| 1 | public StrictMode.VmPolicy.Builder setClassInstanceLimit (Class klass, int instanceLimit) |
舉個栗子,比如一個瀏覽器中只允許存在一個SearchBox實例,我們就可以這樣設置已檢測SearchBox實例的泄露
?
| 1 | StrictMode.setVmPolicy(new VmPolicy.Builder().setClassInstanceLimit(SearchBox.class, 1).penaltyLog().build()); |
noteSlowCall
StrictMode從 API 11開始允許開發者自定義一些耗時調用違例,這種自定義適用于自定義的任務執行類中,比如我們有一個進行任務處理的類,為TaskExecutor。
?
| 1 2 3 4 5 | public class TaskExecutor { ? ? public void execute(Runnable task) { ? ? ? ? task.run(); ? ? } } |
先需要跟蹤每個任務的耗時情況,如果大于500毫秒需要提示給開發者,noteSlowCall就可以實現這個功能,如下修改代碼
?
| 1 2 3 4 5 6 7 8 9 10 11 12 | public class TaskExecutor { ? ? ? private static long SLOW_CALL_THRESHOLD = 500; ? ? public void executeTask(Runnable task) { ? ? ? ? long startTime = SystemClock.uptimeMillis(); ? ? ? ? task.run(); ? ? ? ? long cost = SystemClock.uptimeMillis() - startTime; ? ? ? ? if (cost > SLOW_CALL_THRESHOLD) { ? ? ? ? ? ? StrictMode.noteSlowCall("slowCall cost=" + cost); ? ? ? ? } ? ? } } |
執行一個耗時2000毫秒的任務
?
| 1 2 3 4 5 6 7 8 9 10 11 | TaskExecutor executor = new TaskExecutor(); executor.executeTask(new Runnable() { ? @Override ? ? public void run() { ? ? ? ? try { ? ? ? ? ? Thread.sleep(2000); ? ? ? ? } catch (InterruptedException e) { ? ? ? ? ? ? e.printStackTrace(); ? ? ? ? } ? ? } }); |
得到的違例日志,注意其中~duration=20 ms并非耗時任務的執行時間,而我們的自定義信息msg=slowCall cost=2000才包含了真正的耗時。
?
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | D/StrictMode(23890): StrictMode policy violation; ~duration=20 ms: android.os.StrictMode$StrictModeCustomViolation: policy=31 violation=8 msg=slowCall cost=2000 D/StrictMode(23890):? ? at android.os.StrictMode$AndroidBlockGuardPolicy.onCustomSlowCall(StrictMode.java:1163) D/StrictMode(23890):? ? at android.os.StrictMode.noteSlowCall(StrictMode.java:1974) D/StrictMode(23890):? ? at com.example.strictmodedemo.TaskExecutor.executeTask(TaskExecutor.java:17) D/StrictMode(23890):? ? at com.example.strictmodedemo.MainActivity.onCreate(MainActivity.java:36) D/StrictMode(23890):? ? at android.app.Activity.performCreate(Activity.java:4543) D/StrictMode(23890):? ? at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1071) D/StrictMode(23890):? ? at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2158) D/StrictMode(23890):? ? at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2237) D/StrictMode(23890):? ? at android.app.ActivityThread.access$600(ActivityThread.java:139) D/StrictMode(23890):? ? at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1262) D/StrictMode(23890):? ? at android.os.Handler.dispatchMessage(Handler.java:99) D/StrictMode(23890):? ? at android.os.Looper.loop(Looper.java:156) D/StrictMode(23890):? ? at android.app.ActivityThread.main(ActivityThread.java:5005) D/StrictMode(23890):? ? at java.lang.reflect.Method.invokeNative(Native Method) D/StrictMode(23890):? ? at java.lang.reflect.Method.invoke(Method.java:511) D/StrictMode(23890):? ? at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:784) D/StrictMode(23890):? ? at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:551) D/StrictMode(23890):? ? at dalvik.system.NativeStart.main(Native Method) |
其他技巧
除了通過日志查看之外,我們也可以在開發者選項中開啟嚴格模式,開啟之后,如果主線程中有執行時間長的操作,屏幕則會閃爍,這是一個更加直接的方法。
?
問題來了
日志的時間靠譜么
在下面的過濾日志中,我們看到下面的一個IO操作要消耗31毫秒,這是真的么
?
| 1 2 3 4 5 6 7 8 9 10 11 | D/StrictMode( 2921): StrictMode policy violation; ~duration=31 ms: android.os.StrictMode$StrictModeDiskReadViolation: policy=31 violation=2 D/StrictMode( 2921):? ? at android.os.StrictMode$AndroidBlockGuardPolicy.onReadFromDisk(StrictMode.java:1176) D/StrictMode( 2921):? ? at libcore.io.BlockGuardOs.read(BlockGuardOs.java:148) D/StrictMode( 2921):? ? at libcore.io.IoBridge.read(IoBridge.java:422) D/StrictMode( 2921):? ? at java.io.FileInputStream.read(FileInputStream.java:179) D/StrictMode( 2921):? ? at java.io.InputStreamReader.read(InputStreamReader.java:244) D/StrictMode( 2921):? ? at java.io.BufferedReader.fillBuf(BufferedReader.java:130) D/StrictMode( 2921):? ? at java.io.BufferedReader.readLine(BufferedReader.java:354) D/StrictMode( 2921):? ? at com.example.strictmodedemo.MainActivity.testReadContentOfFile(MainActivity.java:65) D/StrictMode( 2921):? ? at com.example.strictmodedemo.MainActivity.onCreate(MainActivity.java:28) D/StrictMode( 2921):? ? at android.app.Activity.performCreate(Activity.java:4543) |
從上面的stacktrace可以看出testReadContentOfFile方法中包含了文件讀取IO操作,至于是否為31毫秒,我們可以利用秒表的原理計算一下,即在方法調用的地方如下記錄
?
| 1 2 3 4 | long startTime = System.currentTimeMillis(); testReadContentOfFile(); long cost = System.currentTimeMillis() - startTime; Log.d(LOGTAG, "cost = " + cost); |
得到的日志中上述操作耗時9毫秒,非31毫秒。
?
| 1 | D/MainActivity(20996): cost = 9 |
注:通常情況下StrictMode給出的耗時相對實際情況偏高,并不是真正的耗時數據。
注意
- 在線上環境即Release版本不建議開啟嚴格模式。
- 嚴格模式無法監控JNI中的磁盤IO和網絡請求。
- 應用中并非需要解決全部的違例情況,比如有些IO操作必須在主線程中進行。
轉載于:https://my.oschina.net/u/3026396/blog/831710
總結
以上是生活随笔為你收集整理的Android性能调优利器StrictMode的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: NLog在asp.net core中的应
- 下一篇: Android优化系列之apk瘦身