任务栈
先来一段来自官网的介绍
A task is a collection of activities that users interact with when performing a certain job. The activities are arranged in a stack—the back stack)—in the order in which each activity is opened. For example, an email app might have one activity to show a list of new messages. When the user selects a message, a new activity opens to view that message. This new activity is added to the back stack. If the user presses the Back button, that new activity is finished and popped off the stack...(当然后面还有)
大致意思是:任务是用户与Activity交互时的集合,Activity被放置于栈--回退栈--按照Activity打开的顺序。举个例子,一个电子邮件的app可以有一个Activity去展示邮件的列表,当用户点击了一个邮件,会打开一个关于这个邮件Activity。这个新的Activity会被加到回退栈中。如果用户按下了返回键,这个新的Activity会被关闭并弹出回退栈(出栈)
英文不太好,大家可以去官网看看(官网task)
个人理解
你可以理解为处于栈顶的Activity就是我们看到的页面,页面的切换都是通过入栈出栈控制的。
比如一个app,进入到主页a(此时a处于栈顶),然后打开新的页面b,这时候页面b入栈,即页面b处于栈顶,页面a处于页面b下方。按下返回键,从页面b回到页面a,此时页面b出栈,页面a就处于了栈顶,也就是此时我们看到的是页面a
任务栈中Activity的顺序永远不会重新排列
遵守后进先出(Last In First Out)原则
为什么需要LaunchMode
譬如qq的新消息通知,如果我之前已经在这个聊天页面了,那我点击这个消息通知,总不能又新创建一次聊天页面吧,这个时候LaunchMode(启动模式)就起到至关重要的作用。目前有4种启动模式:standard、singleTop、singleTask和singleInstance。在AndroidManifest清单文件中<activity>标签中的LaunchMode中申明,譬如
<activity
android:name=".MainActivity"
android:launchMode="standard"/>
standard
标准模式,同时也是系统默认的模式,就算不像上面那样指定LaunchMode,系统也会默认给你设置为standard。在该模式下,会创建新的Activity实例并入栈,举个例子:
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
@BindView(R.id.button)
Button button;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
button.setOnClickListener(v->startActivity(new Intent(this,MainActivity.class)));
}
点击button后,每次都会打开新的MainActivity 。
singleTop
栈顶复用模式。在该模式下,如果页面处于栈顶,那么不会创建新的实例,同时它的onNewIntent方法会被回调,通过该方法的参数可以取出当前请求的信息,但是onCreate这些生命周期不会被回调。
public class WeclomeActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
Log.d(TAG, "onCreate: ");
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
startActivity(new Intent(this,MainActivity.class)
.putExtra("key","key"));
}
}
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
@BindView(R.id.button)
Button btn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main4);
ButterKnife.bind(this);
btn.setOnClickListener(v -> startActivity(new Intent(this, MainActivity.class)
.putExtra("name", "str")));
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
Log.d(TAG, "onNewIntent: "+getIntent().getStringExtra("key"));
Log.d(TAG, "onNewIntent: "+intent.getStringExtra("name"));
Log.d(TAG, "onNewIntent: "+intent.getStringExtra("key"));
setIntent(new Intent(this,MainActivity.class)
.putExtra("name","newName")
.putExtra("key","newKey"));
Log.d(TAG, "onNewIntent: "+getIntent().getStringExtra("key"));
Log.d(TAG, "onNewIntent: "+intent.getStringExtra("name"));
Log.d(TAG, "onNewIntent: "+intent.getStringExtra("key"));
}
}
点击button后输出如下:
D/MainActivity: onNewIntent: key
onNewIntent: str
onNewIntent: null
onNewIntent: newKey
onNewIntent: str
onNewIntent: null
如果不设置setIntent(),那么getIntent()获取到的值还是WeclomeActivity传递过来的值。
但是singleTop只适用于被设置了该LaunchMode的Activity处于栈顶的时候不会被重新创建实例,如果该Activity未处于栈顶,还是会被创建实例的。譬如我页面A设置了singleTop,然后A->B->C->A,这个时候,栈内就会有两个A元素,即ABCA
应用场景:上面我们说到的qq新消息推送,打开聊天页面就可以应用这种模式
singleTask
栈内复用模式。这是一种单实例的模式,该模式下,只要栈内存在Activity实例,那么启动该Activity都不会重新创建新的实例,并会清空该Activity上所有栈元素,同时也会回调onNewIntent。以上指的同一个app中启动该Activity,如果是其他应用以singleTask模式启动了这个Activity,那么他会创建新的任务栈。但是如果这个app已经被启动过了,这个app的任务栈此时处于后台,那么此时app的任务栈会从后台移动到前台,官网给的示例如下:
有点抽象?我们来举个例子,比如我此时正在看简书app,这时候qq推送了一条新的消息,你点进去后进到了qq的聊天页面,按回退键回到qq主页面,再按回退键就回到了简书app里面,这就与上面的图片的示例一致了。现在再回过头来看官网给的图片,Activity2启动了ActivityY(启动模式为singleTask),所以Activity Y所在Task被切换到前台。如果Activity2启动了Activity X(启动模式为singleTask),则Activity Y也会被出栈,即栈内仅有1、2、X3个页面。
singleInstance
单实例模式。加强版singleTask,该模式下,会为指定的Activity单独开启一个任务栈,并且栈内只有该Activity。
应用场景:譬如支付宝的支付页面。我从某个应用开启了支付宝的支付页面,这个时候我取消支付,则会回到当前应用而非支付宝的其他页面。
Intent Flag启动模式
除却上面在AndroidManifest中指定启动模式,也可通过设置Intent的Flag指定启动模式。下面介绍下常见的Flag:
Intent.FLAG_ACTIVITY_NEW_TASK
如果需要启动的Activity的栈已存在则不会新建栈,否则会创建新的Task来启动Activity。该Flag通常可使用在Service中启动Activity的场景,因为Service中不存在Activity栈,所以需要使用该Flag来创建新的Activity栈。虽然官网说与android:launchMode="singleTask"
效果一致,但是我感觉还是有点差别的。是否建新任务栈的效果是一致,但是singleTask不会重复创建实例,而该Flag是会重复创建的。FLAG_ACTIVITY_SINGLE_TOP
与android:launchMode="singleTop"
效果一致FLAG_ACTIVITY_CLEAR_TOP
与android:launchMode="singleTask"
效果一致FLAG_ACTIVITY_NO_HISTORY
以该模式启动Activity,该Activity启动了别的Activity的时候就会自动消失,不会出现在栈内。
以上两种方式均可指定LaunchMode,方法一与方法二同时存在时,方法二Intent Flag可以覆盖方法一指定的LaunchMode,即优先级高于方法一
再来看看官网给我们的其他一些属性
- taskAffinity
- allowTaskReparenting
- clearTaskOnLaunch
- alwaysRetainTaskState
- finishOnTaskLaunch
以上说的LaunchMode都是基于同一个应用的,上面的singleTask介绍中也说了这一段,如果是其他应用以singleTask模式启动了指定的Activity,那么会开启一个新的任务栈,这个其实也是singleTask的特性之一。指定了singleTask模式的Activity会先去寻找是否有指定了的任务栈,如果没有则创建新的任务栈,如果有该任务栈再去找是否有该Activity实例的存在,有则复用并清空该Activity上所有栈元素,没有则创建。那么如何指定任务栈呢?
taskAffinity
任务亲和性。可以用于指定一个Activity更愿意依附哪一个任务栈。默认情况下,taskAffinity即为应用的包名(以下说的指定taskAffinity的属性值都与包名不一致)。所以指定singleTask+taskAffinity可以启用新的任务栈,举个例子
<activity android:name=".MainActivity"
android:taskAffinity="com.asd.asd"
android:launchMode="singleTask">
清空返回栈
如何用户将任务切换到后台之后过了很长一段时间,系统会将这个任务中除了最底层的那个Activity之外的其它所有Activity全部清除掉。当用户重新回到这个任务的时候,最底层的那个Activity将得到恢复。这个是系统默认的行为,因为既然过了这么长的一段时间,用户很有可能早就忘记了当时正在做什么,那么重新回到这个任务的时候,基本上应该是要去做点新的事情了。
alwaysRetainTaskState
如果将最底层的那个Activity的这个属性设置为true,那么上面所描述的默认行为就将不会发生,任务中所有的Activity即使过了很长一段时间之后仍然会被继续保留。
clearTaskOnLaunch
如果将最底层的那个Activity的这个属性设置为true,那么只要用户离开了当前任务,再次返回的时候就会将最底层Activity之上的所有其它Activity全部清除掉。简单来讲,就是一种和alwaysRetainTaskState完全相反的工作模式,它保证每次返回任务的时候都会是一种初始化状态,即使用户仅仅离开了很短的一段时间。
finishOnTaskLaunch
这个属性和clearTaskOnLaunch是比较类似的,不过它不是作用于整个任务上的,而是作用于单个Activity上。如果某个Activity将这个属性设置成true,那么用户一旦离开了当前任务,再次返回时这个Activity就会被清除掉。
IntentFilter匹配规则
众所周知,启动Activity可以分为隐式启动与显示启动。
显示Intent
用于知晓需要跳转的目标组件名称的前提下,一般应用于同一个应用程序内
//写法一
Intent intent = new Intent();
intent.setClass(FirstActivity.this, SecondActivity.class);
startActivity(intent);
//写法二,当然两种写法是一样的
startActivity(new Intent(FirstActivity.this, SecondActivity.class))
隐式Intent
不同于显示Intent,隐式Intent不关心接收者是谁,只需要匹配到目标组件IntentFilter中设置的过滤规则,即可启动对应的组件,一般用于不同应用程序之间,也可用于同一个应用程序中,譬如最常见的拨打电话:
//权限问题这里就先不讲了
startActivity(new Intent(Intent.ACTION_DIAL, Uri.parse("tel:10086")));
IntentFilter中的过滤信息有action、category、data,可以有多个action、category、data,但是必须能匹配到一组对应的过滤信息才能启动对应的Activity或其他组件。另外组件也可拥有多个IntentFilter,一个Intent只要能匹配到一组IntentFilter即可启动对应的Activity或其他组件。
action匹配的规则
action是一个字符串常量,一个<intent-filter>标签必须包含一个或多个action,区分大小写,如果有多个<action>只需匹配其中一个即可成功启动对应组件,如果不包含action,则无法启动该组件
//AndroidManifest.xml
<intent-filter>
<action android:name="com.asd.string"/>
<action android:name="com.asd.str"/>
</intent-filter>
//Activity
startActivity(new Intent("com.asd.string"));
如上启动,会抛出
ActivityNotFoundException
,其实只配置action是不够的
category匹配的规则
category同样是一个字符串,系统预定义了一些category,同时我们也可以定义自己的category。匹配规则与action大致相同,可以有多个category,至少有一个匹配。但是由于在startActivity()或startActivityForResult()的时候会默认为Intent附加"android.intent.category.DEFAULT"这个category,所以隐式Intent启动的时候,必须申明
<category android:name="android.intent.category.DEFAULT"/>
否则会向上面那样抛出ActivityNotFoundException
data的匹配规则
同样类似于action的匹配规则,但是这个data允许没有,data的结构如下
<data android:scheme="string"
android:host="string"
android:port="string"
android:path="string"
android:pathPattern="string"
android:pathPrefix="string"
android:mimeType="string" />
data由两部分组成,URI与mimeType。mimeType指的媒体类型,例如image/jpeg、audio/mpeg4-generic和video/*等,可以表示图片,文本,视频等不同的媒体类型,而URI就是mimeType上面的那些参数。
<scheme>://<host>:<port>[<path>|<pathPrefix>|<pathPattern>]
举个例子
content://com.example.project:200/folder/subfolder/etc
http://www.baidu.com:80/serach/info
scheme
URI的模式,譬如http、file、content等,如果URI中没有指定该参数,则整个URI的其他参数均无效,等同于该URI无效
Host
主机名,如果URI中没有指定该参数,则整个URI的其他参数均无效,等同于该URI无效。如果需要匹配多个子域,可以使用通配符 *
,且*
需要在第一位,譬如*.google.com
可以匹配www.google.com
,.google.com
和developer.google.com
Port
端口号,只有指定了scheme与Host,该参数才有意义
path、pathPrefix、pathPattern
path :表示完整的路径,譬如上方第二个例子,这里就需写/serach/info
pathPrefix:路径的前缀,从/开始到/serach/info均可匹配,譬如/,/s,/ser,/serach此类均可匹配,不像path必须完全一致
pathPattern:与path一样,表示完整的路径,但是允许使用通配符
“” 用来匹配0次或更多,如:“a” 可以匹配“a”、“aa”、“aaa”…
“.” 用来匹配任意字符,如:“.” 可以匹配“a”、“b”,“c”…
“ * ” 就是用来匹配任意字符0次或更多
需要注意的是由于正则表达式的规范,如果想表达真实的字符串,那么“ * ”需要写成“\*”,“\”需要写成“ \ \ \ \”(这一段来自官网,但是自己实践的时候发现反而不用加转义才能匹配到。。。我觉得是哪里出了问题但是我没有证据XD)
这三个参数只要满足匹配其中一个即可启动对应的Activity
举个例子吧
btn.setOnClickListener(v -> {
startActivity(new Intent("com.asd.string")
.addCategory("android.intent.category.DEFAULT")
.setDataAndType(Uri.parse("https://www.baidu.com:8080/zxc/qwe/asd.html"),"image/jpeg"));
});
<activity android:name=".intentFilter.IntentFilterActivity">
<intent-filter>
<action android:name="com.asd.string"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:scheme="https"
android:host="www.baidu.com"
android:port="8080"
android:path="/zxc/qwe/asd.html"
android:pathPrefix="/"
//以下3中均可匹配
android:pathPattern=".*"
android:pathPattern="/.*/.*"
android:pathPattern="/.*/.*/.*"
android:mimeType="image/jpeg"
/>
</intent-filter>
这里还需要注意,date这里的参数设置官方给了我们三个方法,我们来看看
public @NonNull Intent setDataAndType(@Nullable Uri data
, @Nullable String type) {
mData = data;
mType = type;
return this;
}
//此时mineType被赋null
public @NonNull Intent setData(@Nullable Uri data) {
xmData = data;
mType = null;
return this;
}
//此时uri被赋null
public @NonNull Intent setType(@Nullable String type) {
mData = null;
mType = type;
return this;
}
嗯,不知道为什么这排版看起来有点丑XD。可以看到,如果你指定了uri没有指定mimeType,你可以使用setData(),如果你指定了minmeType而没有指定uri,你可以使用setType(),如果你两者都指定了,则需要使用setDataAndType()。
总结
比较枯燥的一章
参考资料
Android任务和返回栈完全解析,细数那些你所不知道的细节
Android官网
Android开发艺术探索
随笔
这一章写了有一个礼拜。。。其实上是写了两个下午,中间差了一个礼拜,hhh,可能有点问题毕竟隔的有点久,希望如果有人看到了这篇文章还是能自己亲自去动手试试。good luck,boy~