Android中的Bundle疑云——拷贝还是引用?

2,644 阅读4分钟
原文链接: yimu.me

众所周知,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 的 putParcelablegetParcelable 方法并未实际对对象进行序列化反序列化操作,真正的序列化过程发生在 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 销毁重建时数据恢复的需要。