Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> StrictMode介紹

StrictMode介紹

編輯:關於Android編程

作為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為例,如下進行監控。

@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);
}

如何使用

關於StrictMode如何使用,最重要的就是如何啟用嚴格模式。

放在哪裡

嚴格模式的開啟可以放在Application或者Activity以及其他組件的onCreate方法。為了更好地分析應用中的問題,建議放在Application的onCreate方法中。

簡單啟用

以下的代碼啟用全部的ThreadPolicy和VmPolicy違例檢測

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信息。

adb logcat | grep StrictMode

解決違例

如果是主線程中出現文件讀寫違例,建議使用工作線程(必要時結合Handler)完成。如果是對SharedPreferences寫入操作,在API 9 以上 建議優先調用apply而非commit。如果是存在未關閉的Closable對象,根據對應的stacktrace進行關閉。如果是SQLite對象洩露,根據對應的stacktrace進行釋放。

舉個例子

以主線程中的文件寫入為例,引起違例警告的代碼

public void writeToExternalStorage() {
    File externalStorage = Environment.getExternalStorageDirectory();
    File destFile = new File(externalStorage, "dest.txt");
    try {
      OutputStream output = new FileOutputStream(destFile, true);
        output.write("robert.com".getBytes());
        output.flush();
        output.close();
    } catch (FileNotFoundException e) {
          e.printStackTrace();
    } catch (IOException e) {
      e.printStackTrace();
    }
}

引起的警告為

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.(FileOutputStream.java:88)
D/StrictMode( 9730):    at com.robert.strictmodedemo.MainActivity.writeToExternalStorage(MainActivity.java:56)
D/StrictMode( 9730):    at com.robert.strictmodedemo.MainActivity.onCreate(MainActivity.java:30)
D/StrictMode( 9730):    at android.app.Activity.performCreate(Activity.java:4543)

因為上述屬於主線程中的IO違例,解決方法就是講寫入操作放入工作線程。

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("robert.com".getBytes());
              output.flush();
              output.close();
          } catch (FileNotFoundException e) {
              e.printStackTrace();
          } catch (IOException e) {
              e.printStackTrace();
          }
      }
      }.start();
}

然而這並非完善,因為OutputStream.write方法可能拋出IOException,導致存在OutputStream對象未關閉的情況,仍然需要改進避免出現Closable對象未關閉的違例。改進如下

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("robert.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洩露的代碼。

public class LeakyActivity extends Activity{
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        MyApplication.sLeakyActivities.add(this);
    }
}

MyApplication中關於sLeakyActivities的部分實現

public class MyApplication extends Application {
  public static final boolean IS_DEBUG = true;
    public static ArrayList sLeakyActivities = new ArrayList();

}

當我們反復進入LeakyActivity再退出,過濾StrictMode就會得到這樣的日志

E/StrictMode( 2622): class com.robert.strictmodedemo.LeakyActivity; instances=2; limit=1
E/StrictMode( 2622): android.os.StrictMode$InstanceCountViolation: class com.robert.strictmodedemo.LeakyActivity; instances=2; limit=1
E/StrictMode( 2622):    at android.os.StrictMode.setClassInstanceLimit(StrictMode.java:1)

分析日志,LeakyActivity本應該是只存在一份實例,但現在出現了2個,說明LeakyActivity發生了內存洩露。

嚴格模式除了可以檢測Activity的內存洩露之外,還能自定義檢測類的實例洩露。從API 11 開始,系統提供的這個方法可以實現我們的需求。

public StrictMode.VmPolicy.Builder setClassInstanceLimit (Class klass, int instanceLimit)

舉個栗子,比如一個浏覽器中只允許存在一個SearchBox實例,我們就可以這樣設置已檢測SearchBox實例的洩露

StrictMode.setVmPolicy(new VmPolicy.Builder().setClassInstanceLimit(SearchBox.class, 1).penaltyLog().build());

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

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.robert.strictmodedemo.TaskExecutor.executeTask(TaskExecutor.java:17)
D/StrictMode(23890):    at com.robert.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毫秒,這是真的麼

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.robert.strictmodedemo.MainActivity.testReadContentOfFile(MainActivity.java:65)
D/StrictMode( 2921):    at com.robert.strictmodedemo.MainActivity.onCreate(MainActivity.java:28)
D/StrictMode( 2921):    at android.app.Activity.performCreate(Activity.java:4543)

從上面的stacktrace可以看出testReadContentOfFile方法中包含了文件讀取IO操作,至於是否為31毫秒,我們可以利用秒表的原理計算一下,即在方法調用的地方如下記錄

long startTime = System.currentTimeMillis();
testReadContentOfFile();
long cost = System.currentTimeMillis() - startTime;
Log.d(LOGTAG, "cost = " + cost);

得到的日志中上述操作耗時9毫秒,非31毫秒。

D/MainActivity(20996): cost = 9

注:通常情況下StrictMode給出的耗時相對實際情況偏高,並不是真正的耗時數據。

注意

在線上環境即Release版本不建議開啟嚴格模式。嚴格模式無法監控JNI中的磁盤IO和網絡請求。應用中並非需要解決全部的違例情況,比如有些IO操作必須在主線程中進行。

如果不指定檢測函數,也可以用detectAll()來替代。penaltyLog()表示將警告輸出到LogCat,你也可以使用其他或增加新的懲罰(penalty)函數,例如使用penaltyDeath()的話,一旦StrictMode消息被寫到LogCat後應用就會崩潰。

在正式版本中,我們並不希望使用StrictMode來讓用戶的應用因為一個警告而崩潰,所以在應用正式發布時,需要移出這些監視。你可以通過刪除代碼來實現,不過這裡提供一個更好的方式來解決這個問題,即使用AndroidMainifest文件中的debuggable屬性來實現,代碼如下所示:

 

android:debuggable=true

 

在代碼中,使用方法如下所示:

 

// Return if this application is not in debug mode 
ApplicationInfo appInfo = context.getApplicationInfo(); 
int appFlags = appInfo.flags; 
if ((appFlags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) { 
    // Do StrictMode setup here 
    StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() 
        .detectLeakedSqlLiteObjects() 
        .penaltyLog() 
        .penaltyDeath() 
        .build()); 
}
   
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved