安卓开发里,跨进程通信一直是让人上头的话题。两个进程之间传数据,不是你把东西递给隔壁工位同事那么简单,中间隔着一道系统墙。平时搭车用Intent,带上Bundle传一点基础类型,还能忍;可一旦想传一个完整的对象,哪怕是简简单单的用户信息,都会卡在“怎么让对象能过墙”这个问题上。好在Kotlin给了一个很趁手的工具——数据类。它可以让我们把要传的数据收拾得整整齐齐,再配合Android的序列化机制,让数据在进程之间跑得又快又稳。
一、跨进程通信的日子为什么不好过
1.1 跨进程到底难在哪
每个App跑起来,系统都会给它圈一块自己专属的内存。你在一个进程里创建的对象,另一个进程根本没法直接用。哪怕你知道这个对象的内存地址,也摸不着,因为Android不允许进程之间互相碰对方的地盘。这就好比你跟同事不在同一个工位,你桌子上放了一张图纸,同事够不到,除非你把图纸复印一份送过去。
Android最常用的跨进程通信方式是Binder。你可以把Binder想象成一个快递员,专门负责在进程之间传递包裹。但是快递员身上不能背太多东西,当年Binder事务缓冲区就那么大,装不下太沉的物件。所以我们需要一种方法,把要传的对象“拆成包裹”,通过快递员安全送到对方手里。
1.2 大家平时怎么传数据
开发过程中,最常见的做法是在两个页面之间传数据。比如点一下列表,跳到详情页,用Intent把id带上。这种数据量小,放一个整数或者字符串就行。可如果要把整个对象传过去,就没这么简单了。
有人说直接用Serializable,在类上加一个接口,很简单。但Serializable走的是反射,慢是一方面,而且它生成的数据格式并不高效。另一个选择是Android自己推荐的Parcelable,它是为跨进程场景量身定做的,速度更快。很多同学一看到Parcelable那一堆方法就头疼,这时候Kotlin数据类就派上用场了。
二、数据类这个救火队员
2.1 数据类到底是个啥
Kotlin里的data class是专门用来装数据的类。它不像普通类那样要写一堆样板代码,一个关键词就让你拥有equals、hashCode、toString这些方法。说直白点,数据类就是替你把账单算好的管家。
看一个最简单的例子。下面这段就是典型的数据类,字段一摆,其他事情Kotlin自动帮你做了。
// 技术栈:Kotlin
data class User(val name: String, val age: Int)
就这么短短一行,你已经能打印User对象了,也能比较两个User是否相等。以前在Java里得写几十行,现在一行就完事。
2.2 为什么比普通类强
普通类就像办公室里的文件柜,你需要自己去拉开抽屉、整理文件。数据类则像带了标签的档案袋,结构更清晰,用起来也更省心。
不过要提醒一句,数据类并不是跨进程通信的万能钥匙。数据类只是让对象容易管理和复制,真要跨进程传递,还得让这个对象能序列化。也就是把它变成可以写进包裹的一串字节。
三、让数据类能够跨进程穿梭
3.1 Serializable 老大哥体验
Java原生的Serializable,用起来很省事。只要在类上实现Serializable接口,系统就帮你把对象变成字节流。缺点是这个过程会用到很多反射,序列化后的体积也比较大。如果你只是传几个小字段,勉强可以接受,但传大列表或者频繁传递,性能就有点难看。
3.2 Parcelable 才是效率优选
Parcelable是Android自家推出的序列化方案。它和Binder的关系很好,序列化的时候直接把对象的每个字段写进Parcel,对方再按顺序读出来,没有反射那些多余操作,所以快。
缺点也很明显:需要手动写writeToParcel和构造函数。好在Kotlin数据类减少了一部分样板,我们只需要把真正会变的字段照顾到。很多新手刚开始接触Parcelable时,都觉得它难。其实难就难在两点:一个是序列化的顺序,另一个是CREATOR的写法。一旦你把这两点摸清楚,后面就好办了。
3.3 一个完整的手写示例
假设我们要在进程之间传一个用户信息,包含姓名、年龄和电话。可以这样写。
// 技术栈:Kotlin + Android SDK
import android.os.Parcel
import android.os.Parcelable
/**
* 用户数据,作为跨进程传输的载体
*
* @param name 姓名
* @param age 年龄
* @param phone 电话,可能为空
*/
data class UserData(
val name: String,
val age: Int,
val phone: String?
) : Parcelable {
/**
* 从Parcel里读数据,顺序必须和writeToParcel完全一致
*/
constructor(parcel: Parcel) : this(
parcel.readString() ?: "",
parcel.readInt(),
parcel.readString()
)
/**
* 把当前对象的每个字段写进Parcel
*/
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeString(name)
parcel.writeInt(age)
parcel.writeString(phone)
}
/**
* 一般返回0就行
*/
override fun describeContents(): Int = 0
companion object {
@JvmField
val CREATOR = object : Parcelable.Creator<UserData> {
override fun createFromParcel(parcel: Parcel): UserData =
UserData(parcel)
override fun newArray(size: Int): Array<UserData?> =
arrayOfNulls(size)
}
}
}
注意,读和写的顺序要一模一样。你先写了字符串,对方就必须先读字符串;你先写了年龄,对方就必须跟着读整数。如果顺序反了,轻则数据错乱,重则直接崩溃。
四、在 Messenger 里带着数据类跑一趟
4.1 基本思路
Messenger是Android提供的一种轻量级跨进程通信方式。它底层是AIDL,但用起来比AIDL简单。你只需要在服务端弄一个Handler,在客户端拿到一个Messenger,然后发Message就行。
Message里有一个data字段,类型是Bundle,而Bundle支持放Parcelable对象。于是我们就可以让数据类住在Bundle里,然后跟着Message去另一个进程串门。
4.2 服务端代码示例
服务端建一个Service,返回一个Messenger的binder。收到消息后,从msg.data里取出UserData。
// 技术栈:Kotlin + Android SDK
import android.app.Service
import android.content.Intent
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.os.Message
import android.os.Messenger
import android.util.Log
/**
* 服务端:负责接收客户端发送过来的UserData
*/
class RemoteService : Service() {
private val messenger = Messenger(object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
when (msg.what) {
MSG_SEND_USER -> {
// 从Bundle中取出数据类对象
val user: UserData? = msg.data.getParcelable(EXTRA_USER)
if (user != null) {
Log.d("RemoteService", "收到用户:${user.name},年龄${user.age}")
} else {
Log.d("RemoteService", "没有拿到UserData")
}
}
}
}
})
override fun onBind(intent: Intent): IBinder {
return messenger.binder
}
companion object {
const val MSG_SEND_USER = 1
const val EXTRA_USER = "extra_user"
}
}
这里用Handler接收,意味着它默认跑在主线程。如果消息内容特别大,或者要做耗时操作,就要换成HandlerThread,或者把任务派发给线程池。
4.3 客户端代码示例
客户端要做的就是连上服务,然后构造一个Message,把UserData放进Bundle,最后send出去。
// 技术栈:Kotlin + Android SDK
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.Bundle
import android.os.IBinder
import android.os.Message
import android.os.Messenger
/**
* 客户端:连接远程服务并发送UserData
*/
class ClientSample(private val context: Context) {
private var remoteMessenger: Messenger? = null
/**
* 服务连接回调
*/
private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
remoteMessenger = Messenger(binder)
}
override fun onServiceDisconnected(name: ComponentName?) {
remoteMessenger = null
}
}
/**
* 绑定远程服务
*/
fun bindService() {
val intent = Intent(context, RemoteService::class.java)
context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
}
/**
* 解绑服务,别忘记在页面销毁时调用
*/
fun unbindService() {
context.unbindService(serviceConnection)
remoteMessenger = null
}
/**
* 发送UserData到服务端
*/
fun sendUserData() {
val user = UserData("小红", 18, "13800138000")
val bundle = Bundle().apply {
// 用Parcelable序列化后放进去
putParcelable(RemoteService.EXTRA_USER, user)
}
val message = Message.obtain(null, RemoteService.MSG_SEND_USER).apply {
data = bundle
}
try {
remoteMessenger?.send(message)
} catch (e: Exception) {
// 远程服务可能没连上,或者进程莫名挂了
e.printStackTrace()
}
}
}
发送过程本身不会卡线程,因为Messenger的send会把消息封装成一次Binder事务。但如果服务端迟迟没回应,不要傻等,该加超时就加超时。
4.4 数据类如何放进 Bundle
Bundle其实就是一个扁平的容器,存的时候是键值对。用putParcelable可以把数据类放进去,用getParcelable再拿出来。拿的时候最好带上泛型,比如getParcelable。如果拿出来的类型对不上,运行时会报错。
五、数据传着传着就变慢,怎么优化
5.1 能传索引就传索引
很多业务里,真正需要传的是一个id,而不是一整坨用户信息。这时候别傻乎乎地把整个UserData丢过去,传一个字符串id就够了。对端拿到id之后,自己到数据库或者内存缓存里查详细数据。这样既省了序列化时间,也避开了Binder缓冲区压力。
5.2 打包成一次传输,别一把一把撒
有时候一个页面需要展示一整个用户列表,一次传500个对象和分10次每次传50个对象,效果完全不同。Binder事务缓冲区大概1MB,超过就要出TransactionTooLargeException。与其多次送小包裹,不如打包成一个大包裹,一次送过去。
下面这个容器类就是干这个的,把一队UserData塞进一个Parcelable里。
// 技术栈:Kotlin + Android SDK
import android.os.Parcel
import android.os.Parcelable
/**
* 用户列表容器,一次传递多个UserData
*/
data class UserListBundle(
val users: List<UserData>
) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.createTypedArrayList(UserData.CREATOR) ?: emptyList()
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
// 用writeTypedList写出整个列表
parcel.writeTypedList(users)
}
override fun describeContents(): Int = 0
companion object {
@JvmField
val CREATOR = object : Parcelable.Creator<UserListBundle> {
override fun createFromParcel(parcel: Parcel): UserListBundle =
UserListBundle(parcel)
override fun newArray(size: Int): Array<UserListBundle?> =
arrayOfNulls(size)
}
}
}
当然,一次传再多,也不能超过Binder的缓冲区。如果数据实在大到离谱,就别走Binder了。
5.3 用共享内存区域
当你是要传图片、视频这类大块头,Binder就吃不消了。这时候可以试试共享内存,比如MemoryFile,或者干脆传一个文件描述符过去。对方拿到底层内存映射之后自己读,不走Binder复制数据。这个方法实现起来复杂一些,但确实是一条路。
5.4 优化 Parcelable 读写逻辑
写Parcel的时候,别在循环里一个字段一个字段地读。数组、列表都有对应的便捷方法,比如writeStringArray、writeTypedList。能用基础类型就用基础类型,尽量减少字符串拼接。字符串对象写进Parcel是会计算长度的,一次写太多也会增加开销。
另外,尽管理论上Bundle支持很多类型,但真正放到跨进程环境里,还是要控制体积。别把一堆临时变量全塞进去,能清理就清理。写代码时多问自己一句:对面真的需要这么多东西吗?
六、哪些场景特别适合用这套方案
6.1 页面跳转传参
Activity之间传数据是最常见的。用Intent的putExtra放一个UserData,再配合Bundle,目标页面直接getParcelable取出来。如果你的App只需要在页面之间传对象,不需要真正的多进程,这套方案已经能覆盖。
6.2 插件化与组件化通信
大型项目会把功能拆成很多模块,模块之间不想互相依赖。大家定义好一批数据类,然后用一个公共接口传这些数据类出去。底层实现走Binder或者Router,外面看起来就像在调用普通方法。
6.3 后台任务和前台界面
后台Service要向前台Activity报告进度,或者把结果送回来。通过Messenger发送数据类,后台不用关心Activity是不是已经销毁,因为Message发出来就完事。Activity也不可能直接拿到Service里创建的Java对象,所以用Parcelable传一份副本是最稳的。
6.4 多进程架构
有些应用为了稳定,会把播放、下载这类重任务放到独立进程。这时候不同进程之间需要同步状态,比如播放器调进度、下载完任务列表。用数据类配上Messenger,就能非常顺利地在多个进程之间交换这些业务数据。
七、用得爽也要知道坑在哪
7.1 版本兼容
数据类一旦发布出去,Parcelable的字段顺序就最好不要乱动。老版本App传过来的数据,可能没有某个新字段。如果强行读,轻则拿到null,重则崩溃。建议在包里加一个version字段,升级的时候做兼容判断。
7.2 大对象别硬传
Binder缓冲区有限,传大列表、传图片很容易炸。你要么拆小,要么走共享内存。真的没别的办法时,可以分几次传,然后拼接,但那样做用户体验一般,也容易出错。
7.3 线程和主线程问题
服务端如果用的是Handler(Looper.getMainLooper()),handleMessage就跑在主线程。你在这里做网络请求或者数据库操作,页面就会卡顿,甚至触发ANR。正确做法是用HandlerThread,让消息在子线程处理,处理完再发回主线程更新界面。
7.4 混淆规则
开了混淆以后,Parcelable的CREATOR字段一定要保留。很多开发者遇到过明明本地测试没问题,一上发布包就崩溃,多半就是CREATOR被混淆掉了。所以在混淆配置文件里要加上keep规则,保持CREATOR和writeToParcel这些方法名不变。
7.5 顺序和默认值
写Parcel和读Parcel的顺序千万不能换。再多说一次,这个问题太容易出了。另外,如果某个字段可能为空,读取的时候要做空判断,别一上来就用非空断言,那等于拿命赌数据一定存在。
如果你还在用Java方式手写一堆getter和setter,看完这篇不妨试着把数据模型迁移到Kotlin数据类。你会惊喜地发现,不仅是代码变短了,整个团队的沟通成本也变低了。
八、总结
Kotlin数据类配合Parcelable,再借助Bundle和Messenger,基本解决了Android跨进程通信里传递对象这个痛点。它的核心思路很清晰:先用数据类把想要传递的内容整理好,然后用Parcelable把数据类编码成可以过Binder的包裹,最后通过Intent、Bundle或者Message送出去。拿到的人按同样的顺序解码,就得到了一个内容一样的对象。
优化方面,记住四个字:少传精传。能传id就不传对象,能一次传完就不要碎片化传,能走共享内存就不要硬挤Binder。多做一层容器封装,也会让代码更好维护。
希望今天这篇内容,能让你下次再遇到“跨进程传对象”的时候,不再挠头,而是很自然地说一句:这不就是数据类加Parcelable的事嘛。
评论
围绕“Kotlin数据类在Android跨进程通信中的应用与数据传输优化方案”参与讨论