内存泄露的检测和修复一直是每个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.info∗referencescom.common.util.image.BitmapDownloadInfo.imageLoadInterface∗referencescom.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的完整引用链,最终泄露是由于继承INetworkCallbackStub的匿名类没有被释放。通过函数名就可以知道INetworkCallbackStub是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(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 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 get(JNIEnv* env, jobject obj)

{

AutoMutex _l(mLock);

sp 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 跨进程内存泄露相关推荐

  1. android u盘读写权限,Android 外部SD卡/U盘无法写入解决方法(需要root)

    但今天我遇到一个问题,就是我买了只TF卡装上去以后发现:一般程序无法写入TF卡,而系统自带的文件工具能够写入. 什么原因呢? 好在这个平板已经是root的,马上调出rootexplorer文件管理器查 ...

  2. android zip文件读写,【Android】Zip文件解压方法

    android中zip文件解压 public class ZipUtil { /** * 解压到指定路径 * * @param inputStream * @param outPathString * ...

  3. Windows进程与线程学习笔记(九)—— 线程优先级/进程挂靠/跨进程读写

    Windows进程与线程学习笔记(九)-- 线程优先级/进程挂靠/跨进程读写 要点回顾 线程优先级 调度链表 分析 KiFindReadyThread 分析 KiSwapThread 总结 进程挂靠 ...

  4. linux 跨进程读取内存,Android之Linux跨进程通信的方式

    As we all know,Android是基于Linux内核开发的,而市面上几乎所有的App都离开跨进程通信.可能你会说Android是通过Binder完成进程之间的通信的.但是Binder是怎么 ...

  5. android listview 滑动黑屏,Android 跨进程启动Activity黑屏(白屏)的三种解决方案

    当Android跨进程启动Activity时,过程界面很黑屏(白屏)短暂时间(几百毫秒?).当然从桌面Lunacher启动一个App时也会出现相同情况,那是因为App冷启动也属于跨进程启动Activi ...

  6. 进程线程007 进程挂靠与跨进程读写内存

    文章目录 进程挂靠 进程与线程的关系 线程与进程如何关联 为什么需要ApcState.Process CR3的值可以随便改吗 分析NtReadVirtualMemory函数 总结 跨进程读写内存 跨进 ...

  7. Android之使用AIDL时的跨进程回调—Server回调Client

    首先建立在server端建立两个aidl文件 ITaskCallback.aidl 用于存放要回调client端的方法 package com.cmcc.demo.server; interface ...

  8. 13.跨进程读写内存

    跨进程的本质是"进程挂靠"正常情况下, A进程的线程只能访问A进程的地址空间,如果A进程的线程想访问B进程的地址空间,就要修改当前的Cr3的值为B进程的页目录表基值(KPROCES ...

  9. Android中获取系统内存信息以及进程信息-----ActivityManager的使用(一)

    本节内容主要是讲解ActivityManager的使用,通过ActivityManager我们可以获得系统里正在运行的activities,包括 进程(Process)等.应用程序/包.服务(Serv ...

最新文章

  1. iOS中你可能没有完全弄清楚的(一)synthesize
  2. 包浆网图分分钟变高清,伪影去除、细节恢复更胜前辈AI,下载可玩|腾讯ARC实验室出品...
  3. [概念型] 区块链包含术语概念【27术语整理汇总】
  4. Java 洛谷 P1307 数字反转
  5. bootstrap学习(五)代码
  6. 什么是 lnmp 实现原理。
  7. Oracle中Merge into的用法实例讲解
  8. ubuntu16.04下面使用graphviz
  9. 常用的正则表达式的运用--学习笔记(二)
  10. think-in-java(9)接口
  11. PHP 实现简单的 倒计时 时分秒
  12. mysql安装使用--2 用户管理
  13. 构造avl树_浅谈AVL树,B-树,B+树
  14. Android Studio 如何添加悬浮提示
  15. CTP: 为什么报网络原因发送失败,但连接却成功?
  16. js-YDUI 移动端解决方案
  17. java下载https的网络图片,添加安全证书方式
  18. jwplayer播放器初探
  19. 【IoT】产品设计之市场概念:市场定位、产品定位、市场需求、产品需求
  20. Md5码的生成及变种Md5码的生成

热门文章

  1. 网络编程学习笔记(gethostbyname2函数与IPv6支持)
  2. TabHost的使用(二):实现TabHost.TabContentFactory接口
  3. cad 关键字被保留了?选择集关键字保留了? N S W E关键字无法用?
  4. JS、CSS中的相对路径
  5. python3之subprocess常见方法使用
  6. 生成4位验证码(后台)
  7. 电商总结(八)如何打造一个小而精的电商网站架构
  8. QueryList的使用
  9. js 宽窄屏切换效果代码优化
  10. gdb InnoDB Redundant Row Format