美文网首页四大组件知识点Android开发经验谈
005 四大组件-Activity-启动模式问题

005 四大组件-Activity-启动模式问题

作者: 凤邪摩羯 | 来源:发表于2021-05-17 09:25 被阅读0次

    背景

    最近在做App的启动优化,为了达到快速启动的效果,将我们的App的闪屏页(SplashActivity显示固定图片)移除掉,换成MainActivity的背景(windowBackground),最后再替换成App的主题,给用户快速响应的体验。

    <style name="AppWelcomeTheme" parent="BaseAppTheme">
            <item name="android:windowBackground">@drawable/flash_bg</item>
    </style>
    //flash_bg.xml
    <?xml version="1.0" encoding="utf-8"?>
    <layer-list xmlns:android="http://schemas.android.com/apk/res/android">
        <item>
            <shape>
                <solid android:color="#fff" />
            </shape>
        </item>
        <!--底层使用蓝色填充色-->
        <item
            android:gravity="center"
            android:top="60dp">
            <bitmap
                android:layout_width="100dp"
                android:layout_height="100dp"
                android:src="@drawable/ic_splash_logo" />
        </item>
    </layer-list>
    复制代码
    

    AndroidManifest.xml

    <activity
                android:name=".ui.main.MainActivity"
                android:theme="@style/AppWelcomeTheme"
    复制代码
    

    这样一个 MainActivity 启动的时候,就会先显示一个预览窗口,给用户快速响应的体验。当 activity想要恢复原来 theme,可以通过在调用super.onCreate()setContentView()之前调用 setTheme(R.style.AppTheme),如下:

    public class MyMainActivity extends AppCompatActivity {
      @Override
      protected void onCreate(Bundle savedInstanceState) {
        // Make sure this is before calling super.onCreate
        setTheme(R.style.AppTheme);
        super.onCreate(savedInstanceState);
        // ...
      }
    }
    复制代码
    

    但是却优化出了问题,我们的MainActivity使用的启动模式是SingleTask,我将闪屏页去掉后,无论打开多少页面,将应用推至后台再启动就回到了主页(MainActivity),这是个很严重的问题,还好发现的及时。

    问题排查

    排查问题的时候,先看看之前的版本有没有该问题(并没有发现问题),再查看我的代码提交记录,发现AndroidManifest.xml中我主要做的修改去移除了闪屏界面,点击App直接启动的是主页MainActivity,但是坑爹的是我还以为是引入dynamic link带来的问题,还以为动态链需要从启动界面依次传递,等我移除所有的动态链后,发现该问题依旧存在,排除该问题。

    结果我却又陷入了自我怀疑中,做了好几年的Android开发,什么时候(MainActivity)设置为SingleTask会有这种改变,为什么我一直没发现?难道是最新的api版本的变化带来的修改,为什么新的修改这么坑爹?然后我又开始用不同版本的虚拟机进行测试,或者设置不同的targetSdkVersion进行测试,结果都一样,每次都是MainActiivy。我又陷入了沉思,这么多年MainActiviy都是用的SingleTask难道都是错觉吗?可是为什么之前的app都没有这个问题。(其实是之前都有闪屏页SplashActivity,现在没有闪屏页SplashActivity了)

    后面又仔细确定了提交记录的内容,发现可能影响的就是我移除了闪屏界面,恢复闪屏页面后果然没这个问题,确定问题后,就是有无闪屏页照成的问题,或者说是启动界面设置为SingleTask造成的问题。后面网上看了一些解决方案,主要是通过设置启动模式为standard或者SingleTop,然后添加Flag为Intent.FLAG_ACTIVITY_CLEAR_TOP来解决的,或者说达到这SingleTask类似的清栈效果,同时又不会造成每次启动都是MainActivity。

    深入分析SingleTask相关源码

    但网上清一色的文章并没有仔细分析为什么造成该问题,我又看了一些Activity的启动流程源码分析,也只是一笔带过某个方法名,并没有分析到该流程,没办法只能自己动手了。

    启动流程图
    image

    可以看到图中的Activity.startActivity中的启动模块,然后大概看一下流程,很容易就能看出大致的方法出现在哪里,这就是熟悉启动流程的好处,也是画图的好处。

    startActivityUnchecked

    先说一下startActivityUnchecked相关代码的大致逻辑,从getReusableIntentActivity中获取一个reusedActivity,因为这个时候是热启动,我们的Activity之前已经创建了,并没有新的Activity要插入栈中,所以返回不为空;

    进入if (reusedActivity != null) {判断逻辑,下面isLaunchModeOneOf(LAUNCH_SINGLE_INSTANCE, LAUNCH_SINGLE_TASK)条件也成立,又进入下一个逻辑判断,然后判断是否为根Activity,设置启动的Activity为我们的mStartActivity(MainActivity),所以当APP的启动Activity为MainActivity时,同时设置启动模式为SingleTask或者SingleInstance,每次点击app图标看到的界面就是MainActivity。

    private int startActivityUnchecked(final ActivityRecord r, ActivityRecord sourceRecord,
                IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
                int startFlags, boolean doResume, ActivityOptions options, TaskRecord inTask,
                ActivityRecord[] outActivity, boolean restrictedBgActivity) {
      ···
      //从getReusableIntentActivity中获取  
      ActivityRecord reusedActivity = getReusableIntentActivity();  
      ···
    //不为空时进入该循环   
    if (reusedActivity != null) {
                // When the flags NEW_TASK and CLEAR_TASK are set, then the task gets reused but
                // still needs to be a lock task mode violation since the task gets cleared out and
                // the device would otherwise leave the locked task.
     ···
     ···  
    
    // This code path leads to delivering a new intent, we want to make sure we schedule it
    // as the first operation, in case the activity will be resumed as a result of later
    // operations.
    //isLaunchModeOneOf(LAUNCH_SINGLE_INSTANCE, LAUNCH_SINGLE_TASK)表示启动模式为或者SingleInstance或者SingleTask时,进入该判断   
    if ((mLaunchFlags & FLAG_ACTIVITY_CLEAR_TOP) != 0
            || isDocumentLaunchesIntoExisting(mLaunchFlags)
            || isLaunchModeOneOf(LAUNCH_SINGLE_INSTANCE, LAUNCH_SINGLE_TASK)) {
        final TaskRecord task = reusedActivity.getTaskRecord();
    
        // In this situation we want to remove all activities from the task up to the one
        // being started. In most cases this means we are resetting the task to its initial
        // state.
       //大多数情况下我们可能准备清空当前task或者回到task的初始状态
        final ActivityRecord top = task.performClearTaskForReuseLocked(mStartActivity,
                mLaunchFlags);
    
        // The above code can remove {@code reusedActivity} from the task, leading to the
        // the {@code ActivityRecord} removing its reference to the {@code TaskRecord}. The
        // task reference is needed in the call below to
        // {@link setTargetStackAndMoveToFrontIfNeeded}.
        if (reusedActivity.getTaskRecord() == null) {
            reusedActivity.setTask(task);
        }
    
        if (top != null) {
            //是否为根activity  
            //boolean frontOfTask; // is this the root activity of its task?
            if (top.frontOfTask) {
                // Activity aliases may mean we use different intents for the top activity,
                // so make sure the task now has the identity of the new intent.         //设置启动Activity为根Activity
                top.getTaskRecord().setIntent(mStartActivity);
            }
                //将会调用该Activity的onNewIntent,一旦调用了mStartActivity,因为我们也设置了SingleTask或者SingleInstance,所以我们每次看到的都是mStartActivity
            deliverNewIntent(top);
        }
    }
    } 
    ···
    ···
    }
    复制代码
    

    先来看看getReusableIntentActivity方法,看看该方法的注释,很快就明白作用了,所以返回的不是null,所以会进入上面的判断逻辑中

    /**
     * Decide whether the new activity should be inserted into an existing task. Returns null
     * if not or an ActivityRecord with the task into which the new activity should be added.
     */
    private ActivityRecord getReusableIntentActivity() {
        // We may want to try to place the new activity in to an existing task.  We always
        // do this if the target activity is singleTask or singleInstance; we will also do
        // this if NEW_TASK has been requested, and there is not an additional qualifier telling
        // us to still place it in a new task: multi task, always doc mode, or being asked to
        // launch this as a new task behind the current one.
    复制代码
    

    再来看看deliverNewIntent中被调用到的deliverNewIntentLocked,最终决定哪个Activity的onNewIntent会被调用到,也就是我们的mStartActivity

    /**
     * Deliver a new Intent to an existing activity, so that its onNewIntent()
     * method will be called at the proper time.
     */
    final void deliverNewIntentLocked(int callingUid, Intent intent, String referrer) {
        // The activity now gets access to the data associated with this Intent.
    复制代码
    

    首次安装可能带来的问题

    在开发过程中,安装完成一个app时,在安装界面直接点击打开。我们进入了app的首页,这时我们按home键返回桌面,再点击应用图标,会发现没有直接进入首页,而是先进入了app的闪屏页,在进入首页。重复这一步一直如此。这时我们按back键返回,发现没有直接退回桌面,而是返回到之前打开的多个首页。但是如果一开始安装完我们不是直接打开,而是在桌面点击应用进入就不会这样了。

    解决方案

    在你的闪屏界面,或者没有闪屏界面,像我上面启动界面直接就是MainActivity的话,那你就在该界面的onCreate方法中直接添加下面这段代码。具体的分析可以见下面这篇文章

    if (!this.isTaskRoot()) { // 当前类不是该Task的根部,那么之前启动
                Intent intent = getIntent();
                if (intent != null) {
                    String action = intent.getAction();
                    if (intent.hasCategory(Intent.CATEGORY_LAUNCHER) && Intent.ACTION_MAIN.equals(action)) { // 当前类是从桌面启动的
                        finish(); // finish掉该类,直接打开该Task中现存的Activity
                        return;
                    }
                }
            }
    复制代码
    

    总结

    千万要注意,不要在你的启动界面(如果你想把MainActivity的windowbackground设置为闪屏界面,移除闪屏页,直接启动MainActivity给用户造成快速启动的感觉)设置启动模式为SingleTask或者SingleInstance,一旦设置后,不管软启动或者热启动都是从该启动界面开始启动App,除非特殊的需求,否则千万不要这么设置。如果想要实现类似SingleTask的清栈效果,可以使用singleTop结合对应的Flag进行实现(注意standard无论配合什么flag都会重新创建一个新的实例)。

    最后说一下另一种情况,如果你一定想要在启动页设置为SingleTask/SingleInstance(不设置活不下去了),那也有办法,就是添加个闪屏界面(SplashActivity设置为SingleTask/SingleInstance),然后启动MainActivity,这里千万要注意,闪屏界面(SplashActivity)一定要及时关掉,,同时在闪屏页面的onCreate方法中添加如下代码,否则你每次点击app图标都是从闪屏页开始显示了。但是这样上面的移除闪屏页快速启动的优化就没意义了。

    @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
    
            super.onCreate(savedInstanceState);
            Log.i(TAG, "onCreate: =========");
            //关键代码
            if (!isTaskRoot()) {
                Intent intent = getIntent();
                if (intent != null) {
                    if (intent.hasCategory(Intent.CATEGORY_LAUNCHER) && Intent.ACTION_MAIN.equals(intent.getAction())) {
                        finish();
                        return;
                    }
                }
            }
            Intent intent = new Intent(this, MainActivity.class);
            startActivity(intent);
            finish();
        }
    复制代码
    

    相关文章

      网友评论

        本文标题:005 四大组件-Activity-启动模式问题

        本文链接:https://www.haomeiwen.com/subject/gnvuaktx.html