Android上唤起 APP 场景的思考总结

背景

恰逢项目小版本要上一个 极速版轻应用内嵌于主端 APP 内,在 APP 启动时通过读取配置决定是否优先显示轻应用界面。因此,在启动 APP 时需要实时进行不同场景切换。

问题来源

一开始我们通过新增一个 LauncherActivity 用于中转不同场景的切换,原 APP 主页面处理任何 Intent 的逻辑将需要从旧启动页进行 “继承” 处理。这意味着 LauncherActivity 收到任何 Intent 处理的逻辑需从旧启动页代码中拷贝。在继承 LauncherActivity 之后我们发现 一些第三方推送比如小米,华为在进程被杀死的情况下点击通知栏拉起 APP,其 Intent 并没有携带任何业务逻辑进而导致无法精准跳转业务页面。 同时当 APP 处于轻应用页面时收到推送需直接跳转到原主端二级页面时,这些页面的依赖还没有被主端初始化进而导致 Crash

在该版本做 MTL 兼容测试和业务回归测试时发现:LauncherActivity Intent 处理上存在很多小细节问题,频繁的 “提单-修改-测试-回归” 的成本很大。在经过权衡之后决定不采用 LauncherActivity 方案来解决新版本调整导致问题,同时研究细化 Intent 类型来解决第三方推送通知时无法监听点击通知栏消息的问题。

解决方案

大部分问题都是因为原启动流程发生更改导致的,故不采用 LauncherActivity,而是在旧主页面 onCreate方法 在开头判断是否需要拉起 极速版轻应用 。这样的好处是:先拉起轻应用可以快速显示同时主页面流程继续走,比如一些 token 校验,登陆信息等行为。

由于第三方推送 华为/小米/魅族 等都开放了各自推送 SDK,不同的 ROM 经过测试发现:当 app 没有被启动情况下点击通知栏推动信息拉起应用时回调信息不一,有些甚至没有回调

下面是经过测试并总结一些拉起场景。

由于被拉起的主页面的 Intent 一般包含以下信息

<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />

所有拉起场景只能通过 Intent 的其他信息来区分。经调试, FLAG 为目前最有效的切入口。页面启动时携带标示 FLAG_ACTIVITY_XXX。把拉起场景大致区分为 4 种场景。

安装之后点击打开拉起

FLAG_ACTIVITY_NEW_TASK
FLAG_RECEIVER_FOREGROUND

点击桌面图标拉起

FLAG_ACTIVITY_NEW_TASK
FLAG_RECEIVER_FOREGROUND
FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS

第三方推送在 APP 被完全杀死前提下拉起

FLAG_ACTIVITY_NEW_TASK
FLAG_RECEIVER_FOREGROUND
FLAG_ACTIVITY_SINGLE_TOP
FLAG_ACTIVITY_REORDER_TO_FRONT
FLAG_RECEIVER_REPLACE_PENDING

APP 在后台被拉起,但不同推送 SDK 可能出现回调不一样的情况

onNewIntent方法 中回调

FLAG_ACTIVITY_NEW_TASK
FLAG_RECEIVER_FOREGROUND
FLAG_RECEIVER_REPLACE_PENDING
FLAG_ACTIVITY_SINGLE_TOP
FLAG_ACTIVITY_CLEAR_TOP
FLAG_RECEIVER_REPLACE_PENDING
FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT

onCreate方法 中回调

FLAG_ACTIVITY_NEW_TASK
FLAG_RECEIVER_FOREGROUND
FLAG_RECEIVER_REPLACE_PENDING
FLAG_ACTIVITY_SINGLE_TOP
FLAG_RECEIVER_FROM_SHELL
FLAG_ACTIVITY_BROUGHT_TO_FRONT
FLAG_ACTIVITY_REORDER_TO_FRONT

从上述4个场景中可知 任意一次拉起都会同时包含 FLAG_ACTIVITY_NEW_TASK 和 FLAG_RECEIVER_FOREGROUND。在符合这两个条件我们选择优先判断是否同时包含 FLAG_ACTIVITY_RESET_TASK_IF_NEEDEDFLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS,其次再判断是否为 被动冷启动被动热启动

下面封装了一段代码可以直接使用。

public enum AppStartType {
      NONE, //不属于拉起
      AFTER_INSTALL, //安装后启动
      LAUNCHER, //Launcher启动
      COOL_BE_OPEN, //被动冷拉
      HOT_BE_OPEN //被动热拉
}

@NonNull
public static AppStartType parseStartType(Intent intent) {
    parseIntent(intent, "打印intent");
    if (intent != null) {
        int flags = intent.getFlags();
        if ((flags & FLAG_ACTIVITY_NEW_TASK) == FLAG_ACTIVITY_NEW_TASK && (flags & FLAG_RECEIVER_FOREGROUND) == FLAG_RECEIVER_FOREGROUND) {
            flags = flags ^ FLAG_ACTIVITY_NEW_TASK ^ FLAG_RECEIVER_FOREGROUND;
            if (flags != 0) {
                if ((flags & FLAG_ACTIVITY_RESET_TASK_IF_NEEDED) == FLAG_ACTIVITY_RESET_TASK_IF_NEEDED && (flags & FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS) == FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS) {
                    return AppStartType.LAUNCHER;
                } else if ((flags & FLAG_RECEIVER_REPLACE_PENDING) == FLAG_RECEIVER_REPLACE_PENDING) {
                    if (((flags & FLAG_ACTIVITY_CLEAR_TOP) == FLAG_ACTIVITY_CLEAR_TOP && (flags & 0x04000000) == 0x04000000) || (flags & 0x00400000) == 0x00400000) {
                        return AppStartType.HOT_BE_OPEN;
                    }
                    if ((flags & FLAG_ACTIVITY_REORDER_TO_FRONT) == FLAG_ACTIVITY_REORDER_TO_FRONT) {
                        return AppStartType.COOL_BE_OPEN;
                    }
                    return AppStartType.NONE;
                } else {
                    return AppStartType.NONE;
                }
            } else {
                return AppStartType.AFTER_INSTALL;
            }
        }
    }
    return AppStartType.NONE;
}

值得注意的是,被动热拉 在拉起主页面不同厂商的 SDK 回调不一样,因此还需要在 onCreate方法 或者 onNewIntent方法 判断下。

@Override
protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 防止安装完直接点击打开然后Home键回到桌面再点击图标导致的多个实例
        // (getIntent().getFlags() & Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) != 0
        if (!isTaskRoot()) {
                ActivityUtils.AppStartType startType = ActivityUtils.parseStartType(getIntent());
                if(startType == ActivityUtils.AppStartType.HOT_BE_OPEN){
                        //处理业务逻辑
                }
                finish();
                return;
        }
        ......
 }

         @Override
protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        ActivityUtils.AppStartType startType = ActivityUtils.parseStartType(getIntent());
        if(startType == ActivityUtils.AppStartType.HOT_BE_OPEN){
                    //处理业务逻辑
        }
}            

思考

上述遇到的问题是在迭代极速版本时到的,但实际上该问题早就存在于项目中,只是刚好在新业务加入时问题被放大到不得不解决的程度。组内其他同事也尝试过解决,但未果,最后缺陷单落在我这里。
一直觉得 “系统是人设计的,代码是人写的,你想到的问题设计者应该也能想到,既然如此,问题出在代码,答案也应该在代码里面”。 通过问题的表象追踪到推送 SDK 的技术文档对比,到调试不同 ROM最终锁定由 Intent 拉起差异化 导致的结果。既然是 Intent,那必定可解析 Intent 来寻找多个场景的差异化,既然解析了 Action/Bundle/Categories 无法找到差异点,就尝试源码是否还有更多信息提示,在 Flags 方向上尝试,解析出每个场景对应的 Flags 并参考源码注释来分析,最终不负一番苦心解决了这个问题。

想想上个让我彻夜难眠的 BUG 还是去年年末。这个想法是我写代码以来处理任何问题的背后念头。有些问题,可能只是自己的知识面不足,又或许是解决问题的方向一开始就不对,只要有足够的时间给我思考,必定能解决遇到的难题!

如果文章对你帮助,欢迎点赞评论。

欢迎关注同名公众号 Android之禅 - 专注 Android 进阶技术分享,记录架构师野蛮成长之路

file