这是一个在项目中遇到的一个内存泄露,由于隐藏的较深,定位与解决花费了近两天时间[大哭]。特记录其排查与解决过程。
复制代码
由于如今的Android应用大多要适配android6.0新增的运行时权限检查,因此一般都会在首次启动时,Splash 闪屏页进行权限申请。而大多都用了开源库作这件事。公司某项目就用了com.yanzhenjie.permission:support:2.0.1
来进行权限申请。一次我检查Bitmap OOM的问题时,用了AndroidStudio 的profile 内存监控工具, 发现有一张占用大的图片。 html
abstract class ThemedResourceCache<T> {
private ArrayMap<ThemeKey, LongSparseArray<WeakReference<T>>> mThemedEntries;
private LongSparseArray<WeakReference<T>> mUnthemedEntries;
private LongSparseArray<WeakReference<T>> mNullThemedEntries;
复制代码
而弱引用是当发现GC时,会直接回收的。因此这张Bitmap不就在内存中,而后又发现原来是整个SplashActivity都没有被释放掉:java
AndPermission.with(this).runtime().permission(permissions)
.onGranted(data -> {
requestReadPhonePermission();
}).onDenied(permissions1 -> {
if (AndPermission.hasAlwaysDeniedPermission(SplashActivity.this, permissions1)) {
Toast.makeText(SplashActivity.this, "拒绝", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(SplashActivity.this, "永不容许", Toast.LENGTH_SHORT).show();
}
}).start();
复制代码
而后我想看看为何泄露,就先看了这个库的源码,大体原理以下。全部的请求会包装成BridgeRequest对象,里面有activity引用 里面有一个单例RequestManager,里有一个线程RequestExecutor,那么这线程也是单例的。 线程有一个队列private final BlockingQueue<BridgeRequest> mQueue;
线程中不断从queue取数据,取出,注册广播,而且启动一个透明activity:BridgeActivity,在bridgeAct中申请权限,那么结果也是bridge中onRequestPermissionsResult 中,而后发送一个广播,这边再收到广播,onCallback回调给咱们的调用上层。 核心代码以下: RequestManager:android
public class RequestManager {
private static RequestManager sManager;
public static RequestManager get() {
if (sManager == null) {
synchronized (RequestManager.class) {
if (sManager == null) {
sManager = new RequestManager();
}
}
}
return sManager;
}
private final BlockingQueue<BridgeRequest> mQueue;
private RequestManager() {
this.mQueue = new LinkedBlockingQueue<>();
new RequestExecutor(mQueue).start();
}
public void add(BridgeRequest request) {
mQueue.add(request);
}
}
复制代码
RequestExecutor:git
/**
* Created by Zhenjie Yan on 2/13/19.
*/
final class RequestExecutor extends Thread implements Messenger.Callback {
private final BlockingQueue<BridgeRequest> mQueue;
private BridgeRequest mRequest;
private Messenger mMessenger;
public RequestExecutor(BlockingQueue<BridgeRequest> queue) {
this.mQueue = queue;
}
@Override
public void run() {
while (true) {
synchronized (this) {
try {
mRequest = mQueue.take();
} catch (InterruptedException e) {
continue;
}
mMessenger = new Messenger(mRequest.getSource().getContext(), this);
mMessenger.register();
executeCurrent();
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
private void executeCurrent() {
switch (mRequest.getType()) {
case BridgeRequest.TYPE_PERMISSION: {
Intent intent = new Intent(source.getContext(), BridgeActivity.class);
intent.putExtra(KEY_TYPE, BridgeRequest.TYPE_PERMISSION);
intent.putExtra(KEY_PERMISSIONS, permissions);
source.startActivity(intent);
break;
}
}
}
@Override
public void onCallback() {
synchronized (this) {
mMessenger.unRegister();
mRequest.getCallback().onCallback();
notify();
}
}
}
复制代码
而上而说了,此线程是单例一直存活的,而mRequest里有activity引用,关键就是在onCallbackl回调中没有把mRequest置为空,而该git上也有人提相同问题,解决办法也是把mRequest置为空。(这里说一下,在2.0.3版本是解决了此问题的,可是2.0.2以上再也不兼容supportV7,只兼容了androidX,由于项目中尚未升级替换androidX,那么只能本身尝试解决此问题。)github
这里插一下检测activity泄露的几种方式。
一. 本身注册lifeCycler检测,日志输出泄露对象。代码以下bash
registerActivityLifecycleCallbacks(new EmptyActivityLifecycleCallbacks() {
@Override
public void onActivityDestroyed(Activity activity) {
super.onActivityDestroyed(activity);
ReferenceQueue<Activity> refer=new ReferenceQueue<>();
WeakReference<Activity> weak=new WeakReference<>(activity,refer);
new Thread(){
@Override
public void run() {
super.run();
while (true){
try {
Activity act=weak.get();
Reference<Activity> refe= (Reference<Activity>) refer.poll();
Log.d(TAG,"weak ="+ act+" "+refe);
if(act==null && refe==null){
return;
}
act=null;
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
});
复制代码
二. leakcanary。输出泄露源
三. 在androidProfile中看有没有这个activity对象。多线程
其实leakcanary检测内存泄露的步骤和1差很少,就是在onDestory后用弱引用,对activity检测,正常的话GC后,引用中对象会有空,一段时间事后,发现对象还在,认为泄露,再dump内存,分析引用链,输出报告。若是要验证某页面的是否泄露的话,用日志的方法会快一点。框架
好,咱们本身在onCallback 中把这两个mRequest,mMessenger 置为空。想到两种办法,ide
用AOP框架AspectJ,在onCallback执行后,用反射把这个变量为空。工具
把项目2.0.1源码拷下来,直接在源码上修改。
看起来此问题好像就要解决了。若是真是如此,就不必记录了。 这里先用了AOP,发现不行,再用了直接改源码,发现仍是存在泄露。。。
这里不管用那种方式,对象都确实存在,咱们直接看leakcanary的报告。
线程间的不可见性会致使咱们在多线程中操做同一变量会引起的问题,如同一变量i,两个线程同时加,会致使数不等于总和,这个你们应该都知道。
public class JavaTest {
public static void main(String[] args) {
MyThread t = new MyThread();
t.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
t.reset();
System.out.println("main t=" + t.p2);
}
static class MyThread extends Thread {
private Person p2;
public MyThread() {
this.p2 = new Person();
}
@Override
public void run() {
super.run();
while (p2 != null) {
// System.out.println("MyThread " + p2);
}
}
public void reset() {
p2 = null;
}
}
}
复制代码
在这个实验中,子线程MyThread有一变量,子线程经过p2!=null作循环检查。100毫秒后,主线程中调用子线程reset方法把p2置空,那么按照理论,子线程会结束循环,实验结果以下图:
System.out.println("main 2=" + p + " t=" + t.p2);
结果是t.p2 确实为null,这个问题是否是和上面那个很像呢。是在主线程调用的
onCallback()
中置空。
这个就是线程的不可见性致使的,主线程和子线程都有一份这个变量,主线程调用置null,而子线程中的变量没有从主内存中更新,因此对于子线程而言,依然不为null,解决办法就是对这个变量加上volatile
关键字,当更新后,使得子线程当即从主内存中更新。
加上volatile 后:
当找到这问题缘由,再回到Android中,那么就好解决了,加上volative就可解决,而原做者的解决办法是用了系统的线程池,而后把RequestExecutor当一个runnable 用,这样,回调完,该runnable 结束,该runnable对象也就被释放了,内部属性也一样被释放了。
以下:
public class RequestManagerFix {
private static RequestManagerFix sManager;
public static RequestManagerFix get() {
if (sManager == null) {
synchronized (RequestManagerFix.class) {
if (sManager == null) {
sManager = new RequestManagerFix();
}
}
}
return sManager;
}
private final Executor mExecutor;
private RequestManagerFix() {
this.mExecutor = Executors.newCachedThreadPool();
}
public void add(BridgeRequest request) {
mExecutor.execute(new RequestExecutorFix(request));
}
}
final class RequestExecutor implements Messenger.Callback, Runnable {
private BridgeRequest mRequest;
private Messenger mMessenger;
public RequestExecutor(BridgeRequest queue) {
this.mRequest = queue;
}
@Override
public void run() {
mMessenger = new Messenger(mRequest.getSource().getContext(), this);
mMessenger.register();
executeCurrent();
}
private void executeCurrent() {
。。。
}
@Override
public void onCallback() {
synchronized (this) {
mMessenger.unRegister();
mRequest.getCallback().onCallback();
mRequest = null;
mMessenger = null;
}
}
}
复制代码
原RequestExecutor 作的事情就是相似线程池,而确实没有必要本身写一套线程池。
在我测试过程当中,我本身写的这套检测activity是否存在的代码 一. 本身注册lifeCycler检测,日志输出泄露对象。代码以下
registerActivityLifecycleCallbacks(new EmptyActivityLifecycleCallbacks() {
@Override
public void onActivityDestroyed(Activity activity) {
super.onActivityDestroyed(activity);
ReferenceQueue<Activity> refer=new ReferenceQueue<>();
WeakReference<Activity> weak=new WeakReference<>(activity,refer);
new Thread(){
@Override
public void run() {
super.run();
while (true){
try {
Activity act=weak.get();
Reference<Activity> refe= (Reference<Activity>) refer.poll();
Log.d(TAG,"weak ="+ act+" "+refe);
if(act==null && refe==null){
return;
}
act=null;
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}.start();
}
});
复制代码
若是把act=null;
这行代码注释掉,猜猜会发生什么现象?能够本身实验一下。 出现的现象就是一个activity 都释放不了,为何??
若是去掉这行,那么Activity act=weak.get();
这行代码就会有一个子线程引用指向activity 对象,而后休眠一秒,在此过程当中,就算主线程无任何引用,发生GC,发现这个对象还有引用,因此不会释放,休眠结束,又当即从弱引用中取出对象,又建立引用。因此致使对象永远没法释放,因此act=null,这行代码必须加上。这算是多线程引用的问题。
虽然咱们在其它时候或多或少都学习过线程间的问题,如可见性等等,可是在碰到实际问题时,却不会常常往这方面去想,结合实际问题,才能记得更牢,经过此问题,也算是对之前线程的知识复习了一下。
推荐个git项目:
这是我的开源的Android库,能够用来优雅的、精准的埋点:Tracker,但愿你们提点Issue与star。。