您的位置:首页 > 其它

慎用AsyncTask

2016-04-27 16:23 204 查看
       今天销售反应一个问题,首次启动app(进程从无到),直接点击拍照,在拍照成功后会卡顿一段时间再进入截图页,但之后进入就再也没有这个现象。

       把手机借来,直接看android monitor logcat,发现三个信息:

1.该用户的手机通讯录极多;

2.卡顿的时间段里,app在执行上传通讯录的操作;(打印出网络请求和response的数据)

3.上传中,AsyncHttpClient(网络传输使用的是AsyncHttpClient)报出“W/AsyncHttpResponseHandler: Current thread has not called Looper.prepare(). Forcing synchronous mode.”的提示。

       于是分别从这三个信息入手。

       先介绍下通讯录上传操作的实现。通讯录的上传操作,被封装在AsyncTask的doInBackground(),显然是在后台线程中执行。通讯录的上传包括,1.读取本地通讯录到内存中;2.将本地通讯录与app数据库中的通讯录进行比较;3.如果本地通讯录内容增加,则分组(每组20个)同步上传。

        首先,针对现象2,想起之前看到的Android性能优化典范里,“大多数手机的屏幕刷新频率是60hz,如果在1000/60=16.67ms内没有办法把这一帧的任务执行完毕,就会发生丢帧的现象。丢帧越多,用户感受到的卡顿情况就越严重。”于是脑洞大开,难道是doInBackground()操作耗时太久,CPU没有来得及切换,造成没有渲染。

        对操作系统有实际接触的读者应该能想明白,CPU的切换是个频繁的过程,“在Linux中可以使用vmstat来观察上下文切换的次数,一般来说,空闲的系统,每秒上下文切换次数大概在1500以下。”,也就是在其他线程做耗时操作,并不会阻塞UI线程。我的担心显然是多虑的。

        但当时我没有想通这一点,就在doInBackground()里,在耗时的操作(如读取数据库、同步网络请求)之间都加了Thread.sleep()操作,以主动让出CPU。试了下,根据上面的分析,结果当然是失败的。

       

        不气不馁,根据现象3,又脑洞大开,想是否是AsyncHttpClient同步网络请求时,由于非Looper线程,导致回调执行是在UI线程中,造成了UI线程的阻塞?想到就做,我直接将AsyncTask改成IntentService实现,试了下,不卡顿了,oh
yeah!但同时引入新的问题,通讯录的操作没执行,想了下,it's nothing, Service组件是要声明在AndroidManifest.xml中的,试了下,两个问题都完美解决!

        如果止步于此,那就太low了,看了下AsyncHttpResponseHandler的代码,发现如果在非Looper线程中执行,那回调的代码就在执行网络请求(HttpClient.execute())的线程中执行。而且,我用的是同步请求,网络请求直接在发起网络请求(AsyncHttpClient.post())的线程中执行。也就是网络请求、网络请求的回调处理都是在AsyncTask的doInBackground()的线程中执行的。也就不存在回调在UI线程执行,造成UI线程阻塞的问题。

       那问题的原因是什么呢?

       为了分析卡顿时间内到底做了什么,果断上DDMS的TraceView的大招,点击拍照按钮并按下start Method profiling,等进入截图页,按下stop Method profiling。查下inc CPU time最大的几个函数,就有doInBackground(),查看通讯录操作时间:

LoadContactsTask中  
inc CPU time
compareContacts()耗时1006ms
其中执行updateMergeType() 83次 每次6.565ms  共计544.89ms
执行hasFriend() 83次  共计456.948ms
一次网络请求的时间(包括回调处理) 在26ms、61ms



inc REAL time(包含CPU调度,其他线程执行的时间)
compareContacts()耗时2877.501ms
       得出来结论,通讯录的操作中,当联系人多时,数据库的读取占大头,网络请求占小头,插入一条数据,需要6.5ms,一次网络请求时间在60ms。

       问题到了这里,也没找到确切原因,脑洞又开,google下"慎用AsyncTask",随便打开几篇文章,第一个坑:AsyncTask在Android各个版本中可以算是频繁修改了,比较各版本代码发现,修改过多次,而且这种修改会导致很大的差别。最坑的修改是在Android 4.0开始的,AsyncTask的中execute方法的实现在4.0以前是采用Thread
pool executor,各个版本对线程池中的可并行线程数限制不同,但毕竟多个Task是多线程并行。而在4.0开始,改为用serial executor,就是说同一时间只能有一个线程运行,其他线程必须等待该线程完成之后才能开始执行,因此就变成了串行的worker thread。” “3、串行和并行多版本不一致 AsyncTask在1.6之前为串行,在1.6-2.3为并行,在3.0之后又改为串行,在3.0之后虽然可以通过代码来改变默认的串行为并行,但是又是一个繁琐的操作”。瞬间想起。在拍照成功后,采用异步线程将byte[]生成bitmap,之后才打开截图页面。之前使用的是Thread,上个版本被同事换成了AsyncTask方式。也就是说,拍照完成后,通讯录上传操作在后台线程执行,拍照成功后,生成图片的线程被阻塞,必须等通讯录线程处理完成后再执行,而UI线程虽然没被阻塞,但由于没有执行跳转操作(要等生成图片的线程执行完)。所以,给了用户一种假象,界面跳转很卡!!!
wtf!!!
       为了验证是否是真正原因,在其他代码都保持不变的情况下(通讯录处理仍然使用AsyncTask),将图片处理的线程改成Thread实现,试了下,不卡,不卡,我不卡。从而验证,果然是AsyncTask惹得祸。
       其实对于一个问题,总是有很多细节可以定位,比如TraceView的结果如果这样看,可以看到SerialExecutor和三个doInBackground。



        最后还是要读下AsyncTask的源码。  
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: