unmarshalling 解开数据的时候遇到的classNotFound
堆栈表现
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
S
a
v
e
d
S
t
a
t
e
,还有
V
e
r
t
i
c
a
l
V
i
e
w
P
a
g
e
r
SavedState,还有VerticalViewPager
SavedState,还有VerticalViewPagerSavedState,FragmentManagerState等等,反正就是所有用于保存页面状态信息的类都找不到。我们非常确定的是这些类肯定在APK中,那么ClassNotFound的原因盲猜也只能是Classloader的原因,下文我们会继续进行进一步分析。
环境信息
通过观察APMS上采集的环境信息大概发现以下特征:
操作系统版本:主要在28和29,也就是Android 9和10上。
发生时间:大多发生在进程创建的1-10s内,属于启动崩溃。
页面信息:这个Crash可能发生在任何Activity, 也就是说一个Activity只要是重建恢复的情况下就可能会有这个问题。
目前快速获得的这些信息不足以确定什么,所以接下来我们还是要从Crash堆栈入手分析可能的原因,另外可以尝试本地复现,在Android9和10上尝试造一个 Application重建并且Activity也有重建恢复的信息的情况。
源码分析
直接从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;
}
到这里我们的问题就更加具化了一些,可能有如下两个问题:
变成了为什么内层Bundle的ClassLoader会错误?内层Bundle的ClassLoader是怎么决定的呢?
那外层Bundle对象savedInstanceState它的ClassLoader对不对呢?外层Bundle对象的ClassLoader是怎么决定呢?
但不管内层还是外层,反正都是Bundle对象,看下Bundle的ClassLoader是怎么决定准没错。
Bundle对象的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) null, 0);
}
BaseBundle(@Nullable ClassLoader loader, int capacity) {
mMap = capacity > 0 ?
new ArrayMap<String, Object>(capacity) : new ArrayMap<String, Object>();
//如果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对象,而且我相信一定不是开发者本意要访问这个,肯定是间接的,因为要想访问这个对象也不是容易的事情。
END
有了猜想我们就开始验证,我们本地已经能复现了,那么验证的思路很简单,就是在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删除日志么,这个调用还会有么,这就涉及到另一个知识点了。
番外之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);
}