借款App一直比较关注启动流程的高效与稳定,启动流程承载的不止是用户体验的第一场景,更是一款产品技术形象的初始印象。
传统的App启动流程主要经过应用创建,闪屏页倒计时,然后再进入主页(如下图A方案)。借款App最早也是这么做的,启动应用后会先进入一个广告闪屏页,然后再跳转至借款主页。但是闪屏页跳转至主页的时候有割裂感,有时主页尚未渲染完成,用户体验不太好。
我们希望倒计时结束进入主页的时候,页面给用户一种迸发而出的感觉,提升用户体验。
解决这个问题有一个简单的思路:把闪屏Activity与主Activity合并,闪屏页作为Fragment挂载到主页,利用广告倒计时的时间去加载主页,当倒计时结束,主页Fragment已经请求好数据并渲染完成,给用户一种更好的视觉体验。
但是闪屏类PPDSplashActivity仍然不能去除,除了一些业务逻辑之外,更重要的是PPDSplashActivity一旦去掉会引起Activity栈异常,而且还会引起多实例的bug。
所以相当长的一段时间PPDSplashActivity是以一个透明Activity的方式存在(如下图B方案),App启动后先打开PPDSplashActivity后立马finish掉自己,再跳转至主页。
启动速度是指从创建applicaiton开始,一直到首个Activity第一个可视画面展现出来。
但是上述B方案中的PPDSplashActivity是透明的并无画面展示,所以启动速度的统计就变成了PPDHomeActivity 首个画面展示出来。可想而知这个方案的启动速度并不快,体验差强人意。
在启动流程多执行一次Activity的创建与销毁工作,对整个启动流程来说也是一种拖累,我们希望可以去除闪屏页直接启动主页(如下图C方案)。
本文主要介绍下,最终是如何啃下这块硬骨头,去除掉这个多余的PPDSplashActivity,优化启动流程的。
PPDSplashActivity虽然已经没有视图显示了,但是它还承担了隐私协议,权限相关业务逻辑。
这些逻辑需要平移至PPDHomeActivity。push,deeplink数据不再通过PPDSplashActivity透传,打开主页时直接从intent获取。
这样PPDSplashActivity被彻底掏空,为去除做好准备工作。测试通过再进行下面的工作。
将 PPDSplashActivity删除以后,我们的 launcher Activity 变成了 PPDHomeActivity,如果继续使用 singletask 这个 launchMode,当我们App从主页打开二级页面,然后点击 home键出去,再次点击手机桌面icon进入App时,我们将无法回到刚才打开的二级页面,而会回到主页,因为singletask具有清栈功能,因此合并后 PPDHomeActivity 的 launch mode 将不再能够使用 singletask。
经过调研,我们最终选择了使用 singletop 作为主页Activity的 launchMode。但用singletop模式又会引起多实例的问题,即有可能会打开两个首页。
应用内部跳转引起打开多个主页的问题,可以在启动 PPDHomeActivity 的 Intent 增加 FLAG_ACTIVITY_NEW_TASK 与 FLAG_ACTIVITY_CLEAR_TOP 的 flag,以实现类似于 singletask 的 clear top 的特性。
这个比较好解决。但是应用外引起的多实例,比如我现在已经打开了App,通过push短链唤起App时又打开了一个,缺少了PPDSplashActivty做屏障,这种外部多实例问题真的很棘手。
要避免同时出现多个 PPDHomeActivity 对象,我们首先需要知道当前是否已经存在 PPDHomeActivity 对象,解决这个问题的思路比较简单,我们可以去监听 PPDHomeActivity 的生命周期,在 PPDHomeActivity 的 onCreate 和 onDestroy 中分别去增加减少 PPDHomeActivity 的实例数。如果 PPDHomeActivity 实例数为 0 则认为当前不存在 PPDHomeActivity 对象。
解决了 PPDHomeActivity 对象数统计的问题,接下来我们就需要让 PPDHomeActivity 同时存在的对象数永远保持在 1 个以下。
要解决这个问题我们需要回顾一下 Activity 的启动流程,启动一个 Activity 首先会经过 AMS,AMS 会再调用到 Activity 所在的进程,在 Activity 所在的进程会经过主线程的 Handler post 到主线程,然后通过 Instrumentation 去创建 Activity 对象,以及执行后续的生命周期。对于外部启动 PPDHomeActivity ,我们能够控制的是从 AMS 回到进程之后的部分,这里可以选择以 Instrumentation 的 newActivity 作为入口。
1:反射获取 ActivityThread 中 Instrumentaion 对象,并以其为参数创建一个自定义的 Instrumentaion 对象,通过反射方式用自定义的 Instrumentaion 对象替换 ActivityThread 原有的 Instrumentaion;
public class PPDHomeHooker {
private static final String TAG = "Hooker";
public static void hookInstrumentation() {
Class<?> activityThread = null;
try {
activityThread = Class.forName("android.app.ActivityThread");
Method sCurrentActivityThread = activityThread.getDeclaredMethod("currentActivityThread");
sCurrentActivityThread.setAccessible(true);
//获取ActivityThread 对象
Object activityThreadObject = sCurrentActivityThread.invoke(activityThread);
//获取 Instrumentation 对象
Field mInstrumentation = activityThread.getDeclaredField("mInstrumentation");
mInstrumentation.setAccessible(true);
PPDInstrumentation customInstrumentation = new PPDInstrumentation();
//将我们的 customInstrumentation 设置进去
mInstrumentation.set(activityThreadObject, customInstrumentation);
} catch (Exception e) {
e.printStackTrace();
}
}
}
2:继承 Instrumentation 实现一个自定义的 Instrumentaion 类,以代理转发方式重写里面的所有方法;
public class PPDInstrumentation extends Instrumentation {
public PPDInstrumentation() {
}
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
if (className.equals(PPDHomeActivity.class.getName()) && PPDaiApplication.homeActivityCount > 0) {
return super.newActivity(cl, PPDSplashActivity.class.getName(), intent);
}
return super.newActivity(cl, className, intent);
}
}
3:在自定义 Instrumentaion 类的 newActivity 方法中,进行判断当前待创建的 Activity 是否为 PPDHomeActivity,如果不是 PPDHomeActivity 或者当前不存在 PPDHomeActivity 对象,则调用原有实现,否则替换其 className 参数将其指向一个空的 Activity,以创建一个空的 Activity;
在这个空的 Activity 的 onCreate 中 finish 掉自己,同时通过一个添加了 FLAG_ACTIVITY_NEW_TASK 和 FLAG_ACTIVITY_CLEAR_TOP flag 的 Intent 去启动一下 PPDHomeActivity。
通过外链启动App的整体流程改造如下:
public class PPDEmptyActivity extends AppCompatActivity {
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
finish();
}
}
至此,通过层层关卡后,终于啃下这块硬骨头,闪屏Activity被彻底删除,启动流程变的更简洁也更合理。该方案上线以后启动速度从1.5s降低至1s左右,实现了App秒开,慢启动占比也有显著下降,得益于启动流程的整体优化,App的卡顿率也从0.71%也降低至0.35%。
Star,移动研发专家
Java、大数据、前端、测试等各种技术岗位热招中,欢迎扫码了解~