Intent
Intent
是Android
程序中各组件之间进行交互的一种重要方式,它不仅可以指明当前组件想要执行的动作,还可以在不同组件之间传递数据。Intent
一般可用于启动Activity
、启动Service
以及发送广播等场景。
1 显示Intent
和隐式Intent
1.1 显式Intent
Intent
有多个构造函数的重载,其中一个是Intent(Context packageContext, Class<? > cls)
。这个构造函数接收两个参数:第一个参数Context
要求提供一个启动组件的上下文;第二个参数Class
用于指定想要启动的目标组件,通过这个构造函数就可以构建出Intent
的“意图”。
Activity
类中提供了一个startActivity()
方法,专门用于启动Activity
,它接收一个Intent
参数,将构建好的Intent
传入startActivity()
方法就可以启动目标Activity
了。代码如下所示:
button.setOnClickListener {
val intent = Intent(this, SecondActivity::class.java)
startActivity(intent)
}
首先构建了一个Intent
对象,第一个参数传入this
作为上下文,第二 个参数传入SecondActivity::class.java
作为目标Activity
。注意,在Kotlin
中SecondActivity::class.java
的写法就相当于Java
中SecondActivity.class
的写法。接下来再通过startActivity()
方法执行这个Intent
就可以了。
1.2 隐式Intent
显式调用需要明确的指定被启动的对象的组件信息,包括包名和类名,而隐式调用则不需要明确指出组件信息。原则上一个Intent
不应该既是显式调用又是隐式调用,如果二者共存的话以显式调用为主。
隐式Intent
并不明确指出想要启动哪一个哪一个组件,而是指定了一系列更为抽象的action
和category
等信息,然后交由系统去分析这个Intent
,并找出合适的组件去启动。什么叫作合适的组件呢?简单来说,隐式调用需要Intent
能够匹配目标组件的IntentFilter
中所设置的过滤信息,如果不匹配将无法启动目标组件。IntentFilter
中过滤的信息有action
、category
、data
。 在AndroidManifest.xml
,添加如下代码:
<activity android:name=".SecondActivity">
<intent-filter>
<action android:name="com.example.kotlintest.ACTION_START" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
在<action>
标签中指明了当前Activity
可以响应com.example.activitytest.ACTION_START
这个action
,而<category>
标签则包含了一些附加信息,更精确地指明了当前Activity
能够响应的Intent
中还可能带有的category
。只有<action>
和<category>
中的内容同时匹配Intent
中指定的action
和category
时,这个Activity
才能响应该Intent
。
修改按钮的点击事件,代码如下所示:
button.setOnClickListener {
val intent = Intent("com.example.kotlintest.ACTION_START")
startActivity(intent)
}
使用了Intent
的另一个构造函数,直接将action
的字符串传了进去,表明想要启动能够响应com.example.activitytest.ACTION_START
这个action
的Activity
。
前面不是说要<action>
和<category>
同时匹配才能响应吗?怎么没看到哪里有指定category
呢?这是因为android.intent.category.DEFAULT
是一种默认的category
, 在调用startActivity()
方法的时候会自动将这个category
添加到Intent
中。
每个Intent
中只能指定一个action
,但能指定多个category
。目前的Intent
中只有一个 默认的category
,现在再来增加一个,代码如下所示:
button.setOnClickListener {
val intent = Intent("com.example.kotlintest.ACTION_START")
intent.addCategory("com.example.kotlintest.MY_CATEGORY")
startActivity(intent)
}
可以调用Intent
中的addCategory()
方法来添加一个category
,这里指定了一个自定义的category
,值为com.example.activitytest.MY_CATEGORY
。
在AndroidManifest.xml
文件中添加:
<activity android:name=".SecondActivity">
<intent-filter>
<action android:name="com.example.kotlintest.ACTION_START" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="com.example.kotlintest.MY_CATEGORY" />
</intent-filter>
</activity>
说明:
action
的匹配规则:action
是一个字符串,系统预定义了一些action
,同时我们也可以在应用中定义自己的action
。一个过滤规则中可以有多个action
,只要Intent
中的action
能够和过滤规则中的任何一个action
相同即可匹配成功(和category
匹配规则的不同)。需要注意的是,Intent
中如果没有指定action
,那么匹配失败。 另外,action
区分大小写,大小写不同字符串相同的action
会匹配失败。category
的匹配规则:category
是一个字符串,系统预定义了一些category
,同时我们也可以在应用中定义自己的category
。category
的匹配规则和action
不同,它要求如果Intent
中含有category
,那么所有的category
都必须和过滤规则中的其中一个category
相同。 当然,Intent
中可以没有category
,如果没有category
的话,按照上面的描述,这个Intent
仍然可以匹配成功。为什么不设置category
也可以匹配呢?原因是系统在调用startActivity
或者startActivityForResult
的时候会默认为Intent
加上android.intent.category.DEFAULT
这个category
。
1.3 其他隐式Intent
的用法
使用隐式Intent
,不仅可以启动自己程序内的组件,还可以启动其他程序的组件,这就使多个应用程序之间的功能共享成为了可能。 比如应用程序中需要展示一个网页,如果没有必要自己去实现一个浏览器(事实上也不太可能),只需要调用系统的浏览器来打开这个网页就行了。
button.setOnClickListener {
val intent = Intent(Intent.ACTION_VIEW)
intent.data = Uri.parse("https://www.baidu.com")
startActivity(intent)
}
这里首先指定了Intent
的action
是Intent.ACTION_VIEW
,这是一个Android
系统内置的动作,其常量值为android.intent.action.VIEW
。然后通过Uri.parse()
方法将一个 网址字符串解析成一个Uri
对象,再调用Intent.setData()
方法将这个Uri
对象传递进去。
只有当<data>
标签中指定的内容和Intent
中携带的Data
完全一致时,当前组件才能够响应该Intent
。不过,在<data>
标签中一般不会指定过多的内容。例如在上面的浏览器示例中,其实只需要指定android:scheme
为https
,就可以响应所有https
协议的Intent
了。
在AndroidManifest.xml
中修改ThirdActivity
的注册信息:
<activity android:name=".ThirdActivity">
<intent-filter tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="https" />
</intent-filter>
</activity>
在ThirdActivity
的<intent-filter>
中配置了当前Activity
能够响应的action
是Intent.ACTION_VIEW
的常量值,而category
则指定了默认的category
值,另外在<data>
标签中,通过android:scheme
指定了数据的协议必须是https
协议,这样 ThirdActivity
应该就和浏览器一样,能够响应一个打开网页的Intent
了。
另外,由于Android Studio
认为所有能够响应ACTION_VIEW
的Activity
都应该加上BROWSABLE
的category
,否则就会给出一段警告提醒。加上BROWSABLE
的category
是为了实现deep link
功能,和我们 目前学习的东西无关,所以这里直接在<intent-filter>
标签上使用tools:ignore
属性将警告忽略即可。
运行程序:
可以看到,系统自动弹出了一个列表,显示了目前能够响应这个Intent
的所有程序。选择Chrome
还会像之前一样打开浏览器,并显示百度的主页,而如果选择了KotlinTest
,则会启动ThirdActivity
。JUST ONCE
表示只是这次使用选择的程序打开,ALWAYS
则表示以后一直使用这次选择的程序打开。需要注意的是,虽然我们声明了ThirdActivity
是可以响应打开网页的Intent
的,但实际上这个Activity
并没有加载并显示网页的功能,所以在真正的项目中尽量不要出现这种有可能误导用户的行为。
除了https
协议外,我们还可以指定很多其他协议,比如geo
表示显示地理位置、tel
表示拨打电话。在下面的代码展示了如何在程序中调用系统拨号界面:
button1.setOnClickListener {
val intent = Intent(Intent.ACTION_DIAL)
intent.data = Uri.parse("tel:10086")
startActivity(intent)
}
首先指定了Intent
的action
是Intent.ACTION_DIAL
,这又是一个Android
系统的内置动作。然后在data
部分指定了协议是tel
,号码是10086
。重新运行一下程序,点击一下按钮,如图所示:
scheme
使用场景,协议格式,如何使用?
scheme
是一种页面内跳转协议,是一种非常好的实现机制,通过定义自己的scheme
协议,可以非常方便跳转APP
中的各个页面。
APP
根据URL
跳转到另外一个APP
指定页面。
scheme
链接格式样式:scheme://host/path?query
;解析:Uri.parse("hr://test:8080/goods?goodsId=8897&name=test")
hr
代表scheme
协议名称;test
代表scheme
作用的地址域;8080
代表改路径的端口号;/goods
代表的是指定页面(路径);goodsId
和name
代表传递的两个参数
使用
<intent-filter>
<!-- 协议部分配置 ,注意需要跟web配置相同--> <!--协议部分,随便设置 hr://test:8080/goods?name=test -->
<data android:scheme="hr"
android:host="test"
android:path="/goods"
android:port="8080"/>
<!--下面这几行也必须得设置-->
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<action android:name="android.intent.action.VIEW" />
</intent-filter>
调用:
Intent intent = new Intent(Intent.ACTION_VIEW,Uri.parse("hr://test:8080/goods?name=test"));
startActivity(intent);
data
的匹配规则
data
的匹配规则和action
类似,如果过滤规则中定义了data
,那么Intent
中必须也要定义可匹配的data
。在介绍data
匹配规则之前,需要先了解一下data
的结构,因为data
稍有复杂。
data
的语法如下:
<data
android:host="string"
android:mimeType="string"
android:path="string"
android:pathPattern="string"
android:pathPrefix="string"
android:port="string"
android:scheme="string" />
data
由两部分组成,mineType
和URI
。mineType
指媒体类型,比如image/jepg
、addio/mpeg4-generic
和video/*
等,可以表示图片、文本、视频等不同的媒体格式,而URI
中包含的数据就比较多了,下面是URI
的结构:
<scheme>://<host>:<port>/[<pathPrefix>|<pathPattern>]
这里再给几个实际的例子就比较好理解了,如下所示:
content://com.example.project:200/folder/subfolder/etc
http://www.baidu.com:80/search/info
scheme
:URI
的模式,比如http
、file
、content
等,如果URI
中没有指定scheme
,那么整个URI
的其他参数无效,也就意味着URI
是无效的。host
:URI
的主机名,比如www.baidu.com
,如果host
未指定,那么整个URI
中的其他参数无效,这也意味着URI
是无效的。port
:URI
中的端口号,比如80
,仅当URI
中指定了scheme
和host
参数的时候port
参数才是有意义的。path
、pathPattern
、pathPrefix
:这三个参数表述路径信息,其中path
表示完成的路径信息:pathPattern
也表示完整的路径信息;pathPattern
也表示完整的路径信息,但是它里面可以包含通配符*
,*
表示0
个或多个任意字符,需要注意的是,由于正则表达式的规范,如果想表示真正的字符串,那么*
要么写成\\*
,\
要写成\\\\
;pathPrefix
表示路径的前缀信息。
data
的匹配规则和action
类似,它要求Intent
中必须含有data
数据,并且data
数据能够完全匹配过滤规则中的某一个data
。这里的完全匹配是指过滤规则中出现的data
部分也出现在了Intent
中的data
中。
下面分情况说明:
(1)如下过滤原则:
<intent-filter>
<data android:mineType="image/*" />
...
</intent-filter>
这种规则指定了媒体类型为所有类型的图片,那么Intent
中的mineType
属性必须为image/*
才匹配,这种情况下虽然过滤规则没有指定URI
,但是却有默认值,URI
的默认值为content
和file
。也就是说,虽然没有指定URI
,但是Intent
中的URI
部分的scheme
必须为content
或者file
才能匹配,这点是需要尤其注意的。为了匹配(1)中规则,我们可以写出如下示例:
intent.setDataAndType(Uri.parse("file://abc"), "image/png")
另外,如果要为Intent
指定完整的data
,必须要调用setDataAndType
方法,不能先调用setData
再调用setType
,因为这两个方法彼此会清除对方的值,这个看源码就可以很容易理解,比如setData
:
public Intent setData(Uri data) {
mData = data;
mType = null;
return this;
}
可以发现,setData
会把mineType
置为null
,同理setType
也会把URI
置为null
。
(2)如下过滤规则:
<intent-filter>
<data android:mineType="video/mpeg" android:scheme="http" ... />
<data android:mineType="audio/mpeg" android:scheme="http" ... />
...
</intent-filter>
这种规则指定了两组data
规则,且每个data
都指定了完整的属性值,既有URI
又有mineType
,为了匹配(2)中规则:
intent.setDataAndType(Uri.parse("http://abc"), "video/mpeg");
// 或者
intent.setDataAndType(Uri.parse("http://abc"), "audio/mpeg");
通过上面两个示例,应该已经明白了data
的匹配规则,关于data
还有一个特殊情况需要说明一下,这也是它和action
不同的地方,如下面两种特殊的写法,它们的作用是一样的:
<intent-filter ... >
<data android:scheme="file" android:host="www.baidu.com" />
...
</intent-filter>
<intent-filter ... >
<data android:scheme="file" />
<data android:host="www.baidu.com" />
...
</intent-filter>
2 数据传递
2.1 向下一个Activity
传递数据
Intent
中提供了一系列putExtra()
方法的重载,可以把想要传递的数据暂存在Intent
中,在启动另一个Activity
后,只需要把这些数据从Intent
中取出就可以了。 比如说FirstActivity
中有一个字符串,现在想把这个字符串传递到SecondActivity
中,可以这样编写:
button1.setOnClickListener {
val data = "Hello SecondActivity"
val intent = Intent(this, SecondActivity::class.java)
intent.putExtra("extra_data", data)
startActivity(intent)
}
这里使用显式Intent
的方式来启动SecondActivity
,并通过putExtra()
方法传递了 一个字符串。注意,这里putExtra()
方法接收两个参数,第一个参数是键,用于之后从Intent
中取值,第二个参数才是真正要传递的数据。
然后在SecondActivity
中将传递的数据取出,并打印出来,代码如下所示:
class SecondActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_third)
val extraData = intent.getStringExtra("extra_data")
Log.e("CAH", "extra data is $extraData")
}
}
// CAH: extra data is Hello SecondActivity
上述代码中的intent
实际上调用的是父类的getIntent()
方法,该方法会获取用于启动SecondActivity
的Intent
,然后调用getStringExtra()
方法并传入相应的键值,就可以得到传递的数据了。这里由于我们传递的是字符串,所以使用getStringExtra()
方法来获取传递的数据。如果传递的是整型数据,则使用getIntExtra()
方法;如果传递的是布尔型数据, 则使用getBooleanExtra()
方法,以此类推。
2.2 返回数据给上一个Activity
Activity
类中还有一个用于启动Activity.startActivityForResult()
方法,但它期望在Activity
销毁的时候能够返回一个结果给上 一个Activity
。startActivityForResult()
方法接收两个参数:第一个参数还是Intent
;第二个参数是请求码,用于在之后的回调中判断数据的来源。 代码如下所示:
button1.setOnClickListener {
val data = "Hello SecondActivity"
val intent = Intent(this, SecondActivity::class.java)
intent.putExtra("extra_data", data)
startActivityForResult(intent, 1)
}
这里使用了startActivityForResult()
方法来启动SecondActivity
,请求码只要是一个唯一值即可,这里传入了1
。接下来我们在SecondActivity
中给按钮注册点击事件,并在点 击事件中添加返回数据的逻辑,代码如下所示:
class SecondActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_third)
button2.setOnClickListener {
val intent = Intent()
intent.putExtra("data_return", "Hello FirstActivity")
setResult(RESULT_OK, intent)
finish()
}
}
}
可以看到,还是构建了一个Intent
,只不过这个Intent
仅仅用于传递数据而已,它没有指定任何的“意图”。紧接着把要传递的数据存放在Intent
中,然后调用setResult()
方法。这个方法非常重要,专门用于向上一个Activity
返回数据。setResult()
方法接收两个参数:第一 个参数用于向上一个Activity
返回处理结果,一般只使用RESULT_OK
或RESULT_CANCELED
这 两个值;第二个参数则把带有数据的Intent
传递回去。 最后调用了finish()
方法来销毁当前`Activity。
由于是使用startActivityForResult()
方法来启动SecondActivity
的,在SecondActivity
被销毁之后会回调上一个Activity
的onActivityResult()
方法,因此我们需要在FirstActivity
中重写这个方法来得到返回的数据,如下所示:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
when (requestCode) {
1 -> if (resultCode == RESULT_OK) {
val returnedData = data?.getStringExtra("data_return")
Log.e("CAH", "return data is $returnedData")
}
}
}
// CAH: return data is Hello FirstActivity
onActivityResult()
方法带有3
个参数:第一个参数requestCode
,即在启动Activity
时传入的请求码;第二个参数resultCode
,即在返回数据时传入的处理结果;第三个参数data
,即携带着返回数据的Intent
。 由于在一个Activity
中有可能调用 startActivityForResult()
方法去启动很多不同的Activity
,每一个Activity
返回的数据都会回调到onActivityResult()
这个方法中,因此首先要做的就是通过检查requestCode
的值来判断数据来源。确定数据是从SecondActivity
返回的之后,再通过resultCode
的值来判断处理结果是否成功。最后从data
中取值并打印出来,这样就完成了向上一个Activity
返回数据的工作。
如果用户在SecondActivity
中并不是通过点击按钮,而是通过按下Back
键回到FirstActivity
,这样数据不就没法返回了吗?没错,不过这种情况还是很好处理的,可以通过在SecondActivity
中重写onBackPressed()
方法来解决这个问题,代码如下所示:
override fun onBackPressed() {
val intent = Intent()
intent.putExtra("data_return", "Hello FirstActivity")
setResult(RESULT_OK, intent)
finish()
}
这样,当用户按下Back
键后,就会执行onBackPressed()
方法中的代码了。
Activity
之间传递数据的方式Intent
是否有大小限制,如果传递的数据量偏大,有哪些方案?
Activity.startActivity -> Activity.startActivityForResult -> Instrumentation.execStartActivity ->ActivityManger.getService().startActivity
Intent
中携带的数据要从APP
进程传输到AMS
进程,再由AMS
进程传输到目标Activity
所在进程,通过Binder
来实现进程间通信
Binder
驱动在内核空间创建一个数据接收缓存区;- 在内核空间开辟一块内核缓存区,建立内核缓存区和内核空间的数据接收缓存区之间的映射关系,以及内核中数据接收缓存区和接收进程用户空间地址的映射关系;
- 发送方进程通过系统调用
copyfromuser()
将数据copy
到内核空间的内核缓存区,由于内核缓存区和接收进程的用户空间存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间,这样便完成了一次进程间的通信;
使用Intent
来传递数据时,用到了Binder
机制,数据就存放在了Binder
的事务缓冲区里面,而事务缓冲区是有大小限制的。普通的由Zygote
孵化而来的用户进程,映射的Binder
内存大小是不到1M
的。Binder
本身就是为了进程间频繁—灵活的通信所设计的,并不是为了拷贝大量数据。
如果非IPC
:单例、eventBus
、Application
、sqlite
、sharedpreference
、file
都可以;
如果是IPC
:
- 共享内存性能还不错,通过
MemoryFile
开辟内存空间,获得FileDescriptor
; 将FileDescriptor
传递给其他进程;往共享内存写入数据;从共享内存读取数据 Socket
或者管道性能不太好,涉及到至少两次拷贝
3 跨应用启动Activity
3.1 共享uid
的APP
(应用于系统级应用或者"全家桶"应用)
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.android.customwidget"
android:sharedUserId="com.demo">
...
</manifest>
共享uid
不仅能启动其Activity
,系统对于流量的计算等等都是共享的。
3.2 使用exported
<activity android:name=".BActivity"android:exported="true"/>
3.3 使用IntentFilter
,配置action
等
<activity android:name=".BActivity"android:permission="com.demo.b">
<intent-filter>
<action android:name="com.demo.intnet.Test"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
使用:
Intent it = new Intent();
it.setAction("com.demo.intnet.Test");
startActivity(it);
3.4 其他
如果App B
为允许外部启动的Activity B
,需要加权限,示例:
<activity android:name=".BActivity" android:permission="com.demo.b">
<intent-filter>
<action android:name="com.demo.intnet.Test"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
App A
若想启动App B
的ActivityB
,则需要声明权限:<uses-permission android:name="com.demo.b">
什么是服务漏洞? 答:说App A
的Activity A
启动App B
的Activity B
时,传过来一个Bundle
数据,此数据是一个被Serializable
修饰的类SerializableA
。 若App B
中没有SerializableA
这个类,只要App B
的Activity B
中访问了Intent
的Extra(getIntent().getExtras())
则就会发生类找不到异常。此种情况就是服务漏洞 如何解决服务漏洞? 答:try{}catch(Exception e){}
参考
https://juejin.cn/post/6844904056461197326#heading-0
https://cloud.tencent.com/developer/article/1356506
https://zhuanlan.zhihu.com/p/67451239
https://zhuanlan.zhihu.com/p/157842441
https://www.pianshen.com/article/70371941935/
https://www.jianshu.com/p/bb0f3df62501
https://blog.csdn.net/wangjia55/article/details/38536171