声明:大部分内容为从其他文章中摘录感兴趣的部分,只为记录给自己看。
Android内存性能主要包括内存泄漏,内存抖动,内存持续增长(但GC后会下降),内存占用过大等问题。
Android内存分析方向:
- Java内存分析
Java中的内存泄漏主要特征:可达,无用
无用指的是创建了但是不再使用之后没有释放
能重用但是却创建了新的对象进行处理 - Native内存分析
堆中new的对象未释放
对象引用导致无法释放 - JS中内存分析
一、日志分析
查看日志中是否有频繁的GC。通常通过log,我们可以初步定位大部分内存等问题。
二、常见内存泄漏查找
- Context泄漏
主要为Activity传递泄漏,在单例创建时context未使用applicationContext。 - Handler泄漏
handler中持有view,context等做耗时操作。 - Cursor泄漏,cursor未关闭。
- register未unregister。
- Bitmap
- adapter未使用convertView
- 不良代码等
三、命令dumpsys meminfo分析
理解meminfo的信息中各字段都是什么含义,才好进行内存的优化。首先了解两个概念:
- 私有内存(Dirty and Clean)
进程独占内存。也就是进程销毁时可以回收的内存容量。通常private Dirty内存是最重要的部分,因为只被自己进程使用。Dirty内存是已经被修改的内存页,因此必须常驻内存(因为没有swap);Clean内存是已经映射持久文件使用的内存页(例如正在被执行的代码),因此一段时间不使用的话就可以置换出去。 - 实际使用内存(PSS)
将跨进程共享也加进来了,进行按比例计算PSS。这样能够比较准确的表示进程占用的实际物理内存。通常我们需要关注PSS TOTAL和Private Dirty。 - Dalvik Heap
dalvik虚拟机分配的内存。PSS Total包含所有Zygote分配使用的内存,共享跨进程加权。PrivateDirty是应用独占内存大小,包含独自分配的部分和应用进程从Zygote复制时被修改的Zygote分配的内存页。HeapAlloc是Dalvik堆和本地堆分配使用的大小,它的值比Pss Total和Private Dirty大,因为进程是从Zygote中复制分裂出来的,包含了进程共享的分配部分。 - .so mmap & .dex mmap ... mmap
映射本地或虚拟机代码到使用的内存中。 - Unknown
无法归类的其他项。主要包括大部分的本地分配。 - Native Heap
native代码申请的内存,堆和栈,及静态代码块等。 - TOTAL 进程总使用的实际内存。
- Objects 显示持有对象的个数。
这些数据也是分析内存泄漏的重要数据。如activity等。
四、Heap Viewer
Heap Viewer能做什么?
- 实时查看内存分配情况和空闲内存大小。
- 发现memory Leaks。
Android Studio3.0以上可以查看Memory Profiler。用Memory Profiler查找内存泄漏:
- 启动应用,看一下当前内存使用了多少,使用应用一段时间后(不想自己点,可以使用monkey进行自动化测试),退回到应用首页,看看当前内存又是多少。进行一次heap dump看看结果,分析一下有没有可疑的对象分配(比如大量重复的activity,同一个类型对象比较多,对象内存占用较大)。
- 发现可疑点后,通过分析结果,可以找到相应代码,就可以找到使用代码的场景,例如是activity泄漏,反复进行画面的跳转(也可以旋转屏幕),然后强制gc回收,看看内存是否存在只增不减的情况。
- 也可以使用allocation跟踪一段时间内存分配情况,拿来做分析。
常见内存泄漏案例
1. 单例造成的内存泄漏
单例的静态特性导致其生命周期同应用一样长。
解决方案:
- 将改属性的引用方式改为弱引用
- 如果传入Context,使用Application
public class ScrollHelper {
private static ScrollHelper mInstance;
public ScrollHelper() {
}
public static ScrollHelper getInstance() {
if (mInstance == null) {
synchronized (ScrollHelper.class) {
if (mInstance == null) {
mInstance = new ScrollHelper();
}
}
}
return mInstance;
}
}
/**
* 被点击的View
*/
private View mScrolledView = null;
public void setScrolledView(View scrolledView) {
mScrolledView = scrolledView;
}
改成:
/**
* 被点击的View
*/
private WeakReference<View> mScrolledView = null;
public void setScrolledView(View scrolledView) {
mScrolledView = new WeakReference<View>(scrolledView);
}
2. 匿名内部类
在Java中,非静态内部类和匿名类都会潜在的引用他们所属的外部类,但是静态内部类却不会。如果这个非静态内部类实例做了一些耗时的操作,就会造成外围对象不会被回收,从而导致内存泄漏。
- 将内部类变成静态内部类;
- 如果有强引用Activity中的属性,则将改属性的引用方式改为弱引用;
- 在业务允许的情况下,当activity执行onDestroy时,结束这些耗时任务;
public class MainActivity extends AppCompatActivity {
private static TestResource mResource = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if(mResource == null){
mResource = new TestResource();
}
//...
}
class TestResource {
//...
}
}
因为非静态内部类默认会持有外部类的引用,而该非静态内部类又创建了一个静态的实例,该实例的生命周期和应用一样长,这就导致了该静态实例一直会持有该Activity的引用,导致Activity的内存资源不能正常回收。正确的做法是:将改内部类设为静态内部类或将改内部类抽取出来封装成一个单例,如果需要使用Context,使用Application的Context。
3. Handler引起的内存泄漏
当Handler中有延迟的任务或等待执行的任务队列过长,由于消息持有对Handler的引用,而Handler又持有对其外部类的潜在引用,这条引用关系会一直保持到消息得到处理,而导致了Activity无法被回收。
解决方案:
- 可以把Handler类放在一个单独的类文件中,或者使用静态内部类便可以避免泄漏;
- 如果在handler内部去调用所在的Activity,那么可以在handler内部使用弱引用的方式去指向该activity,使用static+WeakReference的方式断开Handler和Activity之间存在的引用关系。
- 在onDestroy里面执行removeCallbacksAndMessages(null);
public class SampleActivity extends Activity {
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) {
// ...
}
}
}
private final MyHandler mHandler = new MyHandler(this);
private static final Runnable sRunnable = new Runnable() {
@Override
public void run() { /* ... */ }
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mHandler.postDelayed(sRunnable, 1000 * 60 * 10);
finish();
}
}
4. 集合中对象没清理造成的内存泄漏
我们通常把一些对象的引用加入到了集合容器中,当不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果它是static的话就更严重了。如果这个集合类是全局性的变量,那么没有相应的删除机制,很可能导致集合所占用的内存只增不减。所以要在退出程序之前,将集合里的东西clear,然后置为null,再退出程序。
5. WebView造成的泄漏
当我们不再使用WebView对象时,应该调用它的destroy()函数来销毁它,并释放其占用的内存,否则其占用的内存长期不能回收。
解决方案:
为WebView开启另外一个进程,通过AIDL与主线程进行通信,WebView所在的进程可以根据业务的需求选择合适的时机进行销毁,从而达到内存的完整释放。
四种引用的区别
强引用
只要引用存在,垃圾回收器永远不会回收。
Object obj = new Object();
只有当这个引用被释放后,对象才会被释放掉。
软引用
非必须引用,内存溢出之前进行回收,可以通过以下代码实现
SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null;
sf.get();// 有时候会返回null
这时候sf是对obj的一个软引用,通过sf.get()方法可以取到这个对象,当然,当这个对象被标记为需要回收的对象时,则返回null。
软引用主要实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的真实来源查询数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源查询这些数据。
弱引用
第二次垃圾回收时回收,可以通过如下代码实现:
Object obj = new Object();
WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null;
wf.get();// 有时候会返回null
wf.isEnQueued();// 返回是否被垃圾回收器标记为即将回收的垃圾
弱引用时在第二次垃圾回收时回收,短时间内通过弱引用取对应的数据,可以取到,当执行过第二次垃圾回收时,将返回null。
弱引用主要在于监控对象是否已经被垃圾回收器标记为即将回收的垃圾,可以通过弱引用的isEnQueued方法返回对象是否被垃圾回收器标记。
虚引用
垃圾回收时回收,无法通过引用取到对象值,可以通过如下代码实现:
Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj = null;
pf.get();// 永远返回null
pf.isEnQueued();// 返回是否从内存中已经删除
虚引用时每次垃圾回收的时候都会被回收,通过虚引用的get方法永远获取到的数据为null,因此也被称为幽灵引用。虚引用主要用于检测对象是否已经从内存中删除。