Android 跨进程内存泄露

来源:互联网 发布:学校网络整合营销策划 编辑:程序博客网 时间:2024/05/22 16:10

内存泄露的检测和修复一直是每个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即可。

0 0