应用首页Activity的单例实现

时间:2022-04-25
本文章向大家介绍应用首页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 命中为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