Android升华之路------activity的启动模式

话说从工作以来,感觉自己明显变懒了,不怎么愿意动笔写点东西了,这个大学时候完全不一样。但是前人有云:好记性不如烂笔头,于是决定还是坚持写点东西,以来和大家分享一下自己的学习成果,而来督促自己学习进步。嘿嘿!!
废话少说,here we go!!
×××××××××××××××××××××××××××××以上为废话×××××××××××××××××××××××××××××××××××××××
在android中最让人熟知的莫过于activity了,从开发人员的hello world,到用户的多彩的界面,activity陪伴我们太久了!但是,突然某一天,某人问我activity的启动模式的问题,总感觉有些模糊,说不清楚,于是今天下点功夫搞清楚这个问题。
要学习android的只是,一手的资料肯定是google的在线文档:developer.android.com。有小伙伴反应,上不去啊,这个问题咱们就不再说了,大家度娘:老Dhost(给老D打个广告,大家没事可以给老D转点经费,让老D吃个盒饭),自然就明白了。
我们在这个链接中:
https://developer.android.com/reference/android/R.styleable.html#AndroidManifestActivity_launchMode
可以看到google对于activity启动模式的解释,为了照顾上不去的同学,这里我截一下图:
这里写图片描述
从上图中我们看到,activity的启动模式一共有四种,但是貌似google说的不够清楚,很多问题没有谈到。这里我们就详细说一说这四个模式。
Activity的启动模式一直以来都是新手的难点,很多奇怪灵异事件级别的BUG都是出现在这里。首先我想说一下android的activity为什么需要启动模式这个玩意。我们知道在默认情况下,当我们在我们的程序中启动不同的多个activity的时候,系统自然会将我们的目标activity实例化,然后将他们的实例放到一个activity回退栈中,并且会回调一些生命周期方法以便实现我们的操作。当我们按下android的返回键的时候,系统会把当前的activity销毁,并且将上个activity显示出来,当我们再次按下返回键的时候,再将更老的activity展示出来,这很符合“栈”的操作思想,事实上android中这个操作就是回退栈的操作。但是我们这里思考一个问题,如果我们反复地启动同一个activity,系统就会反复地创建同一个activity实例,这样是不是有点二??另外,同一个activity中不同的实例会不会有状态无法统一的问题??这还只是一个简单的场景,来个复杂的,假如一个应用A有一个activity中显示一个非常重要关键的值,这个值和当前的状态密切相关,并且这个activity可以被别的应用启动,如果这个时候有多个这个activity的任务栈,多个实例的话,岂不是会乱套??Android作为一个优秀的操作系统,自然会考虑这个问题,因此android自然就引入了activity启动模式来解决这个问题。下面我们先介绍一下activity的启动模式:

1. standard模式

这个是activity的默认模式,也就是标准模式,当你不制定任何启动模式的时候,就是这个模式(也就是你移一直在做的!!)。这个模式最好理解,每次启动一个activity的时候,系统不会理会目前的操作栈中是否已经存在了一个这样activity的实例,而是直接创建这个activity的实例,然后放到合适的栈中。被创建的activity会经历一个完整的生命周期方法,它的onCreate,onStart,onResume都会被调用。这是一个典型的多实例实现,一个任务栈中可以有多个这样的实例,多个任务栈中也可以有多个这样的实例。在这个模式下,只要是谁启动了目标activity,那么这个目标activity的实例就会出现在谁的任务栈中。比如Activity A启动了一个标准模式的Activity B那么这个B的实例就会出现在A的栈中。这里我们给出实际的代码,并且实际运行观察效果。
AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.baniel.activitylaunchmode">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

MainActivity布局

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.baniel.activitylaunchmode.MainActivity">

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="启动MainActivity"
        android:id="@+id/start_main_btn"
        android:layout_centerVertical="true"
        android:layout_centerHorizontal="true" />
</RelativeLayout>

MainActivity.java

package com.baniel.activitylaunchmode;

import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {

    private final String TAG = "MainActivity";

    private Button mButton = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Log.d(TAG, "MainActivity onCreate");

        mButton = (Button) this.findViewById(R.id.start_main_btn);
        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(MainActivity.this, MainActivity.class);
                startActivity(intent);
            }
        });
    }

    @Override
    protected void onStart() {
        super.onStart();

        Log.d(TAG, "MainActivity onStart");
    }

    @Override
    protected void onResume() {
        super.onResume();

        Log.d(TAG, "MainActivity onResume");
    }

    @Override
    protected void onPause() {
        super.onPause();

        Log.d(TAG, "MainActivity onPause");
    }

    @Override
    protected void onStop() {
        super.onStop();

        Log.d(TAG, "MainActivity onStop");
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        Log.d(TAG, "MainActivity onDestroy");
    }

    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);

        Log.d(TAG, "MainActivity onNewIntent");
    }
}

我们在MainActivity的布局中放了一个button,这个button就是用来启动MainActivity这个界面的。现在我们运行这个app,多次点击这个button,有一下log输出:
这里写图片描述
这里我连续点击了四次button,然后我们可以看到这个MainActivity被创建了四次,并且前一个activity先是被pause,然后新的activity生命周期:onCreate,onStart,onResume,之后老的activity就被stop了。另外,这一点我们可以从dumpsys中看到:
在命令行中执行:

adb shell dumpsys activity

输出的内容有点多,我们找到ACTIVITY MANAGER ACTIVITIES这个地方可以看到:
这里写图片描述
在com.baniel.activitylaunchmode这个任务栈中确实有5个MainActivity的实例(1个启动时的实例,4和我们点击button创建的实例)。
这就是standard模式,也是我们最常见的模式。

2. singleTop模式

这个模式中文名称叫做栈顶模式,这个听起来有点费劲,神马栈顶模式啊??其实,所谓栈顶模式就是对我们之前提到的回退栈中顶部的对象的复用,也就是说当我们创建一个新的activity的时候,如果目标栈中有我们的目标activity的话,那么我们就会直接复用这个activity实例,而不是很傻地在实例化一次。同时,系统只会调用onNewIntent方法,不会再走标准模式的一套生命周期方法了。这里我们在上一个实例代码中修改一下,观察下实际效果。
这里我们在AndroidManifest.xml中的MainActivity中加一个启动模式:

<activity android:name=".MainActivity"
            android:launchMode="singleTop">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

同时,复写MainActivity的onNewIntent方法:

@Override
protected void onNewIntent(Intent intent) {
    super.onNewIntent(intent);

    Log.d(TAG, "MainActivity onNewIntent");
}

然后我们再运行一下,同样我们点击4次button,看下log:
这里写图片描述
可以看到,这次和标准模式很不一样,activity会先被pause,然后onNewIntent被调用,然后就直接onResume了。并没有创建实际的实例,我们可以从dumpsys中看到:
这里写图片描述
我们发现com.baniel.activitylaunchmode这个任务栈中只有一个MainActivity实例,并没有多个实例。
这里有一个问题,如果目标activity不在栈的栈顶怎么办呢?也就是说栈中有这个activity的实例,但是不是在栈顶,会出现什么问题呢?我们尝试修改下代码,从实际角度观察,看看会发生什么。为了达到我们的目的,这里我们修改MainActivity中button使其启动ActivityB:

mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(MainActivity.this, ActivityB.class);
                startActivity(intent);
            }
        });

再引入两个新的activity:ActivityB和ActivityC,代码如下:
ActivityB中加入一个button,启动ActivityC:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.baniel.activitylaunchmode.ActivityB">

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="启动ActivityC"
        android:id="@+id/start_c_btn"
        android:layout_centerVertical="true"
        android:layout_centerHorizontal="true" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceLarge"
        android:text="我是activityB"
        android:id="@+id/textView2"
        android:layout_above="@+id/start_c_btn"
        android:layout_centerHorizontal="true"
        android:layout_marginBottom="57dp" />
</RelativeLayout>

ActivityB.java:

package com.baniel.activitylaunchmode;

import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;

public class ActivityB extends AppCompatActivity {

    private final String TAG = "ActivityB";

    private Button mButton = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_activity_b);

        Log.d(TAG, "ActivityB onCreate");

        mButton = (Button) this.findViewById(R.id.start_c_btn);
        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(ActivityB.this, ActivityC.class);
                startActivity(intent);
            }
        });
    }

    @Override
    protected void onStart() {
        super.onStart();

        Log.d(TAG, "ActivityB onStart");
    }

    @Override
    protected void onResume() {
        super.onResume();

        Log.d(TAG, "ActivityB onResume");
    }

    @Override
    protected void onPause() {
        super.onPause();

        Log.d(TAG, "ActivityB onPause");
    }

    @Override
    protected void onStop() {
        super.onStop();

        Log.d(TAG, "ActivityB onStop");
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        Log.d(TAG, "ActivityB onDestroy");
    }
}

ActivityC的布局,添加一个button,用于启动MainActivity:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.baniel.activitylaunchmode.ActivityC">

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="启动MainActivity"
        android:id="@+id/start_main_again_btn"
        android:layout_centerVertical="true"
        android:layout_centerHorizontal="true" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceLarge"
        android:text="我是ActivityC"
        android:id="@+id/textView3"
        android:layout_above="@+id/start_main_again_btn"
        android:layout_centerHorizontal="true"
        android:layout_marginBottom="73dp" />
</RelativeLayout>

ActivityC.java:

package com.baniel.activitylaunchmode;

import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;

public class ActivityC extends AppCompatActivity {

    private final String TAG = "ActivityC";

    private Button mButton = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_activity_c);

        Log.d(TAG, "ActivityC onCreate");

        mButton = (Button) this.findViewById(R.id.start_main_again_btn);
        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(ActivityC.this, MainActivity.class);
                startActivity(intent);
            }
        });
    }

    @Override
    protected void onStart() {
        super.onStart();

        Log.d(TAG, "ActivityC onStart");
    }

    @Override
    protected void onResume() {
        super.onResume();

        Log.d(TAG, "ActivityC onResume");
    }

    @Override
    protected void onPause() {
        super.onPause();

        Log.d(TAG, "ActivityC onPause");
    }

    @Override
    protected void onStop() {
        super.onStop();

        Log.d(TAG, "ActivityC onStop");
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();

        Log.d(TAG, "ActivityC onDestroy");
    }
}

这样一来,我们就可以在ActivityC启动的时候,创造出MainActivity在栈底的效果了,我们运行一下,同样是一直点击界面上的button,观察log输出:
这里写图片描述
可以从log中很清楚地看到,当我们点击ActivityC上的button的时候MainActivity又被创建了一次!也就是现在栈中应该是这样:
这里写图片描述
同样地,我们可以从dumpsys中得到印证:
这里写图片描述
也就是说,当singleTop模式下的activity不是在栈顶的时候是会重新创建新的实例化对象的!!这点有切记。

3. singleTask模式

这是模式比上个模式就稍微复杂了点,但是也是和上个模式有些许关联的。这个模式的中文名字叫做“栈内复用模式”,也就是说在这种模式下,只要activity在一个任务栈中存在,那么多次启动这个activity的话,都不会导致实例化这个类的对象,和之前的singleTop一样,系统也会回调它的onNewIntent方法。从细节上来讲,当启动一个singleTask模式的activity的时候,系统会查看当前系统时候存在这个activity想要的任务栈,如果不存在则新建一个这样的栈,如果存在的话,那么就去这个栈中查找是否有这个activity的实例,如果没有的话,就新建这个类的实例,然后把它放到这个栈的栈顶中;如果存在的话,那就把这个实例调到栈的栈顶中,至于怎么调到栈顶,我们稍后细说。
为了这个模式,我们把之前的MainActivity的启动模式修改为singleTask:

<activity
    android:name=".MainActivity"
    android:launchMode="singleTask">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

然后我们运行这个app,同样地操作,我们查看一下log输出:
这里写图片描述
我们看到,这个log中当我们再次启动MainActivity的时候log输出不一样了,MainActivity并没有重新创建,而是调用onNewIntent,onStart,onResume方法直接显示了,同时我们注意到ActivityB的实例再MainActivity显示之前被销毁了,之后在MainActivity显示之后ActivityC也被销毁了!也就是说,在singleTask模式下,只要目标activity存在于栈中,就不会重新创建这个对象,同时如果这个对象不在栈顶的话,那么这个对象之上的实例化对象将全部出栈并且销毁!这是个singleTop最大的不同!!!我们可以从dumpsys中看到我们的猜想:
这里写图片描述
可以看到目前的栈中就只有MainActivity这个实例化对象,其他的都不存在了,此时按下返回键直接返回到home桌面。

4. singleInstance模式

这个模式是singleTask的加强版本,singleTask模式下的activity可以存在于多个任务栈中,但是一个任务栈的实例化对象只有一个;singleInstance模式下activity就更加严格了,只能存在于一个任务栈中!也就是说,只要这个activity已经存在于一个任务栈中,当再次启动这个activity的时候,系统不管本次activity的目标栈是什么,直接复用这个实例化对象。换句话说,此时的activity就是java设计模式中的单例模式了。
我们还是采用之前的那个代码例子,这里我们需要首先说明一个概念,那就是activity任务栈。我们之前多次提到任务栈这个东西,那么他究竟是什么呢?在说明它之前我们先说明一个参数:TaskAffinity,中文可以翻译成任务默契度,这个翻译可能不好,不过不重要,能理解它的意思就行。这个参数是在AndroidManifest中针对某个activity制定的,值个参数应该赋值为一个字符串,注意这个字符串不是一个随意的字符串,必须是带有.的,通常可以使用公司的域名,否则应用再安装的时候会报出INSTALL_PARSE_FAILED_MANIFEST_MALFORMED的错误。这个参数指定了一个activity运行时所需要的任务栈的名字,一般来说每个activity都会有一个默认任务栈的名字,那就是你的应用包名。但是这个参数给了你自定义栈名的机会,使得一些特殊的activity可以运行在一个特殊的任务栈中。现在我们来说明一下什么是任务栈,任务栈就是一个activity赖以运行生存的一个软件栈,这个软件栈中保存了众多曾经运行过,现在处于stop状态的activity实例,设计这个软件的目的就是为了保证用户的逻辑能有一个上下文连贯的使用效果。说白了,就是为了让用户可以在一个操作完成之后,回到上一个界面。同时,任务栈分为前台和后台任务栈,当一个任务栈的栈顶实例处于显示前端的时候,这个任务栈就是一个前台任务栈,如果一个任务栈中的实例没有一个在前台的,那么这个任务栈就是一个后台任务栈。用户可以通过切换或者回退的方式将后台任务栈调到前台。
在解释了什么是任务栈以及一个特殊的activity怎么指定自己的任务栈之后,我们继续修改我们的代码,来实际观察singleInstance的效果。
首先我们需要将MainActivity改造成singleInstance模式:

<activity
    android:name=".MainActivity"
    android:launchMode="singleInstance">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

然后,我们将ActivityB和ActivityC的任务栈指定为自定名称的栈:

<activity android:name=".ActivityB"
    android:taskAffinity="com.baniel.test_task_1"/>
<activity android:name=".ActivityC"
    android:taskAffinity="com.baniel.test_task_1"/>

最后,我们运行app,依次点击界面上button,观察log输出:
这里写图片描述
可以看到当我们再次点击ActivityC上的button的时候,MainActivity并没有再次创建,而是直接使用原来栈中的实例,我们可以查看dumpsys印证我们的想法:
这里写图片描述
图中,我们可以看出有两个任务栈:com.baniel.activitylaunchmode和com.baniel.test_task_1,第一个栈中只有一个MainActivity实例,第二个栈中有ActivityC和ActivityB两个实例。
相反地,如果我们去掉MainActivity的singleInstance模式会怎么样呢?我们实验一下:

<activity
    android:name=".MainActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

然后运行一下代码,同样的操作,查看log输出:
这里写图片描述
可以清楚地看到,这次点击ActivityC上的button我们发现MainActivity重新被创建了,我们再查查看一下此时的dumpsys:
这里写图片描述
是的,这个时候就是standard模式了,所以一个任务栈中有多个实例了!
×××××××××××××××××××××××××××××××以下为废话××××××××××××××××××××××××××××
这是我从事android开发工作以来第一次写这么长的博文,其中肯定有很多描述不清楚的,也会有错误的地方,如果大家发现错误,请不宁赐教啊,以免误人子弟!!

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值