Android 巧妙的序列化

背景

一次平常的灰度,突然出现了一个 crash,按照以往的经验开始排查,最后越排查越疑问,它是怎么触发的。怎么可能触发?有人黑了我们的 APP ?无数疑问涌上心头。
报错信息

java.lang.ClassCastException: java.util.HashMap cannot be cast to com.alibaba.fastjson.JSONObject
    at com.taobao.android.alimuise.page.MUSPageFragment.doInit(SourceFile:16)
    at com.taobao.android.alimuise.page.MUSPageFragment.onCreateView(SourceFile:14)

一个很平常的类型强转的错误,HashMap 被强转称为了 fastJson 的 JSONObject 类型。开始还以为是传递的参数传错了。定位到具体的代码发现不是那么简单。
//具体的代码, 获取 fragment 保存在 argument 中的参数。

if (initData == null) {
      initData = (JSONObject) bundle.getSerializable(KEY_INIT_DATA);
    }

这里确实是进行强转了,但是传入的时候只有JSONObject ,已经多次在代码里面检索,在创建这个 Fragment 的时候,只会传递 JSONObject。但是 JSONObject 为什么会变成了 HashMap 呢。带着疑问反复排查,由于没法复现,导致一直在猜测。就在没有头绪的时候,我的 leader 在监控上发现了一个 crash,这个 crash 是来自他的测试机的,说明他的手机出现了一次。经过他提示可以打开不保留活动来测试,终于找到了复现的完整方式。当 Acticity 重建的时候,它确实从我传递的 JSONObject 变成了 HashMap。

Activity 的销毁与重建

在一般的 Acitivity 创建的过程中,我们都知道 onCreate 方法是首先被调用的,重建的时候也是如此。在由于内存不足或者其他变更的时候,退到后台的 Activity 会被销毁(执行 onDestroy),当你返回到上一个 Aciticity 的时候就会触发重建。在启动 Activity 的时候有一个重要的参数 ActivityClientRecord 。这是记录一个 Activity 的所有信息,包含所在的进程名、应用包名、所在任务栈,以及 state ,state 是一个 bundle 类型的成员变量,是用来保存 Activity 状态的,当重建的时候需要通过 state 来进行恢复状态。

//ActivityThread.java
/**
     * Extended implementation of activity launch. Used when server requests a launch or relaunch.
     */
    @Override
    public Activity handleLaunchActivity(ActivityClientRecord r,
            PendingTransactionActions pendingActions, Intent customIntent) {
       
    }

销毁时发生了什么

图1

从上图中可以看到,当 Acticivity 执行 Stop 的时候,会回调 onSaveInstanceState,可以在这里存储一下你需要使用的变量。在这之后跨进程调用到 AMS ,执行了 activityStoped 方法。
在 acitivyStoped 中又会调用 ActivityRecord 的 activityStoped,将保存的数据存储在 AMS 进程里面的ActicityRecord 中。

//ActivityTaskManagerService.java
public final void activityStopped(IBinder token, Bundle icicle,
                                  PersistableBundle persistentState, CharSequence description) {
    final long origId = Binder.clearCallingIdentity();

    String restartingName = null;
    int restartingUid = 0;
    final ActivityRecord r;
    synchronized (mGlobalLock) {

        r = ActivityRecord.isInStackLocked(token);
        if (r != null) {
            //这里调用了
            r.activityStopped(icicle, persistentState, description);
        }
    }
}

ActivityRecord 执行 activityStopped ,会将传递的 bundle 进行保存起来,待恢复的时候会再传递到 APP 进程中,构建出一个 ActivityClientRecord 的实例进行 Acticity 的重建流程。

//ActivityRecord.java
 void activityStopped(Bundle newIcicle, PersistableBundle newPersistentState,
            CharSequence description) {
     
        if (newIcicle != null) {
            // If icicle is null, this is happening due to a timeout, so we haven't really saved
            // the state.
            setSavedState(newIcicle); //保存 Activity 的状态
            launchCount = 0;
            updateTaskDescription(description);
        }
    }

ActivityRecord 、 ActivityClientRecord 、Activity 关系?

它们三者是一一对应的关系,也就是 ActivityRecord 是在 AMS 进程中对一个 Activity 的描述,包含这个 Activity 的所有信息,而 ActivityClientRecord 和 Activity 都是处于 APP 进程中。在 ActivityRecord 中有一个参数appToken。它是继承自 IApplicationToken.Stub 这个接口,这个接口实现了 Binder,说明 appToken 具备了 IPC 通信的能力。它的主要作用可以通过它定位到具体的 Activity,在 Activity 启动的时候会将这个 appToken 传递到 App 的进程中,这样当需要调用 AMS 的方法的时候,再将 appToken 传递回来,AMS 进程可以通过它来定位到是谁在调用。

//ActivityRecord.java
   final ActivityTaskManagerService mAtmService;
  final ActivityRecord.Token appToken; //可以看到实现了Binder
  //可以看到 token 和 ActivityRecord 相互引用,可以相互定位。
  static class Token extends IApplicationToken.Stub {
       private WeakReference weakActivity;
       private final String name;
       private final String tokenString;
  }

Fragmet 状态是怎么保存的?
弄清楚 Acticity 的状态是怎么保存的,再来看一下 Fragment 的状态保存。


图2

fragment 中的状态保存都走到 FragmentManagerImpl 中的 saveAllSate 中

//FragmentManagerImpl.java
 Parcelable saveAllState() {
        // Make sure all pending operations have now been executed to get
        // First collect all active fragments.
        int size = mActive.size();
        ArrayList active = new ArrayList<>(size);
        boolean haveFragments = false;
        for (Fragment f : mActive.values()) {
            if (f != null) {
                haveFragments = true;
                FragmentState fs = new FragmentState(f);//这里保存了参数
                active.add(fs);
            }
      }
 }

可以看到 fragment 将状态保存在了 FragmentState 中,问题已经很明显了!保存在这个类的实例中的参数,在经历 IPC 传输之后,它变了。

final class FragmentState implements Parcelable {

    FragmentState(Fragment frag) {
        ...
        mArguments = frag.mArguments;
       ...
    }


    @Override
    public void writeToParcel(Parcel dest, int flags) {
         ...
        dest.writeBundle(mArguments);
       ..
    }
}

Parcelable 是 android 里面特有的序列化方式,执行序列化写入的时候,实现 Parcelable 的类,它的 writeToParcel 方法会被调用。

JsonObject 序列化问题

在对参数序列化的时候 ,Bundle 中的每一个 Value 都有对应的序列化方式,除了基础类型,Map 和 Serializable 都有对应的序列化方式,android SDK 已经自动帮我们完成了。
JSONObject 由于是实现了 Map 这个接口,同时也实现了 Serializable 接口。


图3

但是因为在 Parcel 写入的时候, Map 的判断在 Serializable 前面。会导致在 JSONObject 被当做 Map 类型进行写入。

//Parcel.java
   public final void writeValue(@Nullable Object v) {
        if(...){}
        else if (v instanceof Map) {
            //这里被当做了 Map 类型进行写入了。
            writeInt(VAL_MAP);
            writeMap((Map) v);
        }else{
            Class clazz = v.getClass();
            if (clazz.isArray() && clazz.getComponentType() == Object.class) {
                // Only pure Object[] are written here, Other arrays of non-primitive types are
                // handled by serialization as this does not record the component type.
                writeInt(VAL_OBJECTARRAY);
                writeArray((Object[]) v);
            } else if (v instanceof Serializable) {
                // Must be last
                writeInt(VAL_SERIALIZABLE);
                writeSerializable((Serializable) v);
            }else {...}
        }
   }

在读取的时候自然会变成了当做 MAP ,这样会导致你保存的参数明明是 JSONObject 却变成了 HashMap,如果你没有做类型校验会导致 crash。

//Parcel.java
 public final Object readValue(@Nullable ClassLoader loader) {
        int type = readInt();

        switch (type) {
            case VAL_NULL:
                return null;

            case VAL_STRING:
                return readString();

            case VAL_INTEGER:
                return readInt();

            case VAL_MAP:
                //读成了HashMap
                return readHashMap(loader);
            ...
        }
    }

结论

在给 Activity 或者 Fragment 传递参数不要使用 fastJson 的 JSONObject 。可以使用官方的 GSON 或者 map 以及自己去实现 Parcelable 的接口都可以。在发生一些难以排查的问题的时候,考虑复现不了的时候。可以尝试打开 android 设置里面的不保留活动,这样可以模拟内存不足的一些情况。

你可能感兴趣的:(Android 巧妙的序列化)