在Android中,性能优化是我们持之不懈的工作。这其中,在主线程执行耗时的任务,可能会导致界面卡顿,甚至是ANR(程序未响应)。当然Android提供了很多优秀的工具,比如StrictMode,Method Tracing等,便于我们检测问题。
这里,本文将介绍一个更加简单有效的方法。相比StrictMode来说更加便于发现问题,相比Method Tracing来说更加容易操作。
首先,我们有这样一个程序代码
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
@Override
protected void onCreate ( Bundle savedInstanceState ) {
super . onCreate ( savedInstanceState );
setContentView ( R . layout . activity_main );
writeContentToFile ();
}
private void writeContentToFile () {
File log = new File ( Environment . getExternalStorageDirectory (), "Log.txt" );
Writer outWriter = null ;
try {
outWriter = new BufferedWriter ( new FileWriter ( log . getAbsolutePath (), false ));
outWriter . write ( new Date (). toString ());
outWriter . write ( " : \n" );
outWriter . flush ();
} catch ( Exception e ) {
e . printStackTrace ();
} finally {
if ( outWriter != null ) {
try {
outWriter . close ();
} catch ( IOException e ) {
e . printStackTrace ();
}
}
}
}
上面的代码需要优化,因为
writeContentToFile 是一个本地文件写操作,比较耗时
而writeContentToFile 这个方法却放在了主线程中,必然会阻塞主线程其他的工作顺利执行。
上面介绍StrictMode和Method Traing都可以检测这个问题,这里我们我们用一个更简单的方法
1
2
3
4
5
6
7
8
public void checkWorkerThread () {
boolean isMainThread = Looper . myLooper () == Looper . getMainLooper ();
if ( isMainThread ) {
if ( BuildConfig . DEBUG ) {
throw new RuntimeException ( "Do not do time-consuming work in the Main thread" );
}
}
}
这段方法有几点注意的。
主线程判断,使用Looper.myLooper() == Looper.getMainLooper()
可以准确判断当前线程是否为主线程。
BuildConfig.DEBUG 条件控制,只有在debug环境下抛出异常,给予开发者明显的提示。当然也可以使用自定义的是否抛出异常的逻辑
如果当前线程不是主线程,那么就被认为是工作者线程。
比如上面的方法加入checkWorkerThread检查
1
2
3
4
private void writeContentToFile () {
checkWorkerThread ();
//代码省略,具体实现参考上面
}
再次执行程序,会曝出异常。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java . lang . RuntimeException : Unable to start activity ComponentInfo { com . droidyue . checkthreadsample / com . droidyue . checkthreadsample . MainActivity }: java . lang . RuntimeException : Do not do time - consuming work in the Main thread
at android . app . ActivityThread . performLaunchActivity ( ActivityThread . java : 2664 )
at android . app . ActivityThread . handleLaunchActivity ( ActivityThread . java : 2733 )
at android . app . ActivityThread . access $900 ( ActivityThread . java : 187 )
at android . app . ActivityThread $H . handleMessage ( ActivityThread . java : 1584 )
at android . os . Handler . dispatchMessage ( Handler . java : 111 )
at android . os . Looper . loop ( Looper . java : 194 )
at android . app . ActivityThread . main ( ActivityThread . java : 5869 )
at java . lang . reflect . Method . invoke ( Native Method )
at java . lang . reflect . Method . invoke ( Method . java : 372 )
at com . android . internal . os . ZygoteInit $MethodAndArgsCaller . run ( ZygoteInit . java : 1019 )
at com . android . internal . os . ZygoteInit . main ( ZygoteInit . java : 814 )
Caused by: java . lang . RuntimeException : Do not do time - consuming work in the Main thread
at com . droidyue . checkthreadsample . MainActivity . checkWorkerThread ( MainActivity . java : 34 )
at com . droidyue . checkthreadsample . MainActivity . writeContentToFile ( MainActivity . java : 40 )
at com . droidyue . checkthreadsample . MainActivity . onCreate ( MainActivity . java : 27 )
at android . app . Activity . performCreate ( Activity . java : 6127 )
at android . app . Instrumentation . callActivityOnCreate ( Instrumentation . java : 1123 )
at android . app . ActivityThread . performLaunchActivity ( ActivityThread . java : 2617 )
... 10 more
通过分析crash stacktrace 我们可以很轻松的发现问题的根源并解决。
哪些方法需要加上检查
本地IO读写
网络操作
Bitmap相关的缩放等
其他耗时的任务
如何选择工作者线程
Android中的工作者线程API有很多,简单的有Thread,AsyncTask,也有ThreadPool,HandlerThread等。关于如何选择,可以参考这篇文章。关于Android中工作者线程的思考
对比
StrictMode 是一把利器,但是检测的东西很多,打印出来的日志可能也有很多,查找定位问题可能不如文章的方法方便。
Method Tracing,需要刻意并时不时进行设置start和stop操作,文章的方法,可以说是一劳永逸。
检测会不会有性能问题
理论上是不会的,通常这个检测的代价要远远比耗时任务要小很多。
如果想进一步优化的,可以在编译期屏蔽这个方法的调用,即assumenosideeffects,具体可以参考关于Android Log的一些思考 中的编译期屏蔽 的内容。
延伸阅读
当你刚刚写完一个方法时,考虑这一下这个方法会不会很耗时,如果耗时,不妨增加一个线程的check。注意,一定要加载debug版,不要影响到线上的用户。