近期在对游戏进行内存优化时,bugly上出现一个较为其他的OOM问题:跨进程通讯Parcel 通讯发生OOM。
java堆栈:
源码分析过程:
锁定Parcel 的nativeWriteString16()开始查看。
Parcel: http://aospxref.com/android-11.0.0_r21/xref/frameworks/base/core/jni/android_os_Parcel.cpp#android_os_Parcel_writeString16
这个nativeWriteString16()函数对应的jni 层中的android_os_Parcel_writeString16()函数:
接下来看下android_os_Parcel_writeString16():
297 static void android_os_Parcel_writeString16(JNIEnv* env, jclass clazz, jlong nativePtr, jstring val)298 {
299 Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr); //先获取到c++层Parcel指针对象
300 if (parcel != NULL) {
301 status_t err = NO_MEMORY;
302 if (val) {
//接着,获取Java层String的指针
303 const jchar* str = env->GetStringCritical(val, 0);
304 if (str) {//获取成功
//接着通过Parce指针对象的writeString16()继续写入
305 err = parcel->writeString16(
306 reinterpret_cast<const char16_t*>(str),
307 env->GetStringLength(val));
308 env->ReleaseStringCritical(val, str);
309 }
//注意点:若代码执行到这里,则表明存在内存异常,获取失败,这时erro 是 NO_MEMORY
310 } else {
//因java层string 是空对象,这里写入空值
311 err = parcel->writeString16(NULL, 0);
312 }
313 if (err != NO_ERROR) {//存在err异常状态,则抛出对应的异常。
314 signalExceptionForError(env, clazz, err);
315 }
316 }
317 }
从以上代码可知: 首先获取c++层Parcel指针对象,接着获取Java层String的指针,最后通过Parce指针对象的writeString16()
继续写入。
先看下signalExceptionForError()
结合以上源码可知,执行nativeWriteString16()后,error是为NO_MEMORY,从而程序导致抛出OOM异常。
导致error是为NO_MEMORY 为的原因可能有:
- 执行
env->GetStringCritical(val, 0)
,存在内存溢出,返回Null 所导致; - 执行
parcel->writeString16()
返回所导致;
先简单来了解下c++ Parcel类:其内部也是有buffer数据缓存的设计,有capacity容量,pos位置等等
再通过一张图来了解下这几个概念:
但在初始化构造函数时,默认都是为0的,等真正使用时候,才会计数赋值:
接下来, 继续查看Parcel的writeString16()
:
http://aospxref.com/android-11.0.0_r21/xref/frameworks/native/libs/binder/Parcel.cpp#1036
1036 status_t Parcel::writeString16(const char16_t* str, size_t len){
1038 if (str == nullptr) return writeInt32(-1);//若java层string是空,则写入-1标识
//更新 mDataPos位置 ,存在扩容growData()可能返回NO_MEMORY
1039 status_t err = writeInt32(len);
1041 if (err == NO_ERROR) {
1042 len *= sizeof(char16_t);
//writeInplace计算复制数据的目标所在的地址
1043 uint8_t* data = (uint8_t*)writeInplace(len+sizeof(char16_t));
1044 if (data) {
//根据地址拷贝数据过去,实现跨进程通讯
1045 memcpy(data, str, len);
1046 *reinterpret_cast<char16_t*>(data+len) = 0;
1047 return NO_ERROR;
1048 }
1049 err = mError;
1050 }
1051 return err;
1052 }
每个进程的内存是私有的,不共享。像传递int类型(1)这样的数据,多进程直接进行拷贝就可以,但引用对象,在内存中是地址0xxxx,在其他进程中是不存在,无法直接拷贝过去。因此需要进程A需要将数据进行数据打包(计算出地址+将数据拷贝到该地址),而进程B进行数据解析还原(拿到地址+将地址中数据读取到来)从而实现对引用类型数据的传递。
writeInplace()
是计算出地址的过程:
接下来看下growData()
:若是条件允许,则会扩容需要大小的1.5倍。
从上面可知,当需要扩容的长度超出指定范围,也会返回NO_MEMORY
,从而导致OOM.
Parcel发生OOM的情况有两种:
- 执行
env->GetStringCritical(val, 0)
,存在内存溢出,返回Null 所导致; - 执行
growData()
进行扩容导致,超出限制长度,所导致
推断分析:
通过源码知道发生原因,在去bugly上查找有用的日志,发现有一个很关键的日志:
结论:进程虚拟内存即将达到4G峰值,与此同时,正在进行跨进程parcel 通讯,执行到env->GetStringCritical() 获取java层String指针,内存溢出,返回为Null ,从而抛出OOM。