转载

应用首页 Activity 的单例实现

背景

目前有一部分android APP需要这样一种场景,即应用需要保留一个应用首页主Activity,其它子Activity永远在主Activity之上,跳转到子Activity之后,不管以哪种方式跳转,最终都可以返回到主Activity,这种场景有点类似主桌面的概念。这种场景如果纯fragment来实现,需要管理fragment栈,中间如果发生嵌套跳转,fragment栈的管理会变得非常复杂,所以难免会需要使用部分Activity来实现,并且由于主Activity承载的内容比较丰富,初始化会比较耗时,因此要尽量复用已初始化的Activity。

而不管怎么实现,需要的是始终保证只有一个主Activity,对于fragment的实现这里不发散,讨论下如何实现保证只初始化一个主Activity。

主Activity启动模式的选择

看下android中Activity的launchMode,关于这方面的介绍总结资料很多,这里简单说明:

  • standard: 每次启动都会创建
  • singleTop:跟 Standard 类似,当Activity在栈顶时复用
  • singleTask:一个栈只保持一个实例,并且会在重新启动Activity时清掉栈顶其它Activity
  • singleInstance: 独享一个任务栈

上面四种启动模式,使用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。

将这个中转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恢复到前台

当然,这样实现还不能达到最终的效果,先来分析下会有什么问题。从以上的代码不难看出,正常第一次跳转结果正常,但第三方可以做了一次跳转之后,又切回第三方应用再做一次跳转,我们来模似下看会有什么情况

应用首页 Activity 的单例实现

这就是上面提到的MainActivity存在task中的情况下,会复用task。这是重复从第三方跳转到app中的过程。

另外我们看下从系统主界面跳到mainActivity然后启动子Activity,再从第三方跳转到子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 的单例实现

其它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

原文  http://mp.weixin.qq.com/s?__biz=MzI1NjEwMTM4OA==&mid=2651231865&idx=1&sn=54f0d5225f744bf6935a5fe36594a6d0&scene=0
正文到此结束
Loading...