对线上项目拉起应用场景的思考总结

背景

最近大神项目在做聊天迁移,把原来云信的聊天模块功能替换成 ngpush支持的聊天模块功能。刚好小版本要上一个 “极速版将军令” 的大神,其功能是在启动的时候通过用户配置切换不同的页面分支。如果用户配置了开启极速版,则拉起 “将军令” 页面。否则拉起主页。

问题来源

一开始新增一个 LauncherActivity 用于中转不同场景的切换,原旧版本主页面处理任何意图(intent)的逻辑将需要从 LauncherActivity 进行 “继承” 并处理。这意味着 LauncherActivity 收到任何意图之后处理的逻辑需要从主页面代码中拷贝过来。一些第三方推送比如小米,华为在进程被杀死的情况下进行消息推送拉起,其意图没有携带任何业务逻辑。 另外,当app处于将军令页面时,收到一些推送需要直接跳转到原二级页面,一级主页面此时并没有初始化依赖,这会造成很多隐藏的风险!

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

解决问题

大部分问题都是由于原 app 已启动主页面就进入初始化流程,不存在任何依赖问题。所以不采用 LauncherActivity,而是在原主页面 onCreate() 在开头判断是否需要拉起 “将军令”。这样的好处,先拉起的将军令可以快速显示,同时主页面流程继续走。一些token校验,登陆信息等都可以通过异步回调返回各自页面。通过这种方案可以维持主页面原来处理意图的逻辑。

针对第三方推送,华为/小米/魅族等rom都开放了自己的推送sdk,不同的rom经过测试发现: “当 app 没有被启动情况下,通过点击推送构建的通知栏拉起应用的回调不一,有些甚至没有回调。”

下面是经过测试并总结的一些拉起场景。
由于被拉起的主页面的意图一般包含

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

所以所有拉起场景只能通过意图的其他信息来区分,经调试, flag 为目前最有效的切入口。大部分 FLAG_ACTIVITY_* 为页面启动时携带标示。把拉去场景大致区分为:

  • 安装之后点击打开拉起

    1
    2
    FLAG_ACTIVITY_NEW_TASK
    FLAG_RECEIVER_FOREGROUND
  • 点击桌面图标拉起

    1
    2
    3
    4
    FLAG_ACTIVITY_NEW_TASK
    FLAG_RECEIVER_FOREGROUND
    FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
    FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS
  • 第三方推送在app被完全杀死前提下拉起

    1
    2
    3
    4
    5
    FLAG_ACTIVITY_NEW_TASK
    FLAG_RECEIVER_FOREGROUND
    FLAG_ACTIVITY_SINGLE_TOP
    FLAG_ACTIVITY_REORDER_TO_FRONT
    FLAG_RECEIVER_REPLACE_PENDING
  • app在后台被拉起
    不同推送sdk可能出现不一样的情况

    onNewIntent中回调

    1
    2
    3
    4
    5
    6
    7
    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中回调

    1
    2
    3
    4
    5
    6
    7
    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_TASKFLAG_RECEIVER_FOREGROUND 这两个 flag.而符合这两个条件的优先判断是否同时包含 FLAG_ACTIVITY_RESET_TASK_IF_NEEDEDFLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS。其次再判断被动冷启动,和被动热启动。下面封装了一段代码可以直接使用(持续优化更新).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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().

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@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 文档定位到最终是经过广播传递意图来启动的。既然是有意图,那必定可以解析意图来寻找多个场景的差异化。解析了 Action/Bundle/Categories 无法找到差异化,这也是以前同事的做法。但是多场景拉起,经过系统的分发之后必定应该存在能区分场景差异的内容,最终在所有接收意图的场景下解析所有 Flags,并分析每个场景的 Flags差异及参考源码注释解析来尝试,最终不负一番苦心 Fix 了这个历史痛点。

想想上个让我彻夜难眠的 BUG 还是去年年末。一直觉得 “系统是人设计的,代码是人写的,你想到的问题设计者应该也能想到,既然如此,问题出在代码,答案也应该在代码里面”。这个想法是我写代码以来处理任何问题的背后念头。有些问题,可能只是自己的知识面不足,又或许是解决问题的方向一开始就不对,只要有足够的时间给我思考,必定能解决遇到的难题!