Android 跨进程内存泄露

内存泄露的检测和修复一直是每个APP的重点和难点,也有很多文章讲述了如何检测和修复。本篇文章
结合最近开发的项目遇到的实例,讲述下Android Binder导致的内存泄露的一个案例。

发现问题

参与的项目在最近的版本接入了一个开源的内存检测工具LeakCanary,在提交给QA测试验证后。
瞬间检测出来N多的内存泄露,XXXActivity泄露,XXXActivity泄露…坑爹的是,这种泄露还不是必现的。好在堆栈都基本一样,随便拉一个出来分享吧
* com.ui.theme.ThemeListActivity has leaked:
* GC ROOT com.business.netscene.NetSceneBase1.this0 (anonymous class extends com.data.network.framework.INetworkCallbackStub)referencescom.common.util.image.SceneBitmapDownload.inforeferencescom.common.util.image.BitmapDownloadInfo.imageLoadInterfacereferencescom.ui.common.RoundedImageView4.this$0 (anonymous class implements com.common.util.image.ImageLoadInterface)
* references com.ui.common.RoundedImageView.mContext
* leaks com..ui.theme.ThemeListActivity instance

定位问题

通过堆栈信息可以清楚的看到Activity到GCRoot的完整引用链,最终泄露是由于继承INetworkCallbackStubINetworkCallbackStub是Android自动生成的用于跨进程通信的框架,到对应的NetSceneBase查看对应的代码:

    private INetworkCallback.Stub networkCallback = new INetworkCallback.Stub()

        @Override
        public void onResult(int errType, int respCode, WeMusicCmdTask task)
                throws RemoteException {
            NetSceneBase.this.onResult(errType, respCode, task);
        }

        @Override
        public void onWorking(long progress, long total) throws RemoteException {
            NetSceneBase.this.onProgress( progress, total );
        }

    };

接着再查找networkCallback的引用发现,除了跨进程传递给网络进程外没有其他任何地方引用了networkCallback。
而网络进程在完成相应的网络请求后,便将networkCallback置null,那这里的GC ROOT又是怎么回事呢?
继续看代码,networkCallback是跨进程传递给网络进程的,所以查看AIDL自动生成的代码:

@Override public boolean send(com.data.network.WeMusicCmdTask task, com.data.network.framework.INetworkCallback callback) throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
boolean _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
if ((task!=null)) {
_data.writeInt(1);
task.writeToParcel(_data, 0);
}
else {
_data.writeInt(0);
}
_data.writeStrongBinder((((callback!=null))?(callback.asBinder()):(null)));
mRemote.transact(Stub.TRANSACTION_send, _data, _reply, 0);
_reply.readException();
_result = (0!=_reply.readInt());
if ((0!=_reply.readInt())) {
task.readFromParcel(_reply);
}
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}

跨进程传输必须用到Parcel,在这段代码里有这句_data.writeStrongBinder((((callback!=null))?(callback.asBinder()):(null)));
而这个_data就是Java层的Parcel对象。PS:这里的callback其实是一个Binder对象,而Binder对象构造函数里面有如下这段代码

    public Binder() {
        init();

        if (FIND_POTENTIAL_LEAKS) {
            final Class<? extends Binder> klass = getClass();
            if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                    (klass.getModifiers() & Modifier.STATIC) == 0) {
                Log.w(TAG, "The following Binder class should be static or leaks might occur: " +
                    klass.getCanonicalName());
            }
        }
    }

可以看到如果Binder对象是匿名类、内部成员类或者是局部类就有可能出现内存泄露。
接着往下看

public final void writeStrongBinder(IBinder val) {
        //调用native方法
        nativeWriteStrongBinder(mNativePtr, val);
    }
static void android_os_Parcel_writeStrongBinder(JNIEnv* env, jclass clazz, jint nativePtr, jobject object)  
{  
    Parcel* parcel = reinterpret_cast<Parcel*>(nativePtr);  
    if (parcel != NULL) {
        //ibinderForJavaObject,这里的object就是对应java层IBinder也就是networkCallback
        const status_t err = parcel->writeStrongBinder(ibinderForJavaObject(env, object));  
        if (err != NO_ERROR) {  
            signalExceptionForError(env, clazz, err);  
        }  
    }  
} 
sp<IBinder> ibinderForJavaObject(JNIEnv* env, jobject obj)  
{  
    if (obj == NULL) return NULL;  
    //这里obj是Java层的Binder对象,走下面这部分逻辑。最后调用jbh->get获得native层的IBinder对象指针。
    if (env->IsInstanceOf(obj, gBinderOffsets.mClass)) {  
        JavaBBinderHolder* jbh = (JavaBBinderHolder*)env->GetIntField(obj, gBinderOffsets.mObject);  
        return jbh != NULL ? jbh->get(env, obj) : NULL;  
    }  
    if (env->IsInstanceOf(obj, gBinderProxyOffsets.mClass)) {  
        return (IBinder*)env->GetIntField(obj, gBinderProxyOffsets.mObject);  
    }  
    ALOGW("ibinderForJavaObject: %p is not a Binder object", obj);  
    return NULL;  
}
sp<JavaBBinder> get(JNIEnv* env, jobject obj)  
{  
    AutoMutex _l(mLock);  
    sp<JavaBBinder> b = mBinder.promote();  
    if (b == NULL) {  
        b = new JavaBBinder(env, obj);  
        mBinder = b;  
        ALOGV("Creating JavaBinder %p (refs %p) for Object %p, weakCount=%d\n",  
             b.get(), b->getWeakRefs(), obj, b->getWeakRefs()->getWeakCount());  
    }  

    return b;  
}

JavaBBinder(JNIEnv* env, jobject object)  
    : mVM(jnienv_to_javavm(env)), mObject(env->NewGlobalRef(object))  
    //here,创建了一个全局引用,如不主动调用env->DeleteGlobalRef(object),Java层的对象也就是networkCallback就不会被释放。
{  
    ALOGV("Creating JavaBBinder %p\n", this);  
    android_atomic_inc(&gNumLocalRefs);  
    incRefsCreated(env);  
}

解决问题

定位到问题之后就好办,这里networkCallback是由于nativ层引用了导致无法释放,那系统什么时候才能释放这部分内存呢。
结论是当网络进程的netwCallback执行finalize(),也就是网络进程对其进行垃圾回收的时候,native层才不会引用到主进程的networkCallback。所以,主进程也不是每次检测都会泄露,过段时间网络进程进行GC后,对应的Activity也就被回收了。但其实网络进程用到的内存资源是很少的也是比较稳妥,网络进程可能会很长一段时间不进行GC。那么我们能做的就是,在网络请求完成后切断networkCall与上层的引用,避免Activity的泄露。查看上面的引用链,networkCall是网络进程和主进程通讯的接口,imageLoadInterface是业务层和UI的接口。切断这两个引用的任何一个都可以避免底层的内存泄露进一步导致Activity的泄露,从这里也是看出RoundedImageView这个控件编码也有问题。
最后解决方法是,networkCallback不再以匿名内部类实现,而是单独以一个类实现然后将NetSceneBase以参数的形式传递给NetworkCallback,在网络请求结束后将netSceneBase置null。

总结

以上就是这个case从发现到解决的全部过程,可以看出导致内存泄露的原因有两个
1.忽略了Android底层组件的工作机制以及各个对象的生命周期。
2.上层逻辑编码问题,imageLoadInterface接口没有及时注销。
PS:文章中使用的工具LeakCanary,DDMS和MAT还是很强大的,具体用法google即可。

阅读更多
个人分类: android
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

不良信息举报

Android 跨进程内存泄露

最多只允许输入30个字

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭