前置文章:
《 Android 4.4 Kitkat Phone工作流程浅析(一)__概要和学习计划》《Android 4.4 Kitkat Phone工作流程浅析(二)__UI结构分析》
《Android 4.4 Kitkat Phone工作流程浅析(三)__MO(去电)流程分析》
《Android 4.4 Kitkat Phone工作流程浅析(四)__RILJ工作流程简析》
《Android 4.4 Kitkat Phone工作流程浅析(五)__MT(来电)流程分析》
《Android 4.4 Kitkat Phone工作流程浅析(六)__InCallActivity显示更新流程》
《Android 4.4 Kitkat Phone工作流程浅析(七)__来电(MT)响铃流程》
《Android 4.4 Kitkat Phone工作流程浅析(八)__Phone状态分析》
《Android 4.4 Kitkat Phone工作流程浅析(九)__状态通知流程分析》
《Android 4.4 Kitkat Phone工作流程浅析(十)__"通话显示"查询流程》
《Android 4.4 Kitkat Phone工作流程浅析(十一)__PSensor工作流程浅析》
《Android 4.4 Kitkat Phone工作流程浅析(十二)__4.4小结与5.0概览》
概要
Google在2015年3月9日低调发布了Android 5.1,从官方博客的描述来看只是进行了小幅更新,如增加多SIM卡支持,HD Voice支持等。虽然Google对外声称只是稳定性和性能上的微调,但在查看Telephony Phone相关代码后,Android 5.1在这一块的动作还是很大的。就目前了解的情况来看,Telephony Phone相关的改动包括:
1. 移除com.android.incallui进程;也就是说incallui以后也是运行在com.android.dialer进程中。
2. Telecom Service运行在system_server进程中;
3. 在Telecom Service中,CallActivity和EmergencyActivity使用独立进程com.android.server.telecom:ui;
4. 新增CircularRevealActivity;当发起OutgoingCall时,会先启动一个默认背景为蓝色的Activity即CircularRevealActivity,该Activity的作用主要是显示背景,用以给用户带来响应快的错觉。因为InCallActivity的启动较为耗时,因此先启动该Activity以便给用户带来一些界面改变效果。
本文来自http://blog.csdn.net/yihongyuelan 转载请务必注明出处
在Android AOSP 5.0 中 Telephony Phone 进行了重构,但在后续的测试中发现,MO发起过程中InCallActivity的较慢,且在Qcom和MTK等厂商加入双卡设置后,InCallActivity的界面呈现时间很长,用户在点击拨号按钮后需要等5s左右才能看到通话界面。在分析与解决该问题的过程中Google悄然发布了Android 5.1,其中对InCallActivity启动较慢的问题进行了一些优化。本系列文章主要记录分析Android 5.1 中 InCallActivity启动相关,以及一些优化尝试。
整个分析大纲如图1所示:
图 1 InCallActivity启动分析大纲
InCallActivity启动流程
因无论是MO还是MT,InCallActivity的启动流程类似,这里仅以MO为例。在Android 5.1 中InCallActivity 的启动有些许改动,但整体上与Android 5.0保持一致,整个InCallActivity的启动时序如图2所示 ( 以MO为例 ):
图 2 Android 5.1 InCallActivity start process(MO)
查看上图后可以知道,Android 5.1 中InCallActivity启动流程主要包括以下关键步骤:
1. 构造Dialing Intent
主要包括从Dialer的DialpadFragment的onClick方法开始,到Telecom Service的CallReceiver的onReceive方法中。Android 5.1 与Android 5.0的主要区别在于,前者在CallActivity中,始终会通过广播的方式将Dialing Intent发送给CallReceiver。而后者则是通过代码回调实现。
2. 启动InCallActivity
2.1 bind InCallService
并不是每次拨打电话都会执行bind InCallService 的操作。如果当前正在通话中,此时拨打号码则不会再次执行bind InCallService的操作。对于Android 5.1来说,bind InCallService这里有一点改动。如果是OutgoingCall,在bind InCallService的intent中会新增两个Extra值,即TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS和TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE,这两个Extra值在后续的CircularRevealActivity显示时会用到。
2.2 InCallServiceImpl新增onBind和onUnbind方法
Android 5.1中,InCallServiceImpl中新覆写了onBind和onUnbind方法,并在onBind方法中执行InCallPResenter的setUp以及启动CircularRevealActivity。
2.3 新增CircularRevealActivity用于提高OutgoingCall的响应
bind InCallService一旦成功,立刻启动CircularRevealActivity,该Activity实际上是一个背景。在Android 5.0中,如果bind InCallService成功后,还需要等待InCallActivity的加载,最后才能显示给用户,而此时会导致很长时间的界面凝滞。如用户点击拨号后,需要等大概2s左右(AOSP)才能看到InCallActivity的启动。因此在Android 5.1中为了提高OutgoingCall的响应速度,在bind成功后立刻启动一个带动画的背景Activity,之后等InCallActivity准备好后再显示即可,这样做的目的是为了提高用户响应。
3. 显示动画
在CallCardFragment中,新增animateForNewOutgoingCall()方法用于执行OutgoingCall时CallCardFragment的动画。为了更直观与Android 5.1 MO 启动InCallActivity的流程进行对比,大家可参考图3 Android 5.0 InCallActivity的启动时序图:
图 3 Android 5.0 InCallActivity start process(MO)
从MO的整个流程上来说,从用户点击拨号按钮开始到通话界面呈现,主要包括以下步骤:用户发起拨号操作,启动InCallActivity ,最后 更新InCallActivity状态。从图2和图3的时序图可以看出,虽然Android 5.1 相较于 Android 5.0属于小幅更新,但落实到代码中却有不少改动。如Android 5.0在Telecom Framework的ConnectionService.createConnection()方法中,通过mAdapter.handleCreateConnectionComplete()方法最终启动InCallActivity。而对于Android 5.1,在Telecom Service的 CallReceiver.processOutgoingCallIntent()方法中,通过getCallsManager().startOutgoingCall()方法最终启动InCallActivity。
InCallActivity启动时间统计
在前文了解InCallActivity启动流程的基础上,本节主要分析如何统计InCallActivity的启动时间。通过分析InCallActivity的启动时间,从而确定InCallActivity启动缓慢的原因,并最终找到相关优化方案,这正是本文的意图。
时间统计原理
对于InCallActivity的启动时间统计,想必大家一开始就能想到通过在onCreate和onResume中加入log,最后统计两个log的时间差,从而计算出InCallActivity的启动时间。单对InCallActivity来讲,这样做是可以简单统计出一个时间的,我把这个时间称之为“函数执行时间”,也就是程序在执行中,各个方法所消耗的时间。
对于开发人员来说,函数执行时间确实可以反映出一些问题,比如InCallActivity从onCreate到onResume总共消耗多少时间。同时,也可以在各个方法中插入log,这样可以大致定位出比较耗时的方法,从而分析并优化该方法,最终减少其执行时所消耗的时间。若为了罩住耗时方法,Traceview是不错的选择。
从开发人员的角度来看,函数执行时间确实有用,不过对于用户来说,典型的拨号流程是:点击电话图标->输入号码->点击拨号按钮->显示通话界面。从用户的角度来说,点击拨号按钮后,直到通话界面呈现,整个时间的长度决定了用户体验,我把这个时间称之为“用户体验时间”。用户不会关心拨号的流程经过了多少个进程,也不会关心程序执行的效率,只会从最直观的感受来觉得MO流程的长短。
如果将InCallActivity启动,按照用户体验的方式进行分解,则可以得到以下步骤:
①. 点击拨号按钮,对应的方法是Dialer中DialpadFragment.onClick()方法;
②. Telecom Service处理拨号请求;
③. Telecom Framework处理BindService;
④. 启动背景CircularRevealActivity及其动画;
⑤. 启动InCallActivity以及CallCardFragment相关动画;
因为程序的最终体验是由用户感知的,所以若要优化通话响应时间则需要统计用户体验时间,而“用户体验时间”则包括了“函数执行时间”。用户体验时间可以简单的等价于:
用户体验时间 = 函数时间 + 进程切换 + 动画执行
首先,对函数时间进行统计后可得知耗时函数,对其分析和优化后,可进一步减少。其次,在整个MO流程中涉及com.android.dialer/com.android.server.telecom/system_server等进程的切换,统计该时间后看是否可以减少相关进程切换。最后,对于用户来说,整个界面的完全呈现是在动画执行完成后,也可以考虑加快动画的执行提高用户响应;
时间统计方法
基于前一小节时间统计原理,本小节主要分析如何进行时间统计。在DialpadFragment.onClick方法中加入时间戳T1,同时在CallCardFragment的animationEnd方法中加入时间戳T2,那用户体验时间T = T2 - T1。
Timestamp
使用本地编译的AOSP 5.1 for Nexus 4进行统计测试,测试包括两种情况,即开机后第一次启动InCallActivity和非第一次启动InCallActivity的时间,每种情况统计16次并求取平均值、最小值、最大值,如图4所示:
图 4 InCallActivity用户体验时间统计表
通过图4的分析可以知道,InCallActivity启动用户体验时间在第一次开机时,最短需要1.947s而最长则需要2.589s,平均2.151s。而在非第一次启动时,最短需1.628s而最长则需1.838s,平均1.725s。整体上InCallActivity在第一次启动较为耗时,而非第一次启动则可以平均减少400ms。这是因为在InCallActivity开机后第一次启动时,会有各种资源需要初始化,而非第一次启动时,因为相关资源已经被初始化且未被系统回收,因此二者会有一些时间差。
在完成InCallActivity启动用户体验时间统计之后,对于其中各个关键函数的执行时间也进行了统计,如图5所示:
图 5 AOSP MO函数执行时间统计表
统计函数执行时间非常简单,只需在函数开始时加入Stamp1,函数结束时加入Stamp2,那函数执行时间FunT = Stamp2 - Stamp1. 其中TotalTime是从DialpadFragment.onClick方法到InCallActivity.onResume所消耗的总时间,而Process列则表示各个方法所处的进程名,其中framework仅表示该段代码在framework中并非framework进程。
以上是AOSP 5.1 userdebug for Nexus 4的相关统计结果,为了便于比较,请参看图6对比数据 ( 数据来源于真实项目S ):
图 6 S项目MO函数执行时间统计表
通过对比图5和图6可以很明显的看到差异,同时也将具体的问题和瓶颈暴露了出来,后续要着手优化时,则可针对具体方法使用traceview进一步分析。当然,也可以插入更多更细的timestamp进行分析。
Systrace
Systrace是Android 4.1 时google引入的系统性能分析工具(官方链接),用于分析应用性能以及显示其它进程的相关信息,本小节将使用Systrace对MO流程进行时间统计。在前一小节,使用timestamp的方式可以很好的进行时间统计,但需要在程序执行后对输出log进行收集并处理,而Systrace则可以省去处理log这一步。使用Systrace统计时间与timestamp类似,需要插入关键代码,如:
private void processOutgoingCallIntent(Intent intent) {在关键方法中插入Systrace的标记代码,完成之后编译并push到系统中。通过systrace.py脚本来抓去trace相关信息:
Trace.beginSection("Seven_processOutgoingCallIntent");
Uri handle = intent.getData();
String scheme = handle.getScheme();
String uriString = handle.getSchemeSpecificPart();
//... ...省略
intent.putExtra(CallReceiver.KEY_IS_DEFAULT_DIALER, isDefaultDialer());
sendBroadcastToReceiver(intent);
Trace.endSection();
}
$ cd android-sdk/platform-tools/systrace
$ python systrace.py -o mytrace_Telecom_Service.html -t 10 --app=com.android.server.telecom:ui gfx view wm am
以上代码用于抓去com.android.server.telecom:ui进程的Systrace,该trace中包含Graphics/View/WindowManager/ActivityManager的信息。在脚本执行完成后,在当前目录会生成一个mytrace_Telecom_Service.html的文件,打开后如图7所示:
图 7 TelecomService Systrace输出图
通过浏览器打开mytrace_Telecom_Service.html文件后,在右上角的Categories旁边的搜索框中,输入关键字"Seven"即可搜索到相关方法的执行时间。相对于timestamp来说,使用Systrace可以不用单独去统计日志,同时也可以分析系统当前其它关键进程的执行情况。
AMD Time
在第一次启动Activity时,ActivityManager会有相关log打印出来,包含了该Activity的包名和类名以及时间描述。如:I/ActivityManager( 9342): Displayed com.android.dialer/.DialtactsActivity: +1s59ms
I/ActivityManager( 9342): Displayed com.android.dialer/com.android.incallui.InCallActivity: +1s460ms (total +2s346ms)
不少童鞋将其中的+1s59ms和+1s460ms(total +2s346ms)当做DialtacktsActivity和InCallActivity的启动时间。本小节将对ActivityManager Displayed Time进行分析,并确认是否可以据此进行InCallActivity启动时间的统计分析(后文以AMD Time指代ActivityManager Displayed Time)。
因为该日志是的TAG是ActivityManager,所以直接在源码framework/base/services目录下搜索关键字Displayed:
grep -rnw "Displayed" framework/base/services简单排查后可发现:
frameworks/base/services/core/java/com/android/server/am/ActivityRecord.java:912: sb.append("Displayed ");进一步定位到ActivityRecord.reportLaunchTimeLocked()方法:
private void reportLaunchTimeLocked(final long curTime) { final ActivityStack stack = task.stack; final long thisTime = curTime - displayStartTime; final long totalTime = stack.mLaunchStartTime != 0 ? (curTime - stack.mLaunchStartTime) : thisTime; if (ActivityManagerService.SHOW_ACTIVITY_START_TIME) { Trace.asyncTraceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER, "launching", 0); EventLog.writeEvent(EventLogTags.AM_ACTIVITY_LAUNCH_TIME, userId, System.identityHashCode(this), shortComponentName, thisTime, totalTime); StringBuilder sb = service.mStringBuilder; sb.setLength(0); sb.append("Displayed "); sb.append(shortComponentName); sb.append(": "); TimeUtils.formatDuration(thisTime, sb); if (thisTime != totalTime) { sb.append(" (total "); TimeUtils.formatDuration(totalTime, sb); sb.append(")"); } Log.i(ActivityManagerService.TAG, sb.toString()); } mStackSupervisor.reportActivityLaunchedLocked(false, this, thisTime, totalTime); if (totalTime > 0) { //service.mUsageStatsService.noteLaunchTime(realActivity, (int)totalTime); } displayStartTime = 0; stack.mLaunchStartTime = 0; }查看以上代码可以知道AMD时间是由变量curTime、displayStartTime、mLaunchStartTime计算得到,继续查看变量displayStartTime和mLaunchStartTime,可以定位到ActivityStack.setLaunchTime()方法:
void setLaunchTime(ActivityRecord r) { if (r.displayStartTime == 0) { r.fullyDrawnStartTime = r.displayStartTime = SystemClock.uptimeMillis(); if (mLaunchStartTime == 0) { startLaunchTraces(); mLaunchStartTime = mFullyDrawnStartTime = r.displayStartTime; } } else if (mLaunchStartTime == 0) { startLaunchTraces(); mLaunchStartTime = mFullyDrawnStartTime = SystemClock.uptimeMillis(); } }通过ActivityRecord.reportLaunchTimeLocked()和ActivityStack.setLaunchTime()两个方法,不难猜测出AMD时间的由来,即在Activity启动流程的某个阶段( AMD启动时间戳 )调用ActivityStack.setLaunchTime(),完成Activity窗口创建、显示之后,再调用ActivityRecord.reportLaunchTimeLocked()方法( AMD截止时间戳 ),AMD时间即表示这两个时间戳之差。
AMD启动时间戳
在Activity启动流程中,当执行Activity的Stack初始化时会调用ActivityStack.setLaunchTime(),调用时序如图8所示:
图 8 ActivityStack.setLaunchTime调用时序图
这里需要了解Activity的启动流程(后续会将简单的分析流程整理成文章以供参考)。当执行到ActivityStack.resumeTopActivityInnerLocked()方法时,会先执行mStackSupervisor.startSpecificActivityLocked()方法,从而完成Stack的初始化并在其中调用ActivityStack.setLaunchTime(),记录AMD启动时间戳。此时执行ActivityStack.setLaunchTime()方法中的以下关键代码:
if (r.displayStartTime == 0) {需要注意几个变量:
r.fullyDrawnStartTime = r.displayStartTime = SystemClock.uptimeMillis();
if (mLaunchStartTime == 0) {
startLaunchTraces();
mLaunchStartTime = mFullyDrawnStartTime = r.displayStartTime;
}
}
1. ActivityRecord displayStartTime;
2. ActivityRecord fullyDrawnStartTime;
3. ActivityStack mLaunchStartTime;
4. ActivityStack mFullyDrawnStartTime;
以上变量将用于AMD时间的计算。
ActivityStack和ActivityRecord
前面提到了ActivityStack和ActivityRecord,这里简单描述下二者之间的关系。ActivityManagerService负责管理所有的Activity,在Android 2.3之后google将AMS中的HistoryRecord单独提取出来并命名为ActivityRecord。每一个Activity就是一个ActivityRecord实例,所有的ActivityRecord通过ActivityStack管理,而ActivityStack负责与AMS交互,如图9所示:
图 9 ActivityStack与ActivityRecord关系
在每一个ActivityRecord对象中保存着一个TaskRecord实例,用于标明该Activity属于哪个栈,各个Activity分属不同的栈( 这里所说的栈实际上对应代码里的Task ),如10图所示:
图 10 Activity与TaskRecord关系
在图10中,有三个栈即Task1、Task2、Task3,其中Activity A和Activity C属于Task1,Activity B和Activity D属于Task2和Task3。最近任务栏里的内容就是根据Task来显示,也就是说最近任务栏中只会显示三个应用APP,分别是Activity A和C所属的APP1,Activity B 和 Activity D所属的APP2、APP3。 如果Activity C 是通过Activity A启动,且属于另外一个APP4,在最近任务栏中依然只显示Task1、Task2、Task3,分别对应APP1、APP2、APP3,因为APP4的Activity C 属于Task1。
通过以上分析可以知道:新启动一个Activity时,ActivityRecord会重新创建,ActivityRecord.displayStartTime和ActivityRecord.fullyDrawnStartTime也会重新初始化,而ActivityStack.mLaunchStartTime和ActivityStack.mFullyDrawnStartTime则不会重新创建,ActivityStack唯一而ActivityRecord不唯一。
AMD截止时间戳
Activity的完整启动流程包括窗口创建、窗口显示、窗口管理。在窗口创建流程中,除了创建窗口以外还需要创建View,并通过wm.addView()的方式添加到窗口中。 在将View添加到Window的过程中会执行ViewRootImpl的setView()方法,最终完成View的添加。这一过程完成后会调用AMD时间的计算方法reportLaunchTimeLocked(),并最终打印出AMD时间,整个流程如图11所示:
图 11 ActivityRecord.reportLaunchTimeLocked调用时序图
当View成功创建并添加到Window中后,此时会执行ActivityRecord.reportLaunchTimeLocked方法并打印出AMD时间:
thisTime = curTime - displayStartTime;完整的Activity启动流程包括:
1. 前一个Activity 执行onPause;
2. 当前Activity的Window创建;
3. 当前Activity的Window显示;
thisTime仅包含:前一个Activity执行onPause的时间,当前Activity Window创建( View创建、View添加到Window )的时间,而不包括当前Activity的Window显示,也就是说AMD_thisTime并不是完整的Activity启动时间。
那AMD_totalTime是否可以表示Activity启动的时间呢?在ActivityRecord.reportLaunchTimeLocked()方法中对totalTime的计算方式如下:
totalTime = stack.mLaunchStartTime != 0 ? (curTime - stack.mLaunchStartTime) : thisTime;在AMD时间执行log输出时,对thisTime和totalTime进行了比较:
if (thisTime != totalTime) { sb.append(" (total "); TimeUtils.formatDuration(totalTime, sb); sb.append(")");}只有thisTime != totalTime才会显示totalTime,也就是说通常情况下,thisTime和totalTime是相等的,那什么情况下thisTime和totalTime不相等呢?
在Android 5.1中执行MO操作时,会先调用TelecomService的CallActivity,经过一些列的处理之后才会启动InCallActivity。在CallActivity的onCreate()方法中:
protected void onCreate(Bundle bundle) {虽然CallActivity无任何界面显示且在onCreate()执行完成后即finish(),但通过processIntent()方法的后续处理去启动InCallActivity,这种情况下便会显示AMD_totalTime。也可以这样描述: 在Activity A的onCreate()方法中启动Activity B,用户最终看到的是Activity B而未曾看到Activity A的启动,这种情况下AMD时间将会显示totalTime。
super.onCreate(bundle);
processIntent(getIntent());
// This activity does not have associated UI, so close.
finish();
}
为了更为直观的表现thisTime和totalTime的区别,请参看图12和图13:
图 12 thisTime计算示意图
单独启动Activity A,AMD时间即thisTime如图12所示,因为r1.displayStartTime和s.mLaunchStartTime相等,因此Activity A的AMD thisTime和totalTime相等。如果启动Activity A时,在其onCreate()方法中启动Activity B,也即Activity A并没有显示,AMD时间表示为thisTime(+totalTime),如图13所示:
图 13 totalTime计算示意图 在前文的分析中提到,一个ActivityRecord代表一个Activity。因此不同的Activity启动时会有自己的displayStartTime,即Activity A的r1.displayStartTime和Activity B的r2.displayStartTime。但是,因为系统中只有一个ActivityStack负责管理所有的ActivityRecord,所以s.mLaunchStartTime对于Activity A和Activity B是共用的,而s.mLaunchStartTime的赋值分别在ActivityStack.setLaunchTime()和ActivityRecord.reportLaunchTimeLocked(),前者赋值后者清零。
从图13中可以看到在计算AMD时间时,thisTime指的是Activity B的时间,而totalTime指的是执行Activity A启动到Activity B窗口创建完成的总时间。因此,在这种情况下thisTime != totalTime,所以AMD时间会显示totalTime。
通过本小节的分析,AMD时间thisTime和totalTime,并非Activity完整的启动时间,仅仅包含上一个Activity onPause的时间 + 该Activity Window创建、View创建、View添加到Window上的时间。 因此,使用AMD时间来统计Activity的启动时间,这种方法是错误的。
InCallActivity启动时间统计小节
考虑到用户体验,对于InCallActivity启动时间的统计使用“用户体验时间”,即从用户点击拨号按钮开始直至InCallActivity界面完全呈现( 动画执行完毕 )。对于用户体验时间的统计,使用timestamp较为全面,当然使用Systrace则较为直观和简便,而AMD时间则不能用来进行用户体验时间统计。通过对InCallActivity启动时间的统计,可以初步定为出启动缓慢的瓶颈,从而为下一步优化打好基础。
InCallActivity启动时间优化分析
说到InCallActivity启动时间优化,也就是降低用户点击拨号按钮后看到界面的时间,即减少用户体验时间。通过前文对InCallActivity启动时间的统计,可以定为到当前InCallActivity启动时间较长的瓶颈,针对这些瓶颈进行分析和改善以达到优化的目的。
减少函数执行时间
通过timestamp、Systrace、traceview等工具,可以知道在整个MO流程中,较为耗时的操作包括:com.android.incallui启动 ( 在Android 5.1中该进程已不存在 ),BindService操作,InCallActivity布局加载。
MoveTaskToBack
在Android 5.0时InCallActivity属于com.android.incallui进程,该进程只会在MO/MT流程触发时启动,并在通话结束后通过InCallActivity的finish()方法让系统自动回收,如果在系统对该进程回收前再次触发MO/MT流程,此时InCallActivity启动较为快速。这一点也可以从InCallActivity启动时间统计中看到,非第一次启动InCallActivity时,用户体验时间较短。
每次重新创建com.android.incallui进程,势必会重新申请资源并进行界面初始化。因此,如果将com.android.incallui进行作为后台常驻进程,这样就可以节约不少时间。对InCallActivity的finish()方法修改如下:
@Override
public void finish() {
Log.i(this, "finish(). Dialog showing: " + (mDialog != null));
// skip finish if we are still showing a dialog.
if (!hasPendingErrorDialog() && !mAnswerFragment.hasPendingDialogs()) {
moveTaskToBack(true);
//super.finish();
}
}
这样修改可避免com.android.incallui重新创建,带来的效果是立竿见影的,不过随之而来的问题却也不少。因为InCallActivity没有被finish掉,所以许多资源没有进行释放,许多状态也没有复位,同时在多用户切换后执行MO/MT操作也会带来一系列的问题。最终,不建议使用该方法进行优化。
在Android 5.1中,com.android.incallui已经合入com.android.dailer,从而在用户进入Dialer界面时就已经创建好com.android.dialer进程,因此不存在频繁创建进程,Google的优化更为有效。
Prebind and Don't unbind
在InCallActivity启动时间统计小节中,MO函数执行时间表里可以看到"InCallController bind start"的耗时是753ms,在整个表格中算是耗时较多的方法之一。这是因为每次执行MO/MT流程时,如果之前没有建立过连接,那么TelecomServiceImpl的InCallController就会发起bind InCallService的操作,实际上bind的是InCallUI中的InCallServiceImpl。如果在通话过程中,发起添加通话或者新增来电时,则不会执行bind操作。
通过traceview和timestamp的分析后可以知道,如果能在MO/MT流程发起前,对InCallService进行bind操作,则可节约400ms的时间。又因在Telephony_Phone开机初始化的流程中,会调用CallsManager的构造方法,所以可在CallsManager构造方法中进行prebind操作:
mInCallController.bind();随后在InCallController的onCallRemoved()方法中屏蔽unbind()操作:
@Override public void onCallRemoved(Call call) { Log.i(this, "onCallRemoved: %s", call); if (CallsManager.getInstance().getCalls().isEmpty()) { // TODO: Wait for all messages to be delivered to the service before unbinding. //unbind(); } call.removeListener(mCallListener); mCallIdMapper.removeCall(call); }同时也屏蔽InCallController.onConnected()方法中的unbind()操作:
private void onConnected(ComponentName componentName, IBinder service) { ThreadUtil.checkOnMainThread(); Trace.beginSection("onConnected: " + componentName); Log.i(this, "onConnected to %s", componentName); //... ...省略 onAudioStateChanged(null, CallsManager.getInstance().getAudioState()); onCanAddCallChanged(CallsManager.getInstance().canAddCall()); } else { //unbind(); } Trace.endSection(); }CallsManager在Android 5.1中的初始化流程如图14所示:
图 14 Android 5.1 CallsManager初始化时序图
在Android开发中,如果一个service启动后没有销毁,则service相关资源不会得到释放,虽然节约了时间但会造成内存消耗上有所增加,这也就是“空间换时间”的做法吧。需要注意的是Android 5.0与Android 5.1在CallsManager初始化流程上有些不同,不过该修改可根据实际需求酌情合入。
减少动画执行时间
前文提到用户体验时间 = 函数时间 + 进程切换 + 动画执行,而MoveTaskToBack和Prebind针对的正是函数时间和进程切换,本小节主要分析从动画上优化InCallActivity启动。Android默认支持动画时长的调节,可以通过"设置-开发者选项(关于手机-版本号七连击)-绘图"窗口动画缩放、过渡动画缩放、动画程序时长缩放进行调节。数值越低动画执行速度越快,给用户的感觉是响应越快,动画默认值为1x。
在系统中可修改frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java的:
float mWindowAnimationScaleSetting = 0.5f; //1.0f;虽然这样修改可以将系统默认动画执行速度从1.0提高到0.5,从而缩短用户体验时间,但是, 这样修改后会导致CTS无法通过:
float mTransitionAnimationScaleSetting = 0.5f; //1.0f;
float mAnimatorDurationScaleSetting = 0.5f; //1.0f;
android.animation.cts.LayoutAnimationTest#testIsChangingLayout FAILandroid.animation.cts.LayoutAnimationTest#testIsRunning FAIL如果不需要CTS认证的项目则可以考虑修改系统默认动画执行速度值。
既然考虑通过动画执行速度来优化MO流程的用户体验时间,那自然需要知道包含了哪些动画。为了更清楚的看到各个动画,建议大家将开发者选项中的"窗口动画缩放、过渡动画缩放、动画程序时长缩"都设置到最大值即10x。MO流程从拨号到InCallActivity界面显示动画如图15所示:
图 15 MO流程InCallActivity动画展示分析拨号到InCallActivity显示流程后,可以看到包含以下动画:
DialButton点击动画
DialButton点击后会由绿色变为蓝色并从一个小圆圈开始像外围放大。但是,这并不是MO流程中包含的动画,而是当拨号盘DialpadFragment消失后,DialtactsActivity的floatingButton显示的动画。点击dialButton后会执行到DialtactsActivity的commitDialpadFragmentHide()方法
private void commitDialpadFragmentHide() {
if (!mStateSaved && !mDialpadFragment.isHidden()) {
final FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.hide(mDialpadFragment);
ft.commit();
}
mFloatingActionButtonController.scaleIn(AnimUtils.NO_DELAY);//用户显示DialtactsActivity的floatingButton动画
}
单独查看DialButton点击后的效果如图16所示:
图 16 dialButton点击动画分解图
CircularRevealActivity显示动画
CircularRevealActivity实际是专为显示动画而准备的,其从dialButton位置使用蓝色背景逐渐铺满全屏。在MO流程中,当InCallService完成bind操作时会调用InCallServiceImpl的onBind()方法:
@Override
public IBinder onBind(Intent intent) {
InCallPresenter.getInstance().setUp(
getApplicationContext(),
CallList.getInstance(),
AudioModeProvider.getInstance());
InCallPresenter.getInstance().onServiceBind();
InCallPresenter.getInstance().maybeStartRevealAnimation(intent);//启动CircularRevealActivity背景动画
return super.onBind(intent);
}
去掉DialtactsActivity的floatingButton显示动画后,CircularRevealActivity的完整动画如图17所示:
图 17 CircularRevealActivity显示动画
CallCardFragment显示动画
InCallActivity相当于一个容器,而容器中的CallCardFragment、CallButtonFragment等组件负责各个功能模块的具体实现。在InCallActivity启动过程中会调用internalResolveIntent()方法中的mCallCardFragment.animateForNewOutgoingCall()方法:
public void animateForNewOutgoingCall(final Point touchPoint,
final boolean showCircularReveal) {
//... ...省略
final Animator animator = getOutgoingCallAnimator(touchPoint,
parent.getHeight(), originalHeight, showCircularReveal);
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
setViewStatePostAnimation(listener);
}
});
animator.start();//负责显示CallCardFragment动画,如果需要移除动画则可将本行注释,将下一行解开注释
//added by seven
//setViewStatePostAnimation(listener);
}
});
}
CallCardFragment动画与CircularRevealActivity动画类似,不过多了CallButton组件显示,最终显示通话状态、通话名称以及通话控制按钮。如图18所示:
图 18 CallCardFragment显示动画
EndButton显示动画
在CallCardFragment动画执行完成后,EndButton会执行一个放大动画,并最终显示挂断按钮。EndButton的显示动画与CallCardFragment的动画有关,在CallCardFragment的动画执行完成后,调用setViewStatePostAnimation()方法:
private void setViewStatePostAnimation(OnLayoutChangeListener layoutChangeListener) {如图19所示:
//... ...省略
mPrimaryCallCardContainer.removeOnLayoutChangeListener(layoutChangeListener);
mPrimaryCallInfo.getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING);
//Seven add
//mFloatingActionButtonController.scaleIn(AnimUtils.NO_DELAY); //执行EndButton缩放显示动画
}
图 19 EndButton显示动画EndButton除了缩放显示动画之外,还有一个上滑显示动画,在CallCardFragment的onResume()方法中执行updateFabPosition()方法回调:
private void updateFabPosition() {效果如图20所示:
//... ...省略
//EndButton上滑显示动画
mFloatingActionButtonController.align(
mIsLandscape ? FloatingActionButtonController.ALIGN_QUARTER_END
: FloatingActionButtonController.ALIGN_MIDDLE,
0 /* offsetX */,
offsetY,
true);
//... ...省略
}
图 20 EndButton上滑显示动画 在完成以上动画分析后,可以根据实际情况修改动画的显示速度,或者也可以去掉部分动画。将CircularRevealActivity显示动画、CallCardFragment显示动画、EndButton显示动画去掉后如图21 所示:
图 21 去掉部分动画后InCallActivity显示效果 通过对Animation的修改,可以在视觉上减少用户响应时间。Google加入动画的目的,即为了让各个界面能够平滑过渡,因此少量的动画能够起到“承上启下”的作用,因此建议大家根据实际情况对显示动画进行修改。
总结
Android 5.0 中Google对Telephony Phone模块进行了重构,再加上芯片厂商(Qcom/MTK等)对AOSP代码的修改,使得拨号操作中InCallActivity响应较慢。有时候用户点击拨号按钮之后需要好几秒钟才能看到通话界面,这样的用户体验实在糟糕。
为此,遂计划针对分析InCallActivity启动较慢的原因并进行相关优化,随着分析的深入我大胆的做了一个猜想:能否在bind InCallService成功时就启动InCallActivity呢?此时InCallActivity界面上的内容设置为一个默认值,等Dial成功后将状态返回到InCallActivity中进行更新即可。正准备着手实行时Android 5.1悄然发布,在仔细扒拉代码后发现,Google的工程师使用了类似的方法,即在bind InCallService成功后启动CircularRevealActivity作为背景,避免用户过长的等待。除了对流程的优化和动画的修改,还可以优化InCallActivity的布局,使其加载更快速。
在整个分析过程中遇到了很多困惑的问题,也得到了不少帮助。其中Google工程师Yorke Lee很耐心的解释了为何Google不做Prebind的操作:
可以看到Google是基于内存的考虑才没有做prebind和don't unbind。
在分析开始前制定一份分析计划,计划中可以包含子计划,整个过程就好像拼图游戏,不停的寻找缺失部分,最终实现整个计划。分析过程中遇到了各种各样的问题,有的可以通过网络获取信息,有的则只能靠自己阅读源码加以理解,完成分析后为了避免后续遗忘,同时也为了帮助他人,遂记录此文。
文中涉及图片资源下载:戳这里