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

背景

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

问题来源

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

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

解决方案

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

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

下面是通过测试并总结一些拉起场景。职业规划

因为被拉起的主页面的 Intent 通常包含如下信息设计

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

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

  1. 安装以后点击打开拉起code

    FLAG_ACTIVITY_NEW_TASK
    FLAG_RECEIVER_FOREGROUND
  2. 点击桌面图标拉起继承

    FLAG_ACTIVITY_NEW_TASK
    FLAG_RECEIVER_FOREGROUND
    FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
    FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS
  3. 第三方推送在 APP 被彻底杀死前提下拉起

    FLAG_ACTIVITY_NEW_TASK
    FLAG_RECEIVER_FOREGROUND
    FLAG_ACTIVITY_SINGLE_TOP
    FLAG_ACTIVITY_REORDER_TO_FRONT
    FLAG_RECEIVER_REPLACE_PENDING
  4. 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之禅」公众号,和你分享有价值有思考的技术文章。 可添加微信 「Ming_Lyan」备注 “进群” 加入技术交流群,讨论技术问题严禁一切广告灌水。 若有 Android 领域有遇到技术难题亦或对将来职业规划有疑惑,一块儿讨论交流。 欢迎来扰。
相关文章
相关标签/搜索