做了较长时间的android开发了,发现其实android应用开发入门容易,但是进阶或者成为高级工程师,需要具备的基础能力还是非常高的:性能优化、内存泄露、apk瘦身、热修复等等,这些都非常的考验一个人的能力。android成长之路还很长,自己会持续的走下去。本文主要介绍android内存泄露方面的知识。其实要真的理解内存泄露,需要对JVM、java语言有一定的了解,在这个基础上就比较容易理解本文了。
在java中,如果一个对象没有可用价值了,但又被其他引用所指向,那么这个对象对于gc来说就不是一个垃圾, 所以不会对其进行回收,但是我们认为这应该是个垃圾,应该被gc回收的。这个对象得不到gc的回收, 就会一直存活在堆内存中,占用内存,就跟我们说的霸着茅坑不拉屎的道理是一样的。这样就导致了内存的泄露。
为什么会内存泄露呢,根本原因就是一个永远不会被使用的对象,因为一些引用没有断开,没有满足GC条件,导致不会被回收,这就造成了内存泄露。比如在Activity中注册了一个广播接收器,但是在页面关闭的时候进行unRegister,就会出现内存溢出的现象。如果我们的java运行很久,而这种内存泄露不断的发生,最后就没内存可用了,最终就是我们看到的OOM错误。虽然android的内存泄露做到了应用程序级别的泄露(android中的每个应用程序都是独立运行在单独进程中的,每个应用进程都由虚拟机指定了一个内存上限值,一旦内存占用值超过这个上限值,就会发生oom错误,进程被强制kill掉,kill掉的进程内存会被系统回收),但是对于一名开发工程师,绝对不能放过任何的内存泄露。
public class SampleActivity extends Activity {
private final Handler mLeakyHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// ...
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 延时10分钟发送一个消息
mLeakyHandler.postDelayed(new Runnable() {
@Override
public void run() { }
}, 60 * 10 * 1000);
}
}
当这个Activity被finished后,延时发送的消息会继续在主线程的消息队列中存活10分钟,直到他们被处理。这个消息持有这个Activity的Handler引用,这个Handler有隐式地持有他的外部类(在这个例子中是SampleActivity)。直到消息被处理前,这个引用都不会被释放。因此Activity不会被垃圾回收机制回收,泄露他所持有的应用程序资源。注意,第15行的匿名Runnable类也一样。匿名类的非静态实例持有一个隐式的外部类引用,因此context将被泄露。
具体的引用顺序:
MessageQueue->Message->Runnable->Handler->Activity。所以这就导致当前activity与MessageQueue一直有关联,导致LeakActivity的对象不能被gc回收,从而导致内存泄露。
为了解决这个问题,Handler的子类应该定义在一个新文件中或使用静态内部类。静态内部类不会隐式持有外部类的引用。所以不会导致它的Activity泄露。如果你需要在Handle内部调用外部Activity的方法,那么让Handler持有一个Activity的弱引用(WeakReference)以便你不会意外导致context泄露。为了解决我们实例化匿名Runnable类可能导致的内存泄露,我们将用一个静态变量来引用他(因为匿名类的静态实例不会隐式持有他们外部类的引用)。
public class SampleActivity extends Activity {
/** * 匿名类的静态实例不会隐式持有他们外部类的引用 */
private static final Runnable sRunnable = new Runnable() {
@Override
public void run() {
}
};
private final MyHandler mHandler = new MyHandler(this);
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 延时10分钟发送一个消息.
mHandler.postDelayed(sRunnable, 60 * 10 * 1000);
// 返回前一个Activity
finish();
}
/** * 静态内部类的实例不会隐式持有他们外部类的引用。 */
private static class MyHandler extends Handler {
private final WeakReference<SampleActivity> mActivity;
public MyHandler(SampleActivity activity) {
mActivity = new WeakReference<SampleActivity>(activity);
}
@Override
public void handleMessage(Message msg) {
SampleActivity activity = mActivity.get();
if (activity != null) {
// ...
}
}
}
}
不在一个Activity中使用非静态内部类, 以防它的生命周期比Activity长。相反,尽量使用持有Activity弱引用的静态内部类。
或者:
我们也可以这样做,在activity的onDestroy方法中干掉handler的所有callback和message:
@Override
protected void onDestroy() {
super.onDestroy();
handler.removeCallbacksAndMessages(null);
}
在activity中开启的线程也是一样,如果activity结束了而线程还在跑,一样会导致activity内存泄露,因为”非静态内部类对象都会持有一个外部类对象的引用”,你创建的线程就是activity中的一个内部类,持有activity对象的引用,当activity结束了,但线程还在跑,就会导致activity内存泄露。
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
exampleOne();
}
private void exampleOne() {
newThread() {//匿名内部类,非静态的匿名类会持有外部类的一个隐式引用
@Override
publicvoid run() {
while(true) {
SystemClock.sleep(1000);
}
}
}.start();
}
非静态的内部类会持有外部类的一个隐式引用,有需要的朋友可以参考下。
只要非静态的匿名类对象没有被回收,MainActivity就不会被回收,MainActivity所关联的资源和视图都不会被回收,发生比较严重的内存泄漏。
要解决MainActivity的内存泄漏问题,只需把非静态的Thread匿名类定义成静态的内部类就行了(静态的内部类不会持有外部类的一个隐式引用)。
2中,一旦一个新的Activity创建,那么就有一个Thread永远得不到清理回收,发生内存泄漏。Threads在Java中是GC roots;意味着Dalvik。Virtual Machine (DVM)会为所有活跃的threads在运行时系统中保持一个硬引用,这会导致threads一直处于运行状态,垃圾收集器将永远不可能回收它。
出于这个原因,我们应当为threads添加一个结束的逻辑,如下代码所示:
public class MainActivity extends Activity {
privateMyThread mThread;
@Override
protectedvoid onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
exampleThree();
}
privatevoid exampleThree() {
mThread = new MyThread();
mThread.start();
}
/** * Static inner classes don't hold implicit references to their * enclosing class, so the Activity instance won't be leaked across * configuration changes. */
privatestatic class MyThread extends Thread {
privateboolean mRunning = false;
@Override
publicvoid run() {
mRunning = true;
while(mRunning) {
SystemClock.sleep(1000);
}
}
publicvoid close() {
mRunning = false;
}
}
@Override
protectedvoid onDestroy() {
super.onDestroy();
mThread.close();
}
}
在上述的代码中,当Activity结束销毁时在onDestroy()方法中结束了新创建的线程,保证了thread不会发生泄漏。
LeakActivity1
public class LeakActivity1 extends AppCompatActivity {
private TestManager testManager = TestManager.getInstance();
private MyListener listener=new MyListener() {
@Override
public void doSomeThing() {}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
testManager.registerListener(listener);
}
}
TestManager
public class TestManager {
private static final TestManager INSTANCE = new TestManager();
private MyListener listener;
public static TestManager getInstance() {
return INSTANCE;
}
public void registerListener(MyListener listener) {
this.listener = listener;
}
public void unregisterListener() {
listener = null;
}
}
interface MyListener {
void doSomeThing();
}
TestManager——>listener——>activity,TestManager是单例。
因为TestManager中有static对象,static跟类的生命周期是一样的,类一加载,static就加载了,类一被销毁,static才会跟着销毁(static是存在方法区),这时候jvm会在方法区中存储变量INSTANCE,然后在堆内存开辟空间存放INSTANCE对象,然后把地址值付给INSTANCE变量,使INSTANCE变量就指向这个对象(类似c语言的指针),activity类也是这样的一种执行关系。
因为这是一个单例,当app进程被干掉的时候,堆内存中的INSTANCE对象才会被释放,所以INSTANCE对象的生命周期是很长的,LeakActivity1中,listener持有当前activity的对象,然后testManager.registerListener(listener);执行完,TestManager中的listener就持有activity中listener的对象,而TestManager中的INSTANCE是static的,生命周期长,activity销毁的时候INSTANCE依然还在,INSTANCE还在,那么TestManager类中的全局变量也还是存在的,所以TestManager中的listener变量还在,还一直持有LeakActivity1中的listener对象引用,所以最终是INSTANCE导致LeakActivity1内存泄露。
所以,要解决这个问题,可以这样做,在activity的onDestroy方法中注销注册的listener.
@Override
protected void onDestroy() {
testManager.unregisterListener();
super.onDestroy();
}
这样做后TestManager中的listener不再持有LeakActivity1中的listener对象引用,所以LeakActivity1被销毁后listener对象也可被回收了。最终,问题又解决了,当然你也可以直接把INSTANCE置null。
其实对于内存泄露的分析,用逆向思维来分析更好,从根源一直分析到无法被回收的对象。
出现内存泄露的主要原因是生命周期的不一致造成的:在Android中,长时间运行的任务和Acyivity生命周期进行协调会有点困难,如果你不加以小心的话会导致内存泄漏。
内存泄漏的主要原因在于一个生命周期长的东西间接引用了一个生命周期短的东西,会造成生命周期短的东西无法被回收。反过来,如果是一个生命周期短的东西引用了一个生命周期长的东西,是不会影响生命周期短的东西被回收的。
对象都是有生命周期的,对象的生命周期有的是进程级别的,有的是Activity所在的生命周期,随Activity消亡;有的是Service所在的生命周期,随Service消亡。很多情况下判断对象是否合理存在的一个很重要的理由就是它实际的生命周期是否符合它本来的生命周期。很多Memory Leak的发生,很大程度上都是生命周期的错配,本来在随Activity销毁的对象变成了进程级别的对象,Memory Leak就无法避免了。
MAT
LeakCanary
1.内部类如何不影响外部类的回收呢?
非静态的内部类会持有外部类的一个隐式引用。
在Java里面,非静态的匿名内部类保持了一个对outer class的引用。如果你不够小心,持有了这个引用,那么将导致这个Activity一直保留(本应该被垃圾回收器回收)。Activity对象保持了对整个View结构和所有这些Resources的引用,所以你LeakActivity,也就Leak了很多的内存。
2.activity onbestrory的时候并没有被垃圾回收掉
不要认为Java会为你清理runningthread。在上面的例子中,我们会很容易的就想象当用户离开这个Activity的时候,Activity实例会被垃圾回收器回收,所有在这个Activity中开启的running thread也会被清理掉。事实上不是这样的。Java线程将会一直存在,直到他们被显示的关闭或者处理结束,或者被杀掉整个进程。所以,你需要完成可被取消的线程机制,在Activity的生命周期的某个地方做适当的处理。