Intent

Intent

IntentAndroid程序中各组件之间进行交互的一种重要方式,它不仅可以指明当前组件想要执行的动作,还可以在不同组件之间传递数据。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。注意,在KotlinSecondActivity::class.java的写法就相当于JavaSecondActivity.class的写法。接下来再通过startActivity()方法执行这个Intent就可以了。

1.2 隐式Intent

显式调用需要明确的指定被启动的对象的组件信息,包括包名和类名,而隐式调用则不需要明确指出组件信息。原则上一个Intent不应该既是显式调用又是隐式调用,如果二者共存的话以显式调用为主。

隐式Intent并不明确指出想要启动哪一个哪一个组件,而是指定了一系列更为抽象的actioncategory等信息,然后交由系统去分析这个Intent,并找出合适的组件去启动。什么叫作合适的组件呢?简单来说,隐式调用需要Intent能够匹配目标组件的IntentFilter中所设置的过滤信息,如果不匹配将无法启动目标组件。IntentFilter中过滤的信息有actioncategorydataAndroidManifest.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中指定的actioncategory时,这个Activity才能响应该Intent

修改按钮的点击事件,代码如下所示:

button.setOnClickListener {
  val intent = Intent("com.example.kotlintest.ACTION_START")
  startActivity(intent)
}

使用了Intent的另一个构造函数,直接将action的字符串传了进去,表明想要启动能够响应com.example.activitytest.ACTION_START这个actionActivity

前面不是说要<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,同时我们也可以在应用中定义自己的categorycategory的匹配规则和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)
}

这里首先指定了IntentactionIntent.ACTION_VIEW,这是一个Android系统内置的动作,其常量值为android.intent.action.VIEW。然后通过Uri.parse()方法将一个 网址字符串解析成一个Uri对象,再调用Intent.setData()方法将这个Uri对象传递进去。

只有当<data>标签中指定的内容和Intent中携带的Data完全一致时,当前组件才能够响应该Intent。不过,在<data>标签中一般不会指定过多的内容。例如在上面的浏览器示例中,其实只需要指定android:schemehttps,就可以响应所有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能够响应的actionIntent.ACTION_VIEW的常量值,而category则指定了默认的category值,另外在<data>标签中,通过android:scheme指定了数据的协议必须是https协议,这样 ThirdActivity应该就和浏览器一样,能够响应一个打开网页的Intent了。

另外,由于Android Studio认为所有能够响应ACTION_VIEWActivity都应该加上BROWSABLEcategory,否则就会给出一段警告提醒。加上BROWSABLEcategory是为了实现deep link功能,和我们 目前学习的东西无关,所以这里直接在<intent-filter>标签上使用tools:ignore属性将警告忽略即可。

运行程序:

scheme

可以看到,系统自动弹出了一个列表,显示了目前能够响应这个Intent的所有程序。选择Chrome还会像之前一样打开浏览器,并显示百度的主页,而如果选择了KotlinTest,则会启动ThirdActivityJUST ONCE表示只是这次使用选择的程序打开,ALWAYS则表示以后一直使用这次选择的程序打开。需要注意的是,虽然我们声明了ThirdActivity是可以响应打开网页的Intent的,但实际上这个Activity并没有加载并显示网页的功能,所以在真正的项目中尽量不要出现这种有可能误导用户的行为。

除了https协议外,我们还可以指定很多其他协议,比如geo表示显示地理位置、tel表示拨打电话。在下面的代码展示了如何在程序中调用系统拨号界面:

button1.setOnClickListener {
  val intent = Intent(Intent.ACTION_DIAL)
  intent.data = Uri.parse("tel:10086")
  startActivity(intent)
}

首先指定了IntentactionIntent.ACTION_DIAL,这又是一个Android系统的内置动作。然后在data部分指定了协议是tel,号码是10086。重新运行一下程序,点击一下按钮,如图所示:

tel

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代表的是指定页面(路径);goodsIdname代表传递的两个参数

使用

<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由两部分组成,mineTypeURImineType指媒体类型,比如image/jepgaddio/mpeg4-genericvideo/*等,可以表示图片、文本、视频等不同的媒体格式,而URI中包含的数据就比较多了,下面是URI的结构:

<scheme>://<host>:<port>/[<pathPrefix>|<pathPattern>]

这里再给几个实际的例子就比较好理解了,如下所示:

content://com.example.project:200/folder/subfolder/etc
http://www.baidu.com:80/search/info
  • schemeURI的模式,比如httpfilecontent等,如果URI中没有指定scheme,那么整个URI的其他参数无效,也就意味着URI是无效的。
  • hostURI的主机名,比如www.baidu.com,如果host未指定,那么整个URI中的其他参数无效,这也意味着URI是无效的。
  • portURI中的端口号,比如80,仅当URI中指定了schemehost参数的时候port参数才是有意义的。
  • pathpathPatternpathPrefix:这三个参数表述路径信息,其中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的默认值为contentfile。也就是说,虽然没有指定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()方法,该方法会获取用于启动SecondActivityIntent,然后调用getStringExtra()方法并传入相应的键值,就可以得到传递的数据了。这里由于我们传递的是字符串,所以使用getStringExtra()方法来获取传递的数据。如果传递的是整型数据,则使用getIntExtra()方法;如果传递的是布尔型数据, 则使用getBooleanExtra()方法,以此类推。

2.2 返回数据给上一个Activity

Activity类中还有一个用于启动Activity.startActivityForResult()方法,但它期望在Activity销毁的时候能够返回一个结果给上 一个ActivitystartActivityForResult()方法接收两个参数:第一个参数还是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_OKRESULT_CANCELED这 两个值;第二个参数则把带有数据的Intent传递回去。 最后调用了finish()方法来销毁当前`Activity。

由于是使用startActivityForResult()方法来启动SecondActivity的,在SecondActivity被销毁之后会回调上一个ActivityonActivityResult()方法,因此我们需要在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:单例、eventBusApplicationsqlitesharedpreferencefile都可以;

如果是IPC

  • 共享内存性能还不错,通过MemoryFile开辟内存空间,获得FileDescriptor; 将FileDescriptor传递给其他进程;往共享内存写入数据;从共享内存读取数据
  • Socket或者管道性能不太好,涉及到至少两次拷贝

3 跨应用启动Activity

3.1 共享uidAPP(应用于系统级应用或者"全家桶"应用)
<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 BActivityB,则需要声明权限:<uses-permission android:name="com.demo.b">

什么是服务漏洞? 答:说App AActivity A启动App BActivity B时,传过来一个Bundle数据,此数据是一个被Serializable修饰的类SerializableA。 若App B中没有SerializableA这个类,只要App BActivity B中访问了IntentExtra(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

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值