查看原文
其他

凡猿修仙传:斩杀ClassNotFoundException when unmarshalling Crash

三雒 鸿洋
2024-08-24

本文作者


作者:三雒

链接:

https://juejin.cn/post/7385736245090615333

本文由作者授权发布。


1写在前面


大家好,我是三雒(luo)。今天水一篇最近我们应用内发生的大规模Crash, 整体的分析和解决过程还比较有趣,涉及的小知识细节比较多,分享给大家。

2Crash现场分析

堆栈信息

android.os.BadParcelableException: ClassNotFoundException when unmarshalling: androidx.recyclerview.widget.RecyclerView$SavedState
at android.os.Parcel.readParcelableCreator (Parcel.java:2847)
at android.os.Parcel.readParcelable (Parcel.java:2768)
at android.os.Parcel.readValue (Parcel.java:2671)
at android.os.Parcel.readSparseArrayInternal (Parcel.java:3126)
at android.os.Parcel.readSparseArray (Parcel.java:2354)
at android.os.Parcel.readValue (Parcel.java:2728)
at android.os.Parcel.readArrayMapInternal (Parcel.java:3045)
at android.os.BaseBundle.initializeFromParcelLocked (BaseBundle.java:288)
at android.os.BaseBundle.unparcel (BaseBundle.java:232)
at android.os.Bundle.getSparseParcelableArray (Bundle.java:1010)
at com.android.internal.policy.PhoneWindow.restoreHierarchyState (PhoneWindow.java:2133)
at android.app.Activity.onRestoreInstanceState (Activity.java:1173)
at android.app.Activity.performRestoreInstanceState (Activity.java:1128)
at android.app.Instrumentation.callActivityOnRestoreInstanceState (Instrumentation.java:1318)
at android.app.ActivityThread.handleStartActivity (ActivityThread.java:3016)
at android.app.servertransaction.TransactionExecutor.performLifecycleSequence (TransactionExecutor.java:185)
at android.app.servertransaction.TransactionExecutor.cycleToPath (TransactionExecutor.java:170)
at android.app.servertransaction.TransactionExecutor.executeLifecycleState (TransactionExecutor.java:147)
at android.app.servertransaction.TransactionExecutor.execute (TransactionExecutor.java:73)
at android.app.ActivityThread$H.handleMessage (ActivityThread.java:1858)
at android.os.Handler.dispatchMessage (Handler.java:106)
at android.os.Looper.loop (Looper.java:201)
at android.app.ActivityThread.main (ActivityThread.java:6820)
at java.lang.reflect.Method.invoke (Method.java:-2)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:547)
at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:922)

Crash堆栈如上,我们看堆栈初步能得出来的信息如下:

  • Crash时机:Crash发生在Activity恢复重建的情况下执行 onRestoreInstanceState 方法时。
  • Crash原因:正如message 所示原因是类找不到,而且堆栈里有很多个类不止是RecyclerView$SavedState,还有VerticalViewPager$SavedStateFragmentManagerState等等,反正就是所有用于保存页面状态信息的类都找不到。我们非常确定的是这些类肯定在APK中,那么ClassNotFound的原因盲猜也只能是Classloader的原因,下文我们会继续进行进一步分析。
接下来看一些Crash时候的环境信息是否能帮我们快速定位问题。

环境信息

通过观察APMS上采集的环境信息大概发现以下特征:
  • 操作系统版本:主要在28和29,也就是Android 9和10上。
  • 发生时间:大多发生在进程创建的1-10s内,属于启动崩溃。
  • 页面信息:这个Crash可能发生在任何Activity, 也就是说一个Activity只要是重建恢复的情况下就可能会有这个问题。

目前快速获得的这些信息不足以确定什么,所以接下来我们还是要从Crash堆栈入手分析可能的原因,另外可以尝试本地复现,在Android9和10上尝试造一个 Application重建并且Activity也有重建恢复的信息的情况。

3从源码分析原因


直接从Activity.onRestoreInstanceState方法看起 ,可以看到这里是从savedInstanceState中获取key为android:viewHierarchyState的内部Bundle对象,它记录了window的状态,将它交给PhoneWindow去做window恢复的操作。
private static final String WINDOW_HIERARCHY_TAG = "android:viewHierarchyState";

protected void onRestoreInstanceState(Bundle savedInstanceState) {
    if (mWindow != null) {
        Bundle windowState = savedInstanceState.getBundle(WINDOW_HIERARCHY_TAG);
        if (windowState != null) {
            mWindow.restoreHierarchyState(windowState);
        }
    }
}

接下来看PhoneWindow的restoreHierarchyState方法,这个方法本身的逻辑并不重要,只是看crash时候是在调用内部Bundle对象windowState的getSparseParcelableArray方法对时候Crash的,这里是内部Bundle对象这一点很重要。

@Override
public void restoreHierarchyState(Bundle savedInstanceState) {
    if (mContentParent == null) {
        return;
    }
    //调用windowState的getSparseParcelableArray方法
    SparseArray<Parcelable> savedStates
            = savedInstanceState.getSparseParcelableArray(VIEWS_TAG);
    if (savedStates != null) {
        mContentParent.restoreHierarchyState(savedStates);
    }
   // ...
}

再继续看Bundle对象的getSparseParcelableArray方法,发现其第一行调用了unparcel(),也就是在执行这个unparcel时候失败的。

public <T extends Parcelable> SparseArray<T> getSparseParcelableArray(@Nullable String key) {
    unparcel();
    Object o = mMap.get(key);
    if (o == null) {
        return null;
    }
    try {
        return (SparseArray<T>) o;
    } catch (ClassCastException e) {
        typeWarning(key, o, "SparseArray", e);
        return null;
    }
}

我们看过Bundle代码之后,就会发现无论读写Bundle对象的内容,都会先调用unparcel这个方法,它的作用就是反序列化的核心,将二进制格式的数据转换成内存中的Java对象,所以这个过程中必然是要进行类加载创建对象的,也就是在这个过程中出现ClassNotFoundException

void unparcel() {
    synchronized (this) {
        final Parcel source = mParcelledData;
        if (source != null) {
            initializeFromParcelLocked(source, /*recycleParcel=*/  true , mParcelledByNative);
        } else {
            if (DEBUG) {
                Log.d(TAG, "unparcel "
                        + Integer.toHexString(System.identityHashCode(this))
                        + ": no parcelled data");
            }
        }
    }
}

我们直接略去中间的代码,看最后报错的函数readParcelableCreator,可以看出就是在Class.forName进行类加载时候报错的,原因也就是parcelableClassLoader不对,不是PathClassLoader, 我们也可以猜测其就是用了BootClassLoader

public final Parcelable.Creator<?> readParcelableCreator(@Nullable ClassLoader loader) {
    String name = readString();
    if (name == null) {
        return null;
    }
    Parcelable.Creator<?> creator;
    synchronized (mCreators) {
        //...
        if (creator == null) {
            try {

                ClassLoader parcelableClassLoader =
                        (loader == null ? getClass().getClassLoader() : loader);
                //报错的这一行代码,可见是因为 parcelableClassLoader 不对
                Class<?> parcelableClass = Class.forName(name, false /* initialize */,
                        parcelableClassLoader);
                //...
            }
            catch (ClassNotFoundException e) {
                Log.e(TAG, "Class not found when unmarshalling: " + name, e);
                throw new BadParcelableException(
                        "ClassNotFoundException when unmarshalling: " + name);
            }
            //...
            map.put(name, creator);
        }
    }

    return creator;
}

到这里我们的问题就更加具化了一些,可能有如下两个问题:

  1. 变成了为什么内层Bundle的ClassLoader会错误?内层Bundle的ClassLoader是怎么决定的呢?
  2. 那外层Bundle对象savedInstanceState它的ClassLoader对不对呢?外层Bundle对象的ClassLoader是怎么决定呢?

但不管内层还是外层,反正都是Bundle对象,看下Bundle的ClassLoader是怎么决定准没错。

4Bundle对象的ClassLoader决定逻辑

new Bundle() 对象默认使用BootClassLoader

在创建Bundle对象时候默认不会给Bundle指定ClassLoader, 它会用BaseBundle这个类的ClassLoader,作为一个Android FrameWork的Class,它的ClassLoader当然是BootClassLoader。
public Bundle() {
    super();
    mFlags = FLAG_HAS_FDS_KNOWN | FLAG_ALLOW_FDS;
}

BaseBundle() {
    this((ClassLoader) null0);
}
BaseBundle(@Nullable ClassLoader loader, int capacity) {
    mMap = capacity > 0 ?
            new ArrayMap<StringObject>(capacity) : new ArrayMap<StringObject>();
    //如果loader为null,使用的是当前类的ClassLoader,也就是BootClassLoader     
    mClassLoader = loader == null ? getClass().getClassLoader() : loader;
}


那如果不Crash的话肯定是使用的PathClassLoader啊,不然加载不到APK里的类,那到底什么时候改变的呢?

Bundle还有一个setClassLoader的方法,我们只要看启动过程中在哪里调用即可。
public void setClassLoader(ClassLoader loader) {
    super.setClassLoader(loader);
}


内层Bundle对象ClassLoader由外层Bundle对象决定
我们在上文其实讲过Bundle的unparcel作用了,反序列化过程会按照存储对象的类型分别进行,这个过程在Parcel.readValue方法中, 其中解析Bundle类型对象会调用到readBundle方法。
public final Object readValue(@Nullable ClassLoader loader) {
    int type = readInt();
    switch (type) {
    case VAL_STRING:
        return readString();

    case VAL_INTEGER:
        return readInt();

    case VAL_MAP:
        return readHashMap(loader);
    //解析Bundle类型对象
    case  VAL_BUNDLE :
 return readBundle(loader);

}

而内层Bundle的ClassLoader也就是在readBundle时候设置的,这里的loader对象是由外层Bundle对象传入自己的,而且Bundle对象只会unparcel一次,后面就不会再执行了,所以一旦外层Bundle对象unparcel过内层的Bundle对象的ClassLoader就决定了。

public final Bundle readBundle(@Nullable ClassLoader loader) {
    int length = readInt();
    if (length < 0) {
        if (Bundle.DEBUG) Log.d(TAG, "null bundle: length=" + length);
        return null;
    }

    final Bundle bundle = new Bundle(this, length);
    if (loader != null) {
       // 设置classLoader
       bundle.setClassLoader(loader);
    }
    return bundle;
}

到这里我们可以大胆怀疑是因为外层的ClassLoader弄错了,所以导致内层的也错了,这不就是和尚头上的虱子明摆着么。但是我们经过努力复现在还真在本地复现了一个case,在Activity.onRestoreInstanceState 方法处打的断点,发现savedInstanceState的ClassLoader是PathClassLoader(图上右下角),也就是是正确的,只是内部Bundle对象的ClassLoader是错误的。


这就丈二和尚摸不着头脑了,我们不妨先看看savedInstanceState自己的ClassLoader是在什么时候设置的。

外层savedInstanceState的ClassLoader启动Activity时决定

经过查阅资料发现,其实是在ActivityThread.performLaunchActivity启动Activity之后手动设置成了PathClassLoader, 代码如下:
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    //...
    try {
        java.lang.ClassLoader cl = appContext.getClassLoader();
        //通过Instrumentation创建Activity对象
        activity = mInstrumentation.newActivity(
                cl, component.getClassName(), r.intent);
        StrictMode.incrementExpectedActivityCount(activity.getClass());
        r.intent.setExtrasClassLoader(cl);
        r.intent.prepareToEnterProcess(isProtectedComponent(r.activityInfo),
                appContext.getAttributionSource());
        if (r.state != null) {
            //设置savedInstanceState的ClassLoader为PathClassLoader
           r.state.setClassLoader(cl);
        }
    } catch (Exception e) {
        if (!mInstrumentation.onException(activity, e)) {
            throw new RuntimeException(
                "Unable to instantiate activity " + component
                + ": " + e.toString(), e);
        }
    }
    //...

    return activity;
}

那么正常情况下我们在肯定是在这个方法之后才会去读取Bundle对象内容进行unparcel, 内层的Bundle对象ClassLoader就一定没问题,事实上正常情况下最早unparcel saveInstanceState是在Activity onCreate阶段, performCreate会执行到restoreHasCurrentPermissionRequest 方法读取bundle进而触发unparcel。

private void restoreHasCurrentPermissionRequest(Bundle bundle) {
    if (bundle != null) {
        mHasCurrentPermissionsRequest = bundle.getBoolean(
                HAS_CURENT_PERMISSIONS_REQUEST_KEY, false);
    }
}
那我们这里外层Bundle对象的ClassLoader为 PathClassLoader, 内层Bundle对象为BootClassLoader的合理怀疑就是有人在performLaunchActivity还没执行之前就防访问了saveInstanceState对象,而且我相信一定不是开发者本意要访问这个,肯定是间接的,因为要想访问这个对象也不是容易的事情。
5最终归因


有了猜想我们就开始验证,我们本地已经能复现了,那么验证的思路很简单,就是在Bundle的 unparcel方法处加一个条件断点,savedInstanceState 内部有个 key为android:viewHierarchyState的对象,所以很好加条件过滤。


如上图所示,发现在我们应用Application.onCreate方法中会读取这个Bundle,这个代码大致是遍历主线程的消息队列,解析Message对象从中获取要启动的Activity信息,但是不巧的是它在这里加了一行打印message的日志,这个messages.toString()会调用到LaunchActivityItem.hashCode
private fun extractIntent(messages: Message): Intent? {
    //这行日志是罪魁祸首
    Log.d(TAG, "messages: ${messages} obj: ${messages.obj?. javaClass } " )
    if (messages.obj == null) {
        Log.d(TAG, "messages.obj is null")
        return null
    }
    //...
 }

好巧不巧LaunchActivityItem.hashCode这个方法里会调用到saveInstanceState.size() 从而unparcel。

@Override
public int hashCode() {
    int result = 17;
    result = 31 * result + mIntent.filterHashCode();
    result = 31 * result + mIdent;
    result = 31 * result + Objects.hashCode(mCurConfig);
    result = 31 * result + Objects.hashCode(mOverrideConfig);
    result = 31 * result + Objects.hashCode(mCompatInfo);
    result = 31 * result + Objects.hashCode(mReferrer);
    result = 31 * result + Objects.hashCode(mProcState);
    //mState也就是saveInstanceState对象
    result = 31 * result + (mState != null ? mState.size() : 0 );
    result = 31 * result + (mPersistentState != null ? mPersistentState.size() : 0);
    result = 31 * result + Objects.hashCode(mPendingResults);
    result = 31 * result + Objects.hashCode(mPendingNewIntents);
    result = 31 * result + (mIsForward ? 1 : 0);
    result = 31 * result + Objects.hashCode(mProfilerInfo);
    result = 31 * result + Objects.hashCode(mAssistToken);
    return result;
}

到这里我们Crash的原因也就确定了,就是由于无心的这么一行日志。

那么有同学提出疑问了,我们不是在正式包中有通过R8删除日志么,这个调用还会有么,这就涉及到另一个知识点了。

6番外之Log代码删除


无论是Proguard还是R8我们都可以使用assumenosideeffects指令去指定一些函数调用时无用的,删除之后也对应用运行没有副作用,从而起到优化包体积以及运行时性能的作用。
-assumenosideeffects class android.util.Log {
    public static boolean isLoggable(java.lang.String, int);
    public static int v(...);
    public static int i(...);
    public static int w(...);
    public static int d(...);
    public static int e(...);
}

但是上面的配置只会删除Log.x方法调用指令本身,并不会把整个都删除掉。Log.d(TAG, "messages:𝑚𝑒𝑠𝑠𝑎𝑔𝑒𝑠𝑜𝑏𝑗:messagesobj:{messages.obj?.javaClass}") 这行代码其实会生成很多的指令,大致如下所示,没错就是这么多。

ldc "SHOOK"
    _new 'java/lang/StringBuilder'
    dup
    invokespecial 'java/lang/StringBuilder.<init>','()V'
    ldc "messages:"
    invokevirtual 'java/lang/StringBuilder.append','(Ljava/lang/String;)Ljava/lang/StringBuilder;'
    aload 0
    invokevirtual 'java/lang/StringBuilder.append','(Ljava/lang/Object;)Ljava/lang/StringBuilder;'
    ldc "obj:"
    invokevirtual 'java/lang/StringBuilder.append','(Ljava/lang/String;)Ljava/lang/StringBuilder;'
    aload 0
    getfield 'android/os/Message.obj','Ljava/lang/Object;'
    invokevirtual 'java/lang/Object.getClass','()Ljava/lang/Class;'
    invokevirtual 'java/lang/StringBuilder.append','(Ljava/lang/Object;)Ljava/lang/StringBuilder;'
    invokevirtual 'java/lang/StringBuilder.toString','()Ljava/lang/String;'
    invokestatic 'android/util/Log.i' , '(Ljava/lang/String;Ljava/lang/String;)I'
    pop

而上述的配置只会删除最后一行的invokestatic ****静态方法调用,其他的都还会保留,之所以Progurad不删除这些代码是因为它无法判定这些方法调用是否有外部副作用, 比如StringBuilder.append 方法会不会修改一些其他在使用的对象,因为这是一个Android Platform的方法Proguard并不会对其进行分析。但我们是可以判断这个方法内部不会修改其他任何在使用的对象的,完全可以删除掉,可以通过如下的指令告诉R8或者Proguard进行删除。

-assumenoexternalsideeffects class java.lang.StringBuilder {
    public java.lang.StringBuilder();
    public java.lang.StringBuilder(int);
    public java.lang.StringBuilder(java.lang.String);
    public java.lang.StringBuilder append(java.lang.Object);
    public java.lang.StringBuilder append(java.lang.String);
    public java.lang.StringBuilder append(java.lang.StringBuffer);
    public java.lang.StringBuilder append(char[]);
    public java.lang.StringBuilder append(char[], int, int);
    public java.lang.StringBuilder append(boolean);
    public java.lang.StringBuilder append(char);
    public java.lang.StringBuilder append(int);
    public java.lang.StringBuilder append(long);
    public java.lang.StringBuilder append(float);
    public java.lang.StringBuilder append(double);
    public java.lang.String toString();
}

-assumenoexternalreturnvalues public final class java.lang.StringBuilder {
    public java.lang.StringBuilder append(java.lang.Object);
    public java.lang.StringBuilder append(java.lang.String);
    public java.lang.StringBuilder append(java.lang.StringBuffer);
    public java.lang.StringBuilder append(char[]);
    public java.lang.StringBuilder append(char[], int, int);
    public java.lang.StringBuilder append(boolean);
    public java.lang.StringBuilder append(char);
    public java.lang.StringBuilder append(int);
    public java.lang.StringBuilder append(long);
    public java.lang.StringBuilder append(float);
    public java.lang.StringBuilder append(double);
}


最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!


扫一扫 关注我的公众号

如果你想要跟大家分享你的文章,欢迎投稿~


┏(^0^)┛明天见!

继续滑动看下一个
鸿洋
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存