Android嚴苛模式StrictMode使用詳解


StrictMode類是Android 2.3 (API 9)引入的一個工具類,可以用來幫助開發者發現代碼中的一些不規范的問題,以達到提升應用響應能力的目的。舉個例子來說,如果開發者在UI線程中進行了網絡操作或者文件系統的操作,而這些緩慢的操作會嚴重影響應用的響應能力,甚至出現ANR對話框。為了在開發中發現這些容易忽略的問題,我們使用StrictMode,系統檢測出主線程違例的情況並做出相應的反應,最終幫助開發者優化和改善代碼邏輯。

       官網文檔:http://developer.android.com/reference/android/os/StrictMode.html

StrictMode具體能檢測什么

嚴苛模式主要檢測兩大問題,一個是線程策略,即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為例,如下進行監控。

@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中。

public void onReadFromDisk() { if ((mPolicyMask & DETECT_DISK_READ) == 0) { return; } if (tooManyViolationsThisLoop()) { return; } BlockGuard.BlockGuardPolicyException e = new StrictModeDiskReadViolation(mPolicyMask); e.fillInStackTrace(); startHandlingViolationException(e); }

常見用法

       嚴格模式的開啟可以放在Application或者Activity以及其他組件的onCreate方法。為了更好地分析應用中的問題,建議放在Application的onCreate方法中。 
       其中,我們只需要在app的開發版本下使用 StrictMode,線上版本避免使用 StrictMode,這里定義了一個布爾值變量DEV_MODE來進行控制。

private boolean DEV_MODE = true; public void onCreate() { if (DEV_MODE) { StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() .detectCustomSlowCalls() //API等級11,使用StrictMode.noteSlowCode .detectDiskReads() .detectDiskWrites() .detectNetwork() // or .detectAll() for all detectable problems .penaltyDialog() //彈出違規提示對話框 .penaltyLog() //在Logcat 中打印違規異常信息 .penaltyFlashScreen() //API等級11 .build()); StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() .detectLeakedSqlLiteObjects() .detectLeakedClosableObjects() //API等級11 .penaltyLog() .penaltyDeath() .build()); } super.onCreate(); }

       其中Android3.0引入的方法包括detectCustomSlowCalls()和noteSlowCode(),它們都是用來檢測應用中執行緩慢代碼的或者潛在的緩慢代碼。

查看報告結果

       嚴格模式有很多種報告違例的形式,但是想要分析具體違例情況,還是需要查看日志,終端下過濾StrictMode就能得到違例的具體stacktrace信息。

adb logcat | grep StrictMode

這里寫圖片描述

當然也可以選擇彈窗形式來簡明提醒開發者

彈窗警告

ThreadPolicy 詳解

StrictMode.ThreadPolicy.Builder 主要方法如下

  • detectNetwork() 用於檢查UI線程中是否有網絡請求操作

檢測UI線程中網絡請求案例:

public class MainActivity extends AppCompatActivity implements View.OnClickListener { Button btnTest; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() .detectNetwork() .penaltyLog() .build()); btnTest = (Button) findViewById(R.id.btn_test); btnTest.setOnClickListener(this); } @Override public void onClick(View v) { int id = v.getId(); switch (id) { case R.id.btn_test: postNetwork(); break; } } /** * 網絡連接的操作 */ private void postNetwork() { try { URL url = new URL("http://www.wooyun.org"); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.connect(); BufferedReader reader = new BufferedReader(new InputStreamReader( conn.getInputStream())); String lines = null; StringBuffer sb = new StringBuffer(); while ((lines = reader.readLine()) != null) { sb.append(lines); } } catch (Exception e) { e.printStackTrace(); } } }

運行后,觸發的警告如下

這里寫圖片描述

  • detectDiskReads() 和 detectDiskWrites() 是磁盤讀寫檢查

磁盤讀寫檢查案例:
public class MainActivity extends AppCompatActivity implements View.OnClickListener { Button btnTest; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() .detectDiskWrites() .detectDiskReads() .penaltyLog() .build()); btnTest = (Button) findViewById(R.id.btn_test); btnTest.setOnClickListener(this); } @Override public void onClick(View v) { int id = v.getId(); switch (id) { case R.id.btn_test: writeToExternalStorage(); break; } } /** * 文件系統的操作 */ public void writeToExternalStorage() { File externalStorage = Environment.getExternalStorageDirectory(); File mbFile = new File(externalStorage, "castiel.txt"); try { OutputStream output = new FileOutputStream(mbFile, true); output.write("www.wooyun.org".getBytes()); output.flush(); output.close(); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }

運行后,觸發的警告如下 

這里寫圖片描述

  • noteSlowCall針對執行比較耗時的檢查


           StrictMode從 API 11開始允許開發者自定義一些耗時調用違例,這種自定義適用於自定義的任務執行類中,比如我們有一個進行任務處理的類,為TaskExecutor。
public class TaskExecutor { public void execute(Runnable task) { task.run(); } }

       先需要跟蹤每個任務的耗時情況,如果大於500毫秒需要提示給開發者,noteSlowCall就可以實現這個功能,如下修改代碼

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毫秒的任務

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才包含了真正的耗時。

  • penaltyDeath(),當觸發違規條件時,直接Crash掉當前應用程序。

  • penaltyDeathOnNetwork(),當觸發網絡違規時,Crash掉當前應用程序。

  • penaltyDialog(),觸發違規時,顯示對違規信息對話框。

  • penaltyFlashScreen(),會造成屏幕閃爍,不過一般的設備可能沒有這個功能。

  • penaltyDropBox(),將違規信息記錄到 dropbox 系統日志目錄中(/data/system/dropbox),你可以通過如下命令進行插件:

adb shell dumpsys dropbox dataappstrictmode --print
  • permitCustomSlowCalls()、permitDiskReads ()、permitDiskWrites()、permitNetwork: 如果你想關閉某一項檢測,可以使用對應的permit*方法。

VMPolicy 詳解

StrictMode.VmPolicy.Builder 主要方法如下

  • detectActivityLeaks() 用戶檢查 Activity 的內存泄露情況

內存泄露檢查案例:
public class MainActivity extends AppCompatActivity implements View.OnClickListener { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() .detectActivityLeaks() .penaltyLog() .build() ); new Thread() { @Override public void run() { while (true) { SystemClock.sleep(1000); } } }.start(); } }

我們反復旋轉屏幕就會輸出提示信息(重點在 instances=2; limit=1 這一行) 
這里寫圖片描述 
       這時因為,我們在Activity中創建了一個Thread匿名內部類,而匿名內部類隱式持有外部類的引用。而每次旋轉屏幕是,Android會新創建一個Activity,而原來的Activity實例又被我們啟動的匿名內部類線程持有,所以不會釋放,從日志上看,當先系統中該Activty有4個實例,而限制是只能創建1各實例。我們不斷翻轉屏幕,instances 的個數還會持續增加。

  • detectLeakedClosableObjects()用於資源沒有正確關閉時提醒

// 資源引用沒有關閉檢查案例 public class MainActivity extends AppCompatActivity implements View.OnClickListener { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() .detectLeakedClosableObjects() .penaltyLog() .build() ); File newxmlfile = new File(Environment.getExternalStorageDirectory(), "castiel.txt"); try { newxmlfile.createNewFile(); FileWriter fw = new FileWriter(newxmlfile); fw.write("猴子搬來的救兵WooYun"); //fw.close(); 我們在這里特意沒有關閉 fw } catch (IOException e) { e.printStackTrace(); } } }

 

運行后觸發警告如下 
這里寫圖片描述

  • detectLeakedSqlLiteObjects() 和 
    detectLeakedClosableObjects()的用法類似,只不過是用來檢查 SQLiteCursor 或者 其他 SQLite 
    對象是否被正確關閉

  • detectLeakedRegistrationObjects() 用來檢查 BroadcastReceiver 或者 
    ServiceConnection 注冊類對象是否被正確釋放

  • setClassInstanceLimit(),設置某個類的同時處於內存中的實例上限,可以協助檢查內存泄露

檢測內存泄露案例
public class MainActivity extends AppCompatActivity implements View.OnClickListener { private static class CastielClass{} private static List<CastielClass> classList; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); classList = new ArrayList<CastielClass>(); StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() .setClassInstanceLimit(CastielClass.class, 2) .penaltyLog() .build()); classList.add(new CastielClass()); classList.add(new CastielClass()); classList.add(new CastielClass()); classList.add(new CastielClass()); classList.add(new CastielClass()); classList.add(new CastielClass()); classList.add(new CastielClass()); classList.add(new CastielClass()); } }

運行后觸發警告如下

這里寫圖片描述

其他操作

       除了通過日志查看之外,我們也可以在開發者選項中開啟嚴格模式,開啟之后,如果主線程中有執行時間長的操作,屏幕則會閃爍,這是一個更加直接的方法。 
這里寫圖片描述

注意事項

  • 只在開發階段啟用StrictMode,發布應用或者release版本一定要禁用它。
  • 嚴格模式無法監控JNI中的磁盤IO和網絡請求。
  • 應用中並非需要解決全部的違例情況,比如有些IO操作必須在主線程中進行。

參考鏈接:http://droidyue.com/blog/2015/09/26/android-tuning-tool-strictmode/

 

 

 

安卓開發高級技術交流QQ群:108721298 歡迎入群

微信公眾號:mobilesafehome

(本公眾號支持投票)

Android安全技術大本營


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM