Android性能調優利器StrictMode

做爲Android開發,平常的開發工做中或多或少要接觸到性能問題,好比個人Android程序運行緩慢卡頓,而且經常出現ANR對話框等等問題。既然有性能問題,就須要進行性能優化。正所謂工欲善其事,必先利其器。一個好的工具,能夠幫助咱們發現並定位問題,進而有的放矢進行解決。本文主要介紹StrictMode 在Android 應用開發中的應用和一些問題。java

 

什麼是StrictModeandroid

StrictMode意思爲嚴格模式,是用來檢測程序中違例狀況的開發者工具。最經常使用的場景就是檢測主線程中本地磁盤和網絡讀寫等耗時的操做。數據庫

嚴在哪裏瀏覽器

既然叫作嚴格模式,那麼又嚴格在哪些地方呢?性能優化

在Android中,主線程,也就是UI線程,除了負責處理UI相關的操做外,還能夠執行文件讀取或者數據庫讀寫操做(從Android 4.0 開始,網絡操做禁止在主線程中執行,不然會拋出NetworkOnMainThreadException)。使用嚴格模式,系統檢測出主線程違例的狀況會作出相應的反應,如日誌打印,彈出對話框亦或者崩潰等。換言之,嚴格模式會將應用的違例細節暴露給開發者方便優化與改善。網絡

具體能檢測什麼app

嚴格模式主要檢測兩大問題,一個是線程策略,即TreadPolicy,另外一個是VM策略,即VmPolicy。ide

ThreadPolicy工具

線程策略檢測的內容有oop

  • 自定義的耗時調用 使用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操做必須在主線程中進行。
相關文章
相關標籤/搜索