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操做必须在主线程中进行。
相关文章
相关标签/搜索