背景
目前有一部分android APP需要这样一种场景,即应用需要保留一个应用首页主Activity,其它子Activity永远在主Activity之上,跳转到子Activity之后,不管以哪种方式跳转,最终都可以返回到主Activity,这种场景有点类似主桌面的概念。这种场景如果纯fragment来实现,需要管理fragment栈,中间如果发生嵌套跳转,fragment栈的管理会变得非常复杂,所以难免会需要使用部分Activity来实现,并且由于主Activity承载的内容比较丰富,初始化会比较耗时,因此要尽量复用已初始化的Activity。
而不管怎么实现,需要的是始终保证只有一个主Activity,对于fragment的实现这里不发散,讨论下如何实现保证只初始化一个主Activity。
主Activity启动模式的选择
看下android中Activity的launchMode,关于这方面的介绍总结资料很多,这里简单说明:
上面四种启动模式,使用standard与singleTop不符合要求,singleTask与singleInstance可以保证一个主Activity,但这两模式存在一个问题:从主Activity跳到子Activity后,按home键回要主桌面,再从桌面应用图标启动应用,会发现重新回到了主Activity。虽然可以保证主Activity单例,但是能恢复到子Activity才是我们想要的用户体验。
从上面的场景分析,singleTask与singleInstance不适合作为主Activity的启动模式,standard每次启动都会创建,也不适合,所以只能选择singleTop,使用这种模式,存在几个问题:
1.除了从系统主界面启动应用之外,第三方应用也可以通过Intent启动应用,Intent.Flag参数的设置变得不可控制
2.第三方应用可以随意启动主Activity之外的子Activity
3.当主Activity之上有子Activity存在的情况下,启动时还是会重新创建主Activity。
引入统一处理跳转的Acitivity
为了解决以上三个问题,我们加入专门用来处理跳转请求的Activity,该acitivity主要作用:
1.统一处理外部跳转的请求,规范外部跳转协议
2.统一内部Activity跳转逻辑,并且内部Activity跳转不受第三方跳转影响
3.保证主桌面模式的实现,如控制任务栈恢复,栈顶Activity清除
为了实现可以返回主Activity功能,外部跳转的大概流程为:
这样从最后的Activity返回,可以回到主Activity。
将这个中转Activity 命中为DispacherActivity,看下实现代码
public class DispacherActivity extends Activity { private String TAG = "launcher_test_DispacherActivity"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); startRouter(); finish(); } private void startDispacher() { Log.i(TAG, " startDispacher "); Intent it = new Intent(); it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); it.setClass(this, MainActivity.class); setIntentInfo(it); startActivity(it); } private void setIntentInfo(Intent it) { String toActivity = getIntent().getStringExtra("toActivity"); int dumpTo = DISPACHER_ACTIVITY_MAIN; if (!TextUtils.isEmpty(toActivity)) { if (toActivity.equals("2")) { dumpTo = DISPACHER_ACTIVITY_SECOND; } else if (toActivity.equals("3")) { dumpTo = DISPACHER_ACTIVITY_THIRD; } } it.putExtra(DISPACHER_PARAM_ACTIVITY, dumpTo); } }
主Activity部分处理代码:
public class MainActivity extends Activity { private static String TAG = "launcher_test_MainActivity"; public static WeakReference<MainActivity> instanceOfMainActivity = null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.i(TAG, " onCreate " + this); doAction(); } }
DispacherActivity获取Intent中的跳转参数,并且将参数转成MainActivity的参数,DISPACHER_ACTIVITY_SECOND表示在MainActivity中转到第二个Activity。
DispacherActivity代码中可以看到,在跳转到主Activity时,Intent的flag设置 为FLAG_ACTIVITY_NEW_TASK,该flag的相关介绍可以到 官网查询 ,这里主要的作用有两个:
1.总是保持MainActivity在一个新的task中运行,而不会与启动它的第三方应用在同一个任务栈中
2.如果MainActivity已经存在task中,则复用该task,并且将task恢复到前台
当然,这样实现还不能达到最终的效果,先来分析下会有什么问题。从以上的代码不难看出,正常第一次跳转结果正常,但第三方可以做了一次跳转之后,又切回第三方应用再做一次跳转,我们来模似下看会有什么情况
这就是上面提到的MainActivity存在task中的情况下,会复用task。这是重复从第三方跳转到app中的过程。
另外我们看下从系统主界面跳到mainActivity然后启动子Activity,再从第三方跳转到子Activity
这里需要注意:startActivity时Intent参数有可以设置三个属性:action,category,data, 当这三个属性任何一个值变化,都会导致不能恢复任务栈,而是重新创建新的Activity。
当从第三方应用重复跳转时,虽然Bundle的值有改动,这三个值并没有变化,因此会直接恢复到当前任务栈;当从系统启动应用时,Intent的category设置是android.intent.category.LAUNCHER,第三方startIntent时,没有设置Intent的category属性,默认值为android.intent.category.DEFAULT,因此会重新创建新的Activity。所以这里需要将Intent的category设置成 android.intent.category.LAUNCHER,保证不管从第三方应用还是从系统启动,都能够正常恢复任务栈。
上面只是初步解决了category属性的问题,对于action,也可以设置成与系统相同的启动方式。而使用Data的跳转方式,由于Data的跳转比较难以统一,所以不能保证恢复任务栈。从上面的实现我们知道,外部跳转是需要先通过DispacherActivity,再由DispacherActivity跳转到主Activity的,因此,对开放的Data协议,可以由DispacherActivity接收处理后,再转换成主Activity的跳转参数,这样就可以解决Data方式的跳转问题。通过修改后,跳转函数startDispacher代码为:
private void startDispacher() { Log.i(TAG, " startDispacher "); Intent it = new Intent(); it.setAction(Intent.ACTION_MAIN); it.addCategory(Intent.CATEGORY_LAUNCHER); it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); it.setClass(this, MainActivity.class); setIntentInfo(it); startActivity(it); }
前面又提到,如果action与category相同的情况下,只会恢复已有的任务栈;所以这时从要主Activity已经有子Activity 的情况下,再次跳转又会恢复任务栈,无法正常进行跳转;
这种情况的解决方式有两种:
1.使主Activity具备singleTask的功能,再次跳转时清除栈顶Activity再重新创建新的Activity;
2.判断当前是否需要再次通过主Activity跳转,如果不需要通过主Activity,则直接启动目标Activity
我们知道,Intent在跳转时可以设置多个Flags,想要清除栈顶Activity,只需要加上FLAG_ACTIVITY_CLEAR_TOP即可达到launcherMode的singleTask效果。
因此第1种方式实现的比较简单,不需要处理任务栈各种状态,坏处是每次跳转都会清掉栈顶Activity,有些场景可能不能满足;第1种方式虽然可以保持栈顶Activity,但实现复杂,各种跳转需求可能有可能不一样,所以不太推荐。当然,也可以主要采用第1种处理方式,适当加上第2种方式的逻辑。
再次修改后跳转函数startDispacher代码为:
private void startDispacher() { Log.i(TAG, " startDispacher "); Intent it = new Intent(); it.setAction(Intent.ACTION_MAIN); it.addCategory(Intent.CATEGORY_LAUNCHER); it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_CLEAR_TOP); it.setClass(this, MainActivity.class); setIntentInfo(it); startActivity(it); }
至此,从主桌面启动用singleTop的launcherMode,从第三方跳转用CLEAR_TOP的flag,category始终为android.intent.category.LAUNCHER,这样就可以保证不管以哪种方式启动,都只有一个主Activity,在主Activity复用时,注意处理onNewIntent,注意将intent保存下来:
@Override public void onNewIntent(Intent intent) {
super.onNewIntent(intent); Log.i(TAG, " onNewIntent" + this); setIntent(intent); doAction(); }
这里还需要注意的一个问题DispacherActivity的launcherMode,虽然DispacherActivity每次处理跳转之后都会finish掉,但为了不影响主Activity的任务栈,推荐使用singleInstance启动。
重复初始化拦截
从前面处理,已经解决了主界面启动和第三方跳转的问题,但这里还存在一个隐患:假设第三方直接使用默认category属性来启动主Activity呢?这时与主界面和DispacherActivity启动的category不一致,又回到前面重复创建主Activity的场景。这种情况并不好控制,所以需要所技术上解决该问题。
我们知道,重新创建Activity并且将Ativity添加到栈顶时,需要将该任务栈带到前台,也就是说,如果从第三方跳转到主Activity,会将我们的应用切到前台,同时创建Activity;为了保证只有一个主Activity,在onCreate中做以下处理
public static WeakReference<MainActivity> instanceOfMainActivity = null; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Log.i(TAG, " onCreate " + this); if(!isCreated(this)){ setContentView(R.layout.activity_main); doAction(); } } private static boolean isCreated(MainActivity activity ){ if (instanceOfMainActivity != null && instanceOfMainActivity.get() != null) {//注意处理MainActivity已经finish或destroy但对象没被回收的情况 Intent it = activity.getIntent(); MainActivity act = instanceOfMainActivity.get(); act.onNewIntent((Intent) it.clone()); activity.finish(); return false; } else { instanceOfMainActivity = new WeakReference<MainActivity>(activity); return true; } }
从以上代码看到,首次onCreate将主Activity保存下来,如果重复创建,则将新创建的Activity finish掉,并且调用已有Activity的onNewIntent进行跳转,以达到主Activity不被重复创建的目的。需要注意:虽然主Activity保证一次初始化,但不排除它的生命周期已经结束,但却没被回收的情况,所以要注意加上处理。
通过处理后,关键流程如下:
其它Activity启动参数
1.为了保证子Activity不被第三方直接调用,exported应该设置成false
2.为了保证任务栈顺序,如果没有特殊的场景,不应该设置成singleInstance和singleTask
其它属性设置
横竖屏属性:
从上面的实现,主Activity在onCreate中会拦截初始化,因此在注意Activity横竖屏切换,最保险的方式是只支持竖屏显示,将screenOrientation设置为portrait;如果想支持横竖屏功能,需要将configChanges设置成 orientation|keyboardHidden|screenSize以避免重复初始化主Activity。
以下属性说明见 官网说明 ,这里简单列下设置
taskAffinity 默认设置
alwaysRetainTaskState 设置成true,避免退到后台过久子Activity被系统自动回收
allowTaskReparenting 看应用场景,一般都设置成true即可
clearTaskOnLaunch 设置成false
finishOnTaskLaunch 设置成false
总结
1.主Activity承载了主桌面功能,从第三方跳转到子Activity需要先启动主Activity;
2.主Activity需要保证只初始化一次,但又不能使用singlgeTask和singleInstance的启动模式;
3.外部跳转请求要统一经过中转Activity来处理,重复跳转时,Action、Category、Data相同情况下会直接恢复任务线导致不能处理跳转参数;
4.从中转Activity跳到主Activity,需要将Action、Category、Data设置成与系统启动应用相当方式,并FLAG_ACTIVITY_NEW_TASK|FLAG_ACTIVITY_CLEAR_TOP达到清除栈顶Activity并且复用主Activity的目的;
5.为了避免主Activity重复初始化,可以在onCreate中拦截初始化,并重复已存在的主Activity;
6.除主Activity外,其它Activity应当慎用singlgeTask和singleInstance的启动模式;
7.注意处理主Activity横竖屏切换问题。
1.从第三方跳转到一个子Activity时,总时会先初始化主Activity,如果主Activity未先初始化,会导致跳转等待时间过长;
2.每次跳转都需要先初始化DispacherActivity,会额外增加100-200ms耗时,由于该Activity是singleInstance的启动模式,可以创建后不finish