黄钟锐
摘 要:Android操作系统中应用的Activity组件有4种启动模式(Launch Mode)。根据不同的启动模式,其Activity组件在Back stack内的行为和活动的Activity之间跳转的行为会有所区别。因此,其启动模式会影响应用的性能(这里的性能是基于电量消耗、CPU占用率和内存使用得到的数据)。在实验时通过用马尔可夫链来建立一个虚拟的应用模型,对其中的启动模式进行调整,记录性能的状态并对结果进行对比,在这之后,得出一个基于性能的应用的Activity启动模式的优化建议。
关键词:Android;启动模式;性能测试
中图分类号:TP31 文献标志码:A
0 引言
在通常的Android应用程序当中,许多的行为是依赖于Activity组件才能正常运行的,而这些Activity的加载和存储会不可避免地占用时间和系统资源。实际上,在每个Activity组件的设置中都有对其启动模式的调整,这些启动模式的作用会在当前Activity发生变化时体现出来——不同的启动模式会影响Back Stack中的Activity的信息保留情况和自动新建Back Stack的条件。如果启动模式不做调整,随着用户启动过的Activity的数量越来越多,Back stack中会有更多不活跃的Activity存储,占用和消耗系统资源。因此,合理地使用和调整启动模式有利于缓解Back stack的存储压力。因此,该文将会讨论启动模式的具体工作方式并基于马尔可夫链设计的Android应用模型来模拟这一过程。
1 启动模式的介绍
在常见的Android应用中,通常采用给Activity设置启动模式的方式来控制其生成实例的具体行为以及在Back Stack中的储存。
1.1 Standard(STD)
这是Android里默认的启动模式,在STD的模式下,新的Intent都会被放到Back Stack的栈顶。并且不会创建新的任务(Task),即保持在当前的Back Stack中操作。这意味着,在该启动模式下的Activity内进行返回操作,系统会将Back stack顶层的Activity移除,也就是退回到生成这个Intent的Activity内。
1.2 singleTop(STP)
如果某个Activity的启动模式设置为singleTop,那么之后的操作也不会涉及创建新的Task和生成新的Back Stack。不过在将Activity存储到Back Stack之前,会先检查栈顶的Activity。如果这个Activity也是同样的Activity的实例,那么接下来就不会创建新的Activity并放到栈顶,而是直接返回栈顶的Activity。这意味着不管在栈顶创建多少次一样的singleTop类型的Activity,只要进行一次返回操作,Back stack的状态就会倒回到这一系列的操作之前。在设置Intent的Flag值时,通过将Flag的值设置为FLAG_ACTIVITY_SINGLE_TOP即可得到和xml里声明singleTop相同的行为。
1.3 singleTask(STK)
singleTask会使得该Back Stack中只存在一个该Intent的Activity——如果Back Stack中没有与之相同的Intent,那么新的Activity会压到到栈顶,反之,singleTask的Activity会使得Back Stack中具有相同Intent标识的Activity以上的内容都被从中销毁。用这种机制来保证在一个Task中只有一个相同的实例。而如果在第一种情况下进行返回操作,也就是没有其他相同Intent的Activity的情况下,那么Back Stack中仍然保留有原先的Activity,可以倒回到进行操作之前的状态。而在第二种情况下,因为原先的Activity在Back Stack中已经被销毁,所以此时进行返回操作,会直接退出应用,因为Back stack为空,所以在设置Intent的Flag值时,通过将Flag的值设置为FLAG_ACTIVITY_NEW_TASK即可得到和xml里声明singleTask相同的行为。
1.4 singleInstance(SIT)
如果Activity設置成这种模式,那么新建的Activity会单独占一个Task——同时新建一个仅仅包含新Activity的Back Stack。而任何新的Back Stack会维持在仅有一个Activity的状态。但是,特殊的是,如果在singleinstance中的Activity进行返回操作,会导致Task也随之切换——从新的Task退回原先的Task的栈顶。在远栈顶进行返回操作无法跳转至新的Task上。
2 基于马尔可夫链的多种启动模式性能分析
2.1 设计思想
2.1.1 马尔可夫链的引入
马尔可夫链是指状态空间中一个状态到另一个状态的转换的无记忆性的随机过程。这一过程只与当前的状态有关,和之前的过程没有关联。而状态的转移概率则是取决于当前所处的状态转向其他状态的概率。由时刻n转向时刻n+1的跳转概率表示方法为Pr(Xn+1=x|Xn=xn)其中Pr表示跳转的概率,n与n+1表示第n时刻和第n+1时刻,X表示某时刻的状态,x为具体的状态类型。 而更直观的表示方法,如图1所示,可以直接用有向图在路径上标明概率的方式来表示马尔可夫链。
类比到这里的模型中,在固定的时间内,对于每种状态,我们规定好用户进行各种操作的可能性,方便之后进行模拟。而在数据结构上,用一个xml文件来存储跳转的方向和概率,在读入后转换成用二维数组表示的邻接矩阵。此时处理跳转问题就可以像处理其他问题一样来处理Activity之间的连接关系了。这里引入马尔可夫链的意义不仅是在于实现一个跳转的控制或者得到随机的结论,而是希望能借助这个模型来模拟实际使用时的用户行为。这里笔者首先选用了一组有向图中所表示的数据来进行测试,接下来会有一组根据笔者使用社交软件行为的数据模拟。
2.1.2 资源占用的表示和测量
使用的测量方式是使用Google的Android Profiler来进行应用性能的测量。在应用运行时,通过Android Profiler可以得到我们需要的性能指标。而在这个例子中,我们测试的指标是CPU使用、内存占用和电量消耗。这样的好处是监控和测量消耗的资源是由电脑来提供的而不是手机端进行资源消耗,可以将关注点都放在应用本身。
2.2 模型的构建
在准备好模拟的条件之后,就要做好具体模型环境的搭建。在每个Activity启动时,读取xml配置文件中的数据,从中提取出数据。而在另一方面,每次Activity进行的时候都生成一个随机数,用来记录之后要进行的动作。在这之后,根据xml数据进行对应的动作。不同的Activity也对应着不同的内容和启动模式以及跳转概率。
3 测试与验证
3.1 资源加载
如果没有适量的资源放到应用中进行加载,那么资源的消耗就会变得不是很清晰。因此在设计时,可以特意放一些组件和图片音频等资源来增加对资源加载的负荷,以此来模拟Activity在加载时需要进行的工作。
3.2 实验环境
该次实验的所有实验环境采用的是Pixel 2XL的虚拟机,虚拟机是通过Android Virtual Device生成的。配置列表见表1。
3.3 基于马尔可夫链的启动模式性能分析
3.3.1 全部用TSD模式
观察到的情况是在standard的情况下,如图2所示。内存的使用会出现一个逐渐上升的趋势。这说明Back Stack中仍然存储着这些资源,并且在创建和销毁Activity前CPU使用率和电量的消耗也会出现一个小的极大值。
图中从上至下依次为CPU使用率,内存占用量,和电量消耗速度
3.3.2 混合采用
在这种情况下,内容比较复杂,而且行为稍有混乱,如图3所示,在cover的Activity启动时,电量的消耗要比其他的Activity要高一些,而cover的启动模式为singleTop。另一方面,CPU占用率方面,在nextcover的Activity启动时CPU的占用率增量要比其余的2种情况明显。可以认为是在singleTask的启动模式下对CPU的使用率更高。
3.4 限制
首先,在设计测试时有意忽略掉了singleInstance的情况。因为这种设计模式使用的情况较少,由于每次新建的Activity都会在新的Task中,singleInstance用户的体验是比较不符合习惯。第二,测试时没有引入TaskAffinity这个变量,这意味着在测试环境内不会出现包名不同的Activity在同一个Back Stack中的情况,所以在测试里没有发挥出singleTask的全部功能其实际情况要远远复杂于用马尔可夫链生成的简单模型,包括动态加载的资源。该文只是提供了一种解决这类问题的思路。另一方面,后续还应继续对得到的数据进行更多定量的分析,该文只是对比了其峰值的大小,如果能有更严谨的数学方法分析这些数据,一定能得到更多的信息。因此后续的研究应当着重于对实际情况的拟真和对获取数据进行更详细处理。
4 测试结果的分析和整理
在得到监控数据之后,接下来的工作就是对数据进行进一步的分析和处理。这里通过Android Profiler来获取了模拟程序运行的100 s内其在各层Activity上的状态信息。由于CPU的状态和电量消耗实时发生变化,所以在收集数据的时候更要关注每一段的极值点,并将多次的测量数据进行處理和统计。为了保持数据的直观性和尽可能多的保留更多的信息,在进行统计时采用了箱型图的方式保存数据。
Android Profiler 中没给出电量消耗的纵轴单位,只是用了Light、Medium、High和Heavy来表示电量消耗的速度快慢,为了便于统计,我们以整个运行过程中电量消耗最快的那一点作为1个单位,这里记录的为其余各时刻消耗电量和其所占的百分比。因为在电量消耗的最大值已经达到了Heavy的级别,因此可以作为对电量消耗测量的一个“标杆”来看待。
根据图4与图5中所呈现出的统计分析,从平均值来看,Standard的启动模式对CPU的占用要少一些,而singleTop和singleTask的启动模式在CPU占用上虽然平均值相差很小,但是singleTask对CPU的使用率表现更不稳定。而在电量消耗方面,singleTask的表现非常好,平均值和中位数明显低于另外2种,并且表现也比较稳定。而Standard和singleTop的最低消耗表现相似,singleTop的电量消耗虽然总体表现更稳定,但是最差情况非常糟糕。
5 结论
通过基于马尔可夫链的随机实验测试,可以明显的观测到,不同的启动模式对性能表现的影响是存在差异的。这也证实了实验过程的有效性。在一个Activity从Back Stack被移除前,其内存的占用是不会解除的,也就是说如果需要维持较低的内存占用率,我们可以通过阻止新Activity产生以及摧毁Activity的方式来对内存方面进行限制。而这需要通过活用singleTop和singleTask的方式来达到这一目的。从另一个角度考量,如果我们需要的是减少CPU的占用率,避免对Back stack进行复杂操作可以减少CPU的占用。这就应当在程序设计时减少singleTask和singleInstance的使用,使得应用对栈的操作量减小至最低。
另外,由于启动模式可以通过在Intent中设置Flag的值来控制,这就意味着我们可以在写程序的时候通过设置参数的方式来根据我们的需要选择对资源分配更有利的启动模式。不过在设置启动模式时还应该考虑到用户的体验(如使用singleTask的时候可能会销毁很多处于Back Stack中的Activity)。因此在选用时应当注意这点。而对于不是很确定的情况,可以通过调研等方式获取用户在使用应用时的各种行为及发生频率,通过构建马尔可夫链并在模型中监控资源消耗的方式来确认其是否合理。这给了开发者在选择启动模式时一个相对直观并可行的新思路。
参考文献
[1]Taolue Chen,Jinlong He,Fu Song,ex al.Android Stack Machine[J].LNCS 10982,2018,pp.487–504.
[2]Android documentation[S/OL].https://developer.android.com/guide/components/tasks-and-back-stack.html?hl=zh-CN.
[3]Chuangang Ren,Yulong Zhang,Hui Xue,ex al.Towards Discovering and Understanding Task Hijacking in Android[R].24th USENIX Security Symposium.2015.
[4]Norris,James R.Markov chains[M].UK:Cambridge University Press,1998.