众所周知,Android 中的 Bundle 用于组件间的数据传递,数据在其中以键值对的方式保存。其中传递的数据可以是基本类型或者对象类型,其中,通过 Bundle 传递的对象类型必须是可序列化的,即需要实现 Parcelable 或者 Serializable 接口。
Bundle 为 Android 中的跨进程数据传递带来了极大的方便,通常我们认为,将对象存入 Bundle 是序列化的过程,而取出是反序列化的过程,这意味着取出的对象是原对象的一份深拷贝。然而,实际开发中的一个发现却颠覆了我的认识。这个问题至今在 Google 上没有找到相关的结果,所以打算写篇博客记录一下。
缘起
故事的缘起来自一个报错,具体的内容是通过 Bundle 传递大对象时发生了异常,原因是 Bundle 对于数据的大小是有1024KB限制的。但深入研究发现,这个错误只出现在 Activity 之间的 Bundle 传递时发生,同样的对象在 Activity 和 Fragment 间传递时却不会出错。难道这其中有什么玄机?
验证
Bundle 在 Activity 间的传递
通过定义两个Activity,并观察在 Bundle 在传递过程中的 hashCode 值的变化。其中第一个Activity 向第二个 Activity 传递一个 User 类型,在第二个 Activity 中改变 User 的值,并观察该值的改变是否影响到上一个页面。
首先,定义一下传递的数据类型User,实现了 Parcelable 接口:
User.java
public class User implements Parcelable {
public String name;
public User() {
}
protected User(Parcel in) {
name = in.readString();
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(name);
}
@Override
public int describeContents() {
return 0;
}
public static final Creator<User> CREATOR = new Creator<User>() {
@Override
public User createFromParcel(Parcel in) {
return new User(in);
}
@Override
public User[] newArray(int size) {
return new User[size];
}
};
}
第一个Activity:
public class MainActivity extends AppCompatActivity {
static final String TAG = "MainActivity";
private TextView mTextMessage;
private User mUser;
private BottomNavigationView.OnNavigationItemSelectedListener mOnNavigationItemSelectedListener
= new BottomNavigationView.OnNavigationItemSelectedListener() {
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
switch (item.getItemId()) {
case R.id.navigation_home:
return true;
case R.id.navigation_dashboard:
SecondActivity.start(MainActivity.this, mUser);
return true;
case R.id.navigation_notifications:
SecondFragment.show(getSupportFragmentManager(), mUser);
return true;
}
return false;
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextMessage = (TextView) findViewById(R.id.message);
BottomNavigationView navigation = (BottomNavigationView) findViewById(R.id.navigation);
navigation.setOnNavigationItemSelectedListener(mOnNavigationItemSelectedListener);
mUser = new User();
Log.d(TAG, "User hashCode=" + mUser.hashCode());
mUser.name = "Origin Name";
}
@Override
protected void onResume() {
super.onResume();
updateName();
}
public void updateName() {
mTextMessage.setText(mUser.name);
}
}
第二个Activity:
public class SecondActivity extends AppCompatActivity {
static final String TAG = "SecondActivity";
public static void start(Context context, User user) {
Intent starter = new Intent(context, SecondActivity.class);
starter.putExtra("user", user);
Log.d(TAG, "Put Bundle hashCode=" + starter.getExtras().hashCode());
context.startActivity(starter);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_second);
User user = getIntent().getParcelableExtra("user");
Log.d(TAG, "Receive Bundle hashCode=" + getIntent().getExtras().hashCode());
Log.d(TAG, "User hashCode=" + user.hashCode());
user.name = "Second Name";
Log.d(TAG, String.format("Change name to [%1$s]", user.name));
}
}
打印日志:
05-26 21:26:54.342 25537-25537/me.yimu.bundletest D/MainActivity: User hashCode=19193905
05-26 21:26:57.696 25537-25537/me.yimu.bundletest D/SecondActivity: Put Bundle hashCode=238724848
05-26 21:26:57.751 25537-25537/me.yimu.bundletest D/SecondActivity: Receive Bundle hashCode=13738260
05-26 21:26:57.751 25537-25537/me.yimu.bundletest D/SecondActivity: User hashCode=30268861
05-26 21:26:57.752 25537-25537/me.yimu.bundletest D/SecondActivity: Change name to [Second Name]
由上面的日志可以看到,Bundle 和 User的 hashCode 在传递前后都发生了变化,说明 Bundle 在通过 Intent 传递时传递的是深拷贝。因此,当使用Intent 传递 Bundle 数据时便有了大小的限制,不宜传递过大的对象,如 Bitmap 等。
Bundle 在 Fragment 间的传递
在上面代码的基础上,我们增加一个 SecondFragment.java ,并在fragment销毁时,刷新MainActivity 中的数据。
public class SecondFragment extends DialogFragment {
static final String TAG = "SecondFragment";
public static void show(FragmentManager manager, User user) {
SecondFragment fragment = new SecondFragment();
Bundle bundle = new Bundle();
bundle.putParcelable("user", user);
Log.d(TAG, "Put Bundle hashCode=" + bundle.hashCode());
fragment.setArguments(bundle);
fragment.show(manager, "");
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.fragment_second, container, false);
}
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
User user = getArguments().getParcelable("user");
Log.d(TAG, "Receive Bundle hashCode=" + getArguments().hashCode());
Log.d(TAG, "User hashCode=" + user.hashCode());
user.name = "Second Name";
Log.d(TAG, String.format("Change name to [%1$s]", user.name));
}
@Override
public void onDismiss(DialogInterface dialog) {
super.onDismiss(dialog);
((MainActivity) getActivity()).updateName();
}
}
日志:
05-26 21:27:23.351 25537-25537/me.yimu.bundletest D/MainActivity: User hashCode=25516232
05-26 21:27:24.437 25537-25537/me.yimu.bundletest D/SecondFragment: Put Bundle hashCode=72120544
05-26 21:27:24.447 25537-25537/me.yimu.bundletest D/SecondFragment: Receive Bundle hashCode=72120544
05-26 21:27:24.447 25537-25537/me.yimu.bundletest D/SecondFragment: User hashCode=25516232
05-26 21:27:24.448 25537-25537/me.yimu.bundletest D/SecondFragment: Change name to [Second Name]
神奇的事情发生了, Bundle 和 User 的 hashCode 都未发生改变,并且,在SecondFragment 中的修改影响到了上一个页面!
分析
Bundle 的 putParcelable 和 getParcelable 方法并未实际对对象进行序列化反序列化操作,真正的序列化过程发生在 Intent 数据的封装时期,Bundle 连同其中的数据被序列化了,这也体现了通过 Intent 实现 Android 跨进程通信的原理。
而 Fragment 中的 setArguments 方法只是把 Bundle 传递给了 Fragment ,没有经过 Intent 的封装,所以不会发生序列化与反序列化,传递的只是原数据的引用。
有啥子用呢?
你问我有没有用,当然有用啦!首先,Activity 和 Fragment ,或者 Fragment 和 Fragment 之间经常需要做一些数据同步,了解这个特性之后,通过setArguments方法设置参数后,只需要在前个页面重新展示时,比如onResume方法中对数据进行一次重新绑定即可,而不需要设置一些回调或者使用 EventBus,把对象传来传去了。当然,对象的大小也是没有任何限制的。
彩蛋
Q:Fragment 创建时向其传递数据的方式有哪两种?区别是什么?
A:可以通过setArguments或者自定义的setXXX 方法设置。前者需要传递的数据都是可序列化的,后者没有限制。并且后者会有一个潜在的问题,当 Fragment 异常销毁时,通过setArguments 保存的 Bundle 数据会自动保存并在重建时恢复,而后者不能,除非手工处理。
Q:既然setArguments 传递的是引用,那么为啥不直接传递,非得用 Bundle 包装一下。
A:原因很简单,因为 Fragment 销毁重建时数据恢复的需要。