APP下载

Android应用异步编程模型性能分析*

2018-10-12薛海龙

计算机与生活 2018年10期
关键词:开发人员调用线程

薛海龙,陈 渝,雷 蕾,王 丹

1.北京工业大学 计算机学院,北京 100124

2.清华大学 计算机学院,北京 100084

1 引言

Android系统以其突出的市场占有率和开放的特性吸引了大量的开发人员,上百万类型丰富而且创意独特的应用程序应运而生。然而,用户在享受丰富多彩应用的同时,较差的响应性能一直影响着使用体验。例如,应用的启动时间过长,不能在短时间内迅速对用户输入进行反馈等。在功能相似的Android应用中,响应性能差的往往最先被淘汰。面对竞争如此激烈的Android应用市场,开发人员迫切需要研制有效的手段来解决这个问题。

Android系统的程序开发框架使用单线程模型来处理用户输入事件,应用启动时,系统创建一个UI线程来运行应用程序,这个线程主要负责处理生命周期事件、用户输入事件和显示更新事件。所有的事件默认情况下在UI线程中顺序执行。因此,响应性能低下的原因往往是由于在UI线程执行耗时操作导致的,例如磁盘I/O、数据库的读取或者网络访问等,这些操作会导致UI线程阻塞,使得显示不能及时更新,或者用户陷入长时间的等待中。为避免出现这个问题,移动开发采用了异步编程技术,将这些耗时操作放到UI线程以外的工作线程中去执行。由于Android提供了多种异步编程模型供开发人员使用,例如HandlerThread、IntentService、AsyncTask、ThreadPool以及新建Thread类来执行异步操作。因此,使用异步编程技术可在一定程度上提升系统性能。

然而,从异步编程模型的特点可知,采用该模型的程序往往会在多条线程中异步执行,使得它的执行过程变得十分复杂。如果使用了这些编程模型,性能却没有达到预期的效果,开发人员就很难找到性能异常的根源所在。因此需要研究帮助开发人员分析程序在多条线程中的执行情况的有效方法。

2012年,微软的Ravindranath等人为了帮助Windows手机应用开发人员了解相应应用在真实使用过程中的性能瓶颈和运行失败原因,提出了一种在真实使用过程中跟踪应用性能的检测分析方法[1]。该方法关注以用户对UI的操作为开始节点,以由操作触发应用程序中的所有同步和异步任务处理完成为结束节点,为识别异步处理任务的性能瓶颈还需要正确跟踪跨异步边界处理的因果关系。在实现上,该方法首先确定构建用户事务的执行跟踪所需的全部信息,包括UI操作事件信息、线程执行信息、异步调用因果关系、线程同步信息、UI更新信息以及未处理的异常[2]。然后通过引入两个自定义的库,对Applications层App的二进制代码进行动态插桩,向开发人员展示异步程序执行的全部流程,最终构建出程序执行的关键路径,给开发人员指向可能导致响应性能异常的根源。

利用这种方法,Ravindranath等人开发了App-Insight[3],它准确地向开发人员展示了异步程序执行的整体流程,并构建出程序执行关键路径帮助开发人员识别性能瓶颈和异常根源。AppInsight是基于Windows编程框架中的指定接口实现的,因此只能对Windows平台上的应用进行性能监测与分析,并不能直接应用到Android应用性能的监测分析上。

Facebook公司在这个方法的基础上,针对于Android编程框架的特点,开发出了一套远程性能监控工具,用于对自己所开发的应用在真实用户的使用情况进行监测[4]。Facebook官方称利用此方法确实解决了一些性能问题。

然而,以上对App的二进制源码进行动态插桩的办法只能作用于App自身,即只有被测的App启动之后,才可以监测用户的行为操作得到插入的Log,App启动过程中的异步性能瓶颈是没办法检测到的。针对这个问题,本文提出对Android系统的Framework层进行静态插桩来捕捉异步任务处理的各项信息,以监测到App启动之后,并覆盖到App启动过程中的所有异步执行流程。本文的主要贡献:

(1)提出在Framework层进行静态插桩对应用程序层的所有异步任务的执行进行的跟踪方法。该方法可以有针对性地在Framework层的API接口中找到其对应的关键代码,通过一次插桩就可以解决所有应用程序的监测,相比较对应用程序的插桩是更轻量级的,减轻了性能分析的复杂性。

(2)在实现策略上,本文通过对异步任务上的所有事务信息进行插桩后,找到处理流程的一条关键路径。该路径上包括触发用户事务的关键事件。如果存在异步任务的处理会有新线程开始的时间戳,之后就是异步任务开始处理的过程,处理完成后会通过Handler发送消息给主线程进行界面的刷新。通过对关键路径的分析,可帮助开发人员找到应用程序的异步任务性能瓶颈。

2 Android系统结构概述及其异步处理任务

2.1 Framework层

Android的系统架构和其操作系统一样,采用了分层的架构,分为4层,从高层到低层分别是应用程序层、应用程序框架层(Framework)、系统运行库层和Linux核心层[5],如图1所示。

Framework层提供给Android开发人员一系列的服务和API的接口,然后将一些基本功能实现,通过接口提供给上层调用,可以重复调用。应用程序层的所有App都是通过调用Framework层的各种API来实现业务需求的。

2.2 异步任务处理机制

Android程序的大多数代码操作都必须在主线程执行,例如系统事件、输入事件、程序回调服务、UI绘制等。那么在上述事件或者方法中插入的代码也将在主线程中执行。一旦在主线程里面添加了操作复杂的代码,这些代码会影响应用程序的响应性能。因此,为了提高用户体验或者避免ANR(application is not responding)通常都会把一些耗时的任务放在子线程中去处理。

对于异步执行的任务,不需要等待返回结果,而是等任务处理完成后,再通知界面刷新。在Android中开启异步任务的方式主要有Thread、HandlerThread、IntentService、AsyncTask和ThreadPool这5种[6],开发人员可以针对不同的应用场景选择不同的异步处理方式。

子线程和主线程的消息传递使用Android中的异步消息处理机制:Handler。Handler是谷歌封装好的一个消息处理接口,它可以绑定在主线程或者子线程中,当异步任务处理完成后可以利用Handler发送一个消息出去,主线程接收到这个消息之后就说明异步任务已经执行完成了进而可以刷新界面,这就实现了异步消息的传递[7]。

3 关键路径构建及分析

本文通过分析用户事务期间所对应的关键路径来判断性能瓶颈。为方便叙述,下面先给出这些术语的定义。

定义1(用户事务)用户事务指用户对UI的操作开始,并完成操作触发的所有异步任务结束。例如在图2中,用户事务从onClick事件开始,并在UI update事件结束。

定义2(关键路径)关键路径是指用户事务中的瓶颈路径,从而更改关键路径的任何部分的长度将改变用户感知的持久性。关键路径以用户操纵事件开始,并以UI更新事件结束。在图2中,从①到⑦的整个路径构成事务的关键路径。

Fig.1 Android system structure图1 Android系统结构

Fig.2 Critical path图2 关键路径

本文通过对这条关键路径上的节点所对应Framework层中的源码进行插桩来确定每个节点运行的时间点和整个用户事务的运行时间,并通过Logcat输出插桩信息。

鉴于Android自带的Logcat功能非常强大,利用它可以得到插桩Log,并通过进程号和一些筛选条件可以过滤掉大部分的无关Log,然后通过打印Log所在的线程ID,可以一目了然地得到用户事务的关键路径。

本文对应用程序的迭代过程中的所有用户事务构建关键路径,并对关键路径的运行时间进行对比分析。如果某个版本的一个用户事务的运行时间明显过长,可以确定此处是有问题,即是性能瓶颈,然后把这个问题反馈给开发人员,让开发人员对代码进行优化。

4 事件插桩

由于关键路径以用户操纵事件开始,并以UI更新事件结束,下面介绍事件的插桩。

4.1 点击事件插桩

Android中点击事件分为4类:(1)匿名内部类;(2)自定义事件监听类;(3)Activity继承View.On-ClickListener,由 Activity 实现OnClick(View view)方法;(4)在XML文件中显示指定按钮的onClick属性,这样点击按钮时会利用反射的方式调用对应Activity中的 Click()方法[8]。

无论哪种写法,经过分析源码,其最终都会调用Framework中View.java文件中的performClick函数,因此在这个位置插桩就可以得到点击事件触发和结束的临界点,如图2中的①和③所示。

4.2 异步消息处理的插桩

Fig.3 Handler dispatch message process图3 Handler处理消息流程

Android中异步消息处理使用最广泛的就是Handler机制,Handler处理消息的流程如图3所示,包括以下4个要素。(1)Message:消息,理解为线程间通讯的数据单元;(2)Message Queue:消息队列,用来存放通过Handler发布的消息,按照先进先出执行;(3)Handler:Handler是Message的主要处理者,负责将Message添加到消息队列以及对消息队列中的Message进行处理;(4)Looper:循环器,扮演Message Queue和Handler之间桥梁的角色,循环取出Message Queue里面的Message,并交付给相应的Handler进行处理[9]。

经过对Handler源码的分析,确定对于异步消息处理的插桩需要分为两部分:子线程的sendMessage和主线程的handlerMessage,找到这个关系就可以确定主线程和子线程的因果关系。

4.2.1 sendMessage

在应用层子线程发送消息主要有send和post两种方式[10],其中send包括sendEmptyMessage(int what)、sendEmptyMessageAtTime(int what,long uptimeMillis)、sendEmptyMessageDelayed(int what,long delayMillis)、sendMessage(Message msg)4 种;而 post包括 post(Runnable)、postAtTime(Runnable long)和 postDelayed(Runable long)3种。跟踪这几种方法的调用栈发现最终调用的会是sendMessageAtTime和send-MessageAtFrontOfQueue其中一个函数,而这两个函数最终返回的都是enqueueMessage,因此选择在enqueueMessage的关键位置进行插桩,得到如图2中的⑥。

4.2.2 handlerMessage

应用层处理消息是在绑定Handler的线程中进行的,利用handler的handlerMessage处理MessageQueue中的消息,handlermessage最终都会调用Framework层的dispatchMessage[11],因此选择在dispatchMessage的关键位置进行插桩,得到如图2所示的⑦。

4.3 子线程插桩

Android中开启子线程主要有Thread、Handler-Thread、IntentService、AsyncTask和ThreadPool 5种方式,针对不同的应用场景开发人员可以选择不同的开启方式。其中AsyncTask和ThreadPool都是开启一个线程池来处理异步任务,而这种方式开启子线程其实就是封装了一个Thread,因此这3种方式可以归为一种来讨论。以上5种方式可以分为3类:Thread、HandlerThread和IntentService。

4.3.1 Thread

Android用Thread开启子线程的方式和Java中的相似,有继承Thread和实现Runnable接口两种方式,Runnable接口里只有一个无参无返回值的run()方法,而Thread类也是实现了Runnable接口并重写了run()方法,因此主要分析Thread类来找关键位置插桩。

开发人员在应用层start开启子线程是调用的libcore库中Thread类的start方法,接着转调VMThread的native方法create,然后转到native层去创建子线程并设置属性,创建成功后调用Thread类的run方法执行开发人员自定义的异步任务[12]。在这个创建的过程中,需要关注的有两个地方:线程start和线程run。线程start是主线程调用的,这就是子线程开启的始点,如图2所示的②。线程run的过程是子线程处理异步任务的过程,找到子线程run的起始点,就得到了如图2所示的④和⑤。

4.3.2 HandlerThead

HandlerThread本质上是一个Thread对象,只不过其内部创建了该线程的Looper和MessageQueue[13]。开发人员使用HandlerThread很简单,只需要新建一个对象,让它start,并在handlerMessage中处理异步任务。

分析源码知道这里调用的start仍然会调用是Thread的start方法来创建新的线程,但是创建完成之后不会调用Thread的run方法,而直接调用Handler-Thread的run方法,因此需要在HandlerThread的run方法里插桩来分辨开启的是一个HandlerThread。它处理异步任务是在handlerMessage中进行的,这就可以利用之前消息处理的插桩结合线程ID得到它处理异步任务的临界点。

4.3.3 IntentService

IntentService 是Looper、Handler、Service的集合体[14],IntentService是继承于Service并处理异步请求的一个类,在IntentService内有一个工作线程来处理耗时操作,启动IntentService的方式和启动传统Service一样。IntentService可以自动开启一个Handle-Thread,并自动调用IntentService中的onHandleIntent方法来处理异步任务。

既然IntentService是开启了一个HandlerThread,那之前插的桩都可以直接使用,只是IntentService处理异步任务和之前的方式都不同,它是自定义了一个onHandleIntent方法,因此捕捉它处理异步任务的临界点就需要在这个方法里插桩。

4.4 界面刷新插桩

Android中的任何一个布局、任何一个控件其实都是直接或间接继承自View实现的,当然自定义控件也不例外,因此这些View应该都具有相同的绘制流程与机制才能显示到屏幕上。经过总结发现每一个View的绘制过程都必须经历3个最主要的过程,也就是measure、layout和draw[15]。而整个View树的绘图流程是在ViewRootImpl类的performTraversals方法开始的,监测界面刷新的流程需要在perform-Traversals处插桩。

5 实验与分析

为了测试此方法的可行性和有效性,本文设计了相关的测试用例,并在实际Android项目中使用。

5.1 实验环境

5.1.1 软件环境

本文的所有实验都在OPENTHOS系统上完成。OPENTHOS是将Android 5.1原生系统移植到PC端并实现多窗口、多任务等一系列功能的开源操作系统,是由清华大学、清华同方和一铭公司共同联合开发的。

5.1.2 硬件环境

把OPENTHOS系统安装在清华同方T45笔记本上进行实验与分析。T45的配置如下:

CPU,Intel酷睿i56200U;内存,4 GB;磁盘,500 GB;显卡,2 GB独立显卡。

5.2 集合所有异步编程模型的实例

在上文已经介绍过,在Android异步编程模型中有5种开启子线程的方式,为了验证本文插桩方案的有效性,需要对每一种进行测试,基于此编写了一个测试用例。本文设计的这个测试用例的主要功能是根据一个图片的网络地址,开启一个子线程下载这个图片并通知主线程刷新界面,将图片显示到界面上,分别用 Thread、HandlerThread、IntentService、AsyncTask、ThreadPool实现这个功能,通过插桩得到异步任务处理的关键路径。图4所示就是一条关键路径,其中前两列是日期和时间,第3列是进程ID,第4列是线程ID、第5列和第6列是插桩的Log标签,第7列是主要的Log信息。

Fig.4 Critical path of Thread图4 Thread的关键路径

如图4是Thread的关键路径,第1行和第4行是用户点击事件的临界点,对应图2的①和③;第3行是主线程开启子线程时调用的Thread的start方法,对应的是图2的②;第5行和第9行是子线程处理耗时任务的临界点,对应图2的④和⑤;第6行是子线程处理完异步消息后向主线程发送消息通知主线程刷新界面,对应图2的⑥;第7、8、10、11行是主线程处理子线程发来的消息并进行界面刷新的过程,对应图2的⑦。

图5是HandlerThread的关键路径,它与Thread的最大区别就是子线程在dispatchMessage中处理异步任务,所以第4行的Log会显示这是一个Handler-Thread,它的异步任务是在第7行和第8行的地方处理的。

图6是IntentService的关键路径,上文提到它其实是封装了一个HandlerThread,因此对Handler-Thread的插桩也存在,而且IntentService是在onHandleIntent中处理异步任务,通过对onHandleIntent的插桩得到图6的第8条和第12条Log。

Fig.5 Critical path of HandlerThread图5 HandlerThread的关键路径

Fig.6 Critical path of IntentService图6 IntentService的关键路径

图7和图8分别是AsyncTask和ThreadPool的关键路径,这两种异步处理的方式和Thread是一致的,因此得到的关键路径和Thread无很大的区别。

Fig.7 Critical path ofAsyncTask图7 AsyncTask的关键路径

Fig.8 Critical path of ThreadPool图8 ThreadPool的关键路径

得到以上关键路径之后,为了对比当异步处理出现问题影响响应性能的实例,让子线程都sleep 2 s,这样得到的关键路径如图9所示,可以发现第5行开始子线程处理异步任务,第7条处理完成的时间比图4延长了1.993 s,这就说明这次的异步处理任务是有性能问题的(其余的几种异步方式结果都是类似的,就不再赘述)。

Fig.9 Thrad performance exception图9 Thread性能异常

5.3 OPENTHOS的StartMenu实例

OPENTHOS在开发的过程中一直碰到Start-Menu卡顿的问题,尤其是在安装的几十个应用的时候,点击StartMenu之后需要2 s左右才能显示出来,为了分析这个问题利用本文的方法在OPENTHOS中进行插桩并分析原因。

本文对初期版本和近期的分别进行分析,在对初期版本进行插桩分析的时候发现StartMenu启动的时候并没有开启子线程,也就是说启动的过程中所有的任务都是在主线程完成的。带着这个问题对StartMenu的源码进行了分析,发现在启动的过程中是需要查询数据库把所有已经安装的应用显示出来的,而查询数据库这样耗时的任务在主线程执行,势必会影响性能。

接下来,对近期的版本进行分析,插桩得到的Log信息如图10所示,第3行是子线程处理耗时任务,即查询数据库的开始点,第5行是结束点,但是可以发现这本应该是一个时间段,结果却显示是一个时间点,而且子线程只发送了一条消息就结束了,这绝对是不应该的。带着这样的问题,分析源码并梳理StartMenu的逻辑关系,发现启动的时候开启的子线程仍然没有处理耗时任务,而只是进行了一个消息的传递,这和初期的版本区别并不大。因此和开发人员一起重新整理StartMenu的逻辑流程,对代码进行调整,得到最新的版本。如图11所示,这样耗时的任务就在子线程中执行,性能也提升了不少。

Fig.10 StartMenu analysis 1图10 StartMenu分析1

Fig.11 StartMenu analysis 2图11 StartMenu分析2

6 结论

(1)对Android的Framework层进行插桩,把插桩得到的Log信息保存下来进行离线分析,以发现异步处理任务的关键路径,可以帮助移动应用程序的开发人员监控和诊断他们的应用程序的性能。

(2)对Framework层进行插桩是轻量级的,只需要一次插桩就可以获得关键路径,有更好的通用性,对于迭代发展的版本之间的性能对比尤其明显。

(3)对Framework层进行插桩分析可以找到程序中性能瓶颈的位置,并向开发人员提供反馈。

猜你喜欢

开发人员调用线程
5G终端模拟系统随机接入过程的设计与实现
实时操作系统mbedOS 互斥量调度机制剖析
浅析体育赛事售票系统错票问题的对策研究
核电项目物项调用管理的应用研究
Semtech发布LoRa Basics 以加速物联网应用
系统虚拟化环境下客户机系统调用信息捕获与分析①
后悔了?教你隐藏开发人员选项
利用RFC技术实现SAP系统接口通信
三星SMI扩展Java论坛 开发人员可用母语
C++语言中函数参数传递方式剖析