Android内存优化详解
Posted 小陈乱敲代码
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android内存优化详解相关的知识,希望对你有一定的参考价值。
1. 内存抖动
短时间内创建大量对象,挤占 Eden 区,导致频繁 MinorGC,内存就会发生抖动。
1.1. 现象
MemoryProfile内存图为锯齿状,并伴随大量的白色垃圾桶。
常见引发的问题就是在最小一层循环里面创建大量对象
<pre data-language="java" id="ea4e73dd" class="ne-codeblock language-java" style="border: 1px solid #e8e8e8; border-radius: 2px; background: #f9f9f9; padding: 16px; font-size: 13px; color: #595959">String s = "";
for (int i = 0; i < 10000; i++)
s += "," + i; // 创建大量的字符串常量对象
</pre>
1.2. 避免
通常会采取一些办法避免这类问题:
-
尽量避免在循环体内创建对象,应该把对象创建移到循环体外。
-
避免在自定义View的
onMeasure
、onLayout
、onDraw
等方法中调用invalidate()
方法(通知主线程当前画面已过期,再线程空闲时重绘),且不要创建大量对象。 -
当需要大量使用
Bitmap
的时候,试着把它们缓存在数组中实现复用。 -
对于能够复用的对象,同理可以使用对象池将它们缓存起来。
-
字符串拼接,改用
StringBuffer
或StringBuilder
来执行。
2. 内存泄漏
长生命周期对象持有短生命周期对象的引用,导致分配的内存空间没有及时回收,使得可使用内存越来越少。
2.1. 内存泄漏的检测与定位
Memory Profiler
使用Memory Profiler 捕获堆转储之后,筛选出我们自己的对象。
在退出 LeakActivity 回到 MainActivity 之后,并且手动出发了一次GC后,LeakActivity 还在内存当中,表是发生了内存泄漏。
LeakActivity$1 和LeakActivity$2分别表示 LeakActivity 中第1个和第2个匿名内部类(编译的时候由系统自动起名为外部类名$1.class)对 LeakActivity 的引用。
右边四列分别表示:
● Allocations:Java 堆中的实例个数。
● Native Size:native 层分配的内存大小。
● Shallow Size:Java 堆中分配实际大小。
● Retained Size:这个类的所有实例保留的内存总大小(并非实际大小)。
如果 Shallow Size 和 Retained Size 都非常小并且相等,都可以认为是已经被回收的对象(空的 Activity,大概是270)。但是 LeakActivity 明显不是这样,也说明了 LeakActivity 发生了内存泄漏。
左边选中 LeakActivity,右边 Instance 中一个一个去查看,现在只有一处,点击第一个,默认 Reference 以 Depth 排序,只看 Reference Depth 小于 Instance Depth 的行。右键 -> Jump to Source,则自动定位到代码。
如上图,展开发现是Handler引起的泄漏。
2.2. 常见的内存泄漏
2.2.1. 静态变量内存泄漏
stact
变量所指向的引用,虚拟机会一直保留该引用对象,除非把 static
变量设置为 null
。
<pre data-language="java" id="bc73ec48" class="ne-codeblock language-java" style="border: 1px solid #e8e8e8; border-radius: 2px; background: #f9f9f9; padding: 16px; font-size: 13px; color: #595959">public class ToastUtil
private static Toast toast;
public static void show(Context context, String message)
if (toast == null)
// 静态变量toast会一直持有Toast对象的引用,Toast对象将不会被回收
// 如果context为Activity Context,相应的Activity也会泄漏
toast = Toast.makeText(context, message, Toast.LENGTH_SHORT);
else
toast.setText(message);
toast.show();
</pre>
解决办法①: 将 makeText()
中的参数 context
改为context.getApplicationContext()
,长生命周期的 static Toast
持有的也是长生命周期的 Applaiction Context
,就不会内存泄漏了。
<pre data-language="java" id="ef3cc5a7" class="ne-codeblock language-java" style="border: 1px solid #e8e8e8; border-radius: 2px; background: #f9f9f9; padding: 16px; font-size: 13px; color: #595959">private static Activity sActivity;
@Override
protected void onCreate(Bundle savedInstanceState)
// 静态变量sActivity会在APP生命周期内一直持有Activity的实例对象,内存泄漏
sActivity = this;
</pre>
解决办法②:在 Acticity 生命周期结束时,让静态变量断开与其引用关系。即在 onDestory()
回调方法中,执行 sActivity = null
。
2.2.2. 非静态内部类、匿名内部类内存泄漏
非静态内部类对象会持有外部类对象的引用,外部类的生命周期结束,但是仍可能被内部类所引用。
- 编译器会自动为内部类添加一个成员变量, 用来指向外部类对象的引用。
- 编译器会自动为内部类的构造方法添加一个参数,用来给上面的成员变量赋值。
- 在调用内部类的构造方法时,会默认传入外部类的引用。
通过 Java在线反编译工具,并使用 Fernflower 反编译器,可以查看反编译内容:
<pre data-language="java" id="54ebfec8" class="ne-codeblock language-java" style="border: 1px solid #e8e8e8; border-radius: 2px; background: #f9f9f9; padding: 16px; font-size: 13px; color: #595959">public class Test
// ① 匿名内部类实现接口并重写接口方法
Runnable runnable = new Runnable()
@Override
public void run()
;
// ② 匿名内部类重写类方法
Object object = new Object()
;
// ③ 静态常量 + 匿名内部类实现接口并重写接口方法
static final Runnable runnable2 = new Runnable()
@Override
public void run()
;
// ④ 非静态内部类
class InnerClass
void test()
runnable.run();
// ⑤ 静态内部类
static class InnerClass2
// ⑥ 外部类
class OuterClass
</pre>
① `Test$1.class` 即 `new Runnable() ` 的内容:
<pre data-language="java" id="b15054ed" class="ne-codeblock language-java" style="border: 1px solid #e8e8e8; border-radius: 2px; background: #f9f9f9; padding: 16px; font-size: 13px; color: #595959">import com.company.Test;
class Test$1 implements Runnable
// 持有外部类的引用
final Test this$0;
// 外部类的引用通过构造方法传入
Test$1(Test this$0)
this.this$0 = this$0;
public void run()
</pre>
② `Test$2.class` 即 `new Object() ` 的内容:
<pre data-language="java" id="05768f7c" class="ne-codeblock language-java" style="border: 1px solid #e8e8e8; border-radius: 2px; background: #f9f9f9; padding: 16px; font-size: 13px; color: #595959">import com.company.Test;
class Test$2
// 持有外部类的引用
final Test this$0;
// 外部类的引用通过构造方法传入
Test$1(Test this$0)
this.this$0 = this$0;
</pre>
③ `Test$3.class` 即 `static final Runnable runnable2 = new Runnable() ` 的内容:
<pre data-language="java" id="fa783491" class="ne-codeblock language-java" style="border: 1px solid #e8e8e8; border-radius: 2px; background: #f9f9f9; padding: 16px; font-size: 13px; color: #595959">class Test$3 implements Runnable
public void run()
</pre>
④ `Test$InnerClass.class` 即 `class InnerClass ` 的内容:
<pre data-language="java" id="1e1cad08" class="ne-codeblock language-java" style="border: 1px solid #e8e8e8; border-radius: 2px; background: #f9f9f9; padding: 16px; font-size: 13px; color: #595959">import com.company.Test;
class Test$InnerClass
// 持有外部类的引用
final Test this$0;
// 外部类的引用通过构造方法传入
Test$InnerClass(Test this$0)
this.this$0 = this$0;
// 通过外部类的引用,直接访问外部类的成员
void test()
this.this$0.runnable.run();
</pre>
⑤ `Test$InnerClass2.class` 即 `static class InnerClass2 ` 的内容:
<pre data-language="java" id="75304453" class="ne-codeblock language-java" style="border: 1px solid #e8e8e8; border-radius: 2px; background: #f9f9f9; padding: 16px; font-size: 13px; color: #595959">class Test$InnerClass2
// 不会持有外部类的引用
Test$InnerClass2()
</pre>
⑥ `OuterClass.class` 即 `class OuterClass ` 的内容:
<pre data-language="java" id="d2a61ea4" class="ne-codeblock language-java" style="border: 1px solid #e8e8e8; border-radius: 2px; background: #f9f9f9; padding: 16px; font-size: 13px; color: #595959">class OuterClass
// 不会持有外部类的引用
OuterClass()
</pre>
【非静态内部类】
<pre data-language="java" id="2f719f64" class="ne-codeblock language-java" style="border: 1px solid #e8e8e8; border-radius: 2px; background: #f9f9f9; padding: 16px; font-size: 13px; color: #595959">class Outer
// 非静态内部类,默认持有外部类的引用,方法体内可直接调用外部类的成员和方法
class Inner1 /* ... */
// 静态内部类,不会持有外部类的引用,方法体内不可直接调用外部类的成员和方法
static class Inner2 /* ... */
</pre>
【匿名内部类】
-
直接 new interface、直接 new abstract class 、直接 new class 后并重写(或只写大括号,但是不重写)其中的成员方法,而不是手动新建一个类继承或实现它们再重写方法。因为没有显式的指名新的类名,编译器会自动为他们分别生成一个匿名内部类。
-
编译器会自动将匿名内部类创建成非静态内部类,并取名为
外部类名$1.class
、``外部类名$2.class`等以此类推。
<pre data-language="java" id="5a025c4c" class="ne-codeblock language-java" style="border: 1px solid #e8e8e8; border-radius: 2px; background: #f9f9f9; padding: 16px; font-size: 13px; color: #595959">// 直接new一个接口,并重写成员方法
new Runnable()
@Override
public void run() /* ... */
;
// 直接new一个抽象类,并重写成员方法
new AbstractSet<Object>()
@NonNull
@Override
public Iterator<Object> iterator() /* ... */
@Override
public int size() /* ... */
;
// 直接new一个类,并重写成员方法
new Handler()
@Override
public void handleMessage(@NonNull Message msg) /* ... */
;
// 直接new一个类,只写大括号,但不重写任何成员方法。抽象类、接口也同理
new Handler() ;</pre>
【常见的 Handler 内存泄漏】
<pre data-language="java" id="4e586f96" class="ne-codeblock language-java" style="border: 1px solid #e8e8e8; border-radius: 2px; background: #f9f9f9; padding: 16px; font-size: 13px; color: #595959">public class MainActivity extends AppCompatActivity
private Handler mHandler = new Handler()
@Override
public void handleMessage(Message msg) /* ... */
// new Handler()并重写方法,编译器会自动生成MainActivity$1.class的非静态内部类
// 因此new出的这个对象会默认持有MainActivity对象的引用,从而导致内存泄漏
// Activity退出后,msg可能还在MessageQueue中没被处理完毕,则Activity对象不能回收
;
</pre>
解决办法:改为静态内部类+弱引用,因为静态内部类不需依赖外部类的成员和方法。
类似的还有直接 new Thread,new Runnable,new AsyncTask,new Listiner 等也可能造成内存泄漏。
<pre data-language="java" id="a205daf6" class="ne-codeblock language-java" style="border: 1px solid #e8e8e8; border-radius: 2px; background: #f9f9f9; padding: 16px; font-size: 13px; color: #595959">public class MainActivity extends AppCompatActivity
private Handler mHandler;
// 静态常量不会持有外部类的引用,不会内存泄漏,但是会生成匿名内部类
private static final Runnable sRunnable = new Runnable()
@Override
public void run() /* ... */
;
@Override
protected void onCreate(Bundle savedInstanceState)
mHandler = new MyHandler(this);
@Override
protected void onDestroy()
// 虽然不会导致Activity泄漏,但msg可能还在MessageQueue中,最好移除掉
mHandler.removeCallbacksAndMessages(null);
// 自己写一个静态内部类,不让编译器自动生成非静态内部类
private static class MyHandler extends Handler
// 使用弱引用持有外部类的引用
private WeakReference<MainActivity> activityWeakReference;
public MyHandler(MainActivity activity)
activityWeakReference = new WeakReference<>(activity);
@Override
public void handleMessage(Message msg) /* ... */
</pre>
2.2.3. 单例模式内存泄漏
单例的静态特性使得它的生命周期同App的生命周期一样长,如果单例一直持有一个短生命周期的引用,就容易导致内存泄漏。
<pre data-language="java" id="ef3db6fe" class="ne-codeblock language-java" style="border: 1px solid #e8e8e8; border-radius: 2px; background: #f9f9f9; padding: 16px; font-size: 13px; color: #595959">public class Singleton
// 静态变量一直持有Singleton对象
private static Singleton singleton = null;
// Singleton对象又持有Context对象
private Context mContext;
private Singleton(Context mContext)
this.mContext = mContext;
public static Singleton getSingleton(Context context)
// 如果context为Activity Context,则Activity就始终无法回收,导致内存泄漏
if (null == singleton) singleton = new Singleton(context);
return singleton;
public class Singleton
// 静态变量一直持有Singleton对象
private volatile static Singleton singleton = null;
// Singleton对象又持有Context对象
private Context mContext;
private Singleton(Context mContext)
this.mContext = mContext;
public static Singleton getSingleton(Context context)
if (singleton == null)
synchronized (Singleton.class)
if (singleton == null)
// 如果context为Activity,则Activity内存泄漏
singleton = new Singleton(context);
return singleton;
</pre>
解决办法①: 调用的时候传入 ApplicationContext 或改为 this.mContext = mContext.getApplicationContext();
。
解决办法②: 将该属性的引用方式改为弱引用。
<pre data-language="java" id="d7a281ed" class="ne-codeblock language-java" style="border: 1px solid #e8e8e8; border-radius: 2px; background: #f9f9f9; padding: 16px; font-size: 13px; color: #595959">public class Singleton
private volatile static Singleton singleton = null;
// 使用弱引用,使Singleton对象持有Context对象
private WeakReference<Context> mContextWeakRef;
private Singleton(Context mContext)
this.mContextWeakRef = new WeakReference<Context>(mContext);
public static Singleton getSingleton(Context context)
if (singleton == null)
synchronized (Singleton.class)
if (singleton == null)
// 如果context为Activity,则Activity内存泄漏
singleton = new Singleton(context);
return singleton;
</pre>
2.2.4. 系统服务内存泄漏
使用系统服务时,会调用 getSystemService()
方法,还有可能注册系统服务的监听器,这两处都可能引起内存泄漏。类似的还有 BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap 等,也需要在 onDestry 中及时的关闭、注销或者释放内存。
<pre data-language="java" id="1b44f048" class="ne-codeblock language-java" style="border: 1px solid #e8e8e8; border-radius: 2px; background: #f9f9f9; padding: 16px; font-size: 13px; color: #595959">// 直接调用getSystemService方法,往往是Activity,所以系统服务会一直持有它的引用
SensorManager sm = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
Sensor sensor = sm.getDefaultSensor(Sensor.TYPE_ALL);
// 设置监听器传入了Activity,同样会被系统服务一直持有它的引用
sm.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST);</pre>
解决办法:
-
使用
getApplicationContext().getSystemService()
获取系统服务。 -
在
onDestory
里面注销监听器,断开系统服务与 Activity 的引用关系。
2.3. 防止内存泄漏
● 需要 Context 时优先考虑 ApplicationContext 。
● 生命周期比 Activity 长的内部类对象,且内部类使用了外部类的成员变量,则
a. 手动将内部类改写成静态内部类;
b. 在静态内部类中使用弱引用来引用外部类的成员变量;
● 不再使用的对象,显式地赋值为 null,例如 Bitmap,先调用 recycle() 后,在赋值为 null。
2.4. LeakCanary 工具
在 dependencies 中加入 debugImplementation ‘com.squareup.leakcanary:leakcanary-android:2.6’ ,无需再添加任何调用代码。
当有内存泄漏发生时,LogCat 就会打印相关信息:
====================================
HEAP ANALYSIS RESULT
====================================
1 APPLICATION LEAKS
References underlined with "~~~" are likely causes.
Learn more at https://squ.re/leaks.
333202 bytes retained by leaking objects
Displaying only 1 leak trace out of 2 with the same signature
Signature: d01f393e64386a95c368a4c4209c91c136f7720
┬───
│ GC Root: Local variable in native code
│
├─ android.os.HandlerThread instance
│ Leaking: NO (PathClassLoader↓ is not leaking)
│ Thread name: 'LeakCanary-Heap-Dump'
│ ↓ Thread.contextClassLoader
├─ dalvik.system.PathClassLoader instance
│ Leaking: NO (ToastUtil↓ is not leaking and A ClassLoader is never leaking)
│ ↓ ClassLoader.runtimeInternalObjects
├─ java.lang.Object[] array
│ Leaking: NO (ToastUtil↓ is not leaking)
│ ↓ Object[].[188]
├─ com.example.androidtest.ToastUtil class
│ Leaking: NO (a class is never leaking)
│ ↓ static ToastUtil.toast
│ ~~~~~
├─ android.widget.Toast instance
│ Leaking: YES (This toast is done showing (Toast.mTN.mWM != null && Toast.mTN.mView == null))
│ Retaining 166.6 kB in 1384 objects
│ mContext instance of com.example.androidtest.LeakActivity with mDestroyed = true
│ ↓ Toast.mContext
╰→ com.example.androidtest.LeakActivity instance
Leaking: YES (ObjectWatcher was watching this because com.example.androidtest.LeakActivity received
Activity#onDestroy() callback and Activity#mDestroyed is true)
Retaining 69.9 kB in 1256 objects
key = 1f27bf38-b690-4983-b3f1-64bd991e7f9f
watchDurationMillis = 7553
retainedDurationMillis = 2549
mApplication instance of android.app.Application
mBase instance of android.app.ContextImpl
====================================
0 LIBRARY LEAKS
A Library Leak is a leak caused by a known bug in 3rd party code that you do not have control over.
See https://square.github.io/leakcanary/fundamentals-how-leakcanary-works/#4-categorizing-leaks
====================================
0 UNREACHABLE OBJECTS
An unreachable object is still in memory but LeakCanary could not find a strong reference path
from GC roots.
====================================
METADATA
Please include this in bug reports and Stack Overflow questions.
Build.VERSION.SDK_INT: 29
Build.MANUFACTURER: samsung
LeakCanary version: 2.6
App process name: com.example.androidtest
Stats: LruCache[maxSize=3000,hits=2715,misses=45083,hitRate=5%]
RandomAccess[bytes=2304319,reads=45083,travel=16121653814,range=16075212,size=20801120]
Heap dump reason: 5 retained objects, app is visible
Analysis duration: 3857 ms
Heap dump file path: /data/user/0/com.example.androidtest/cache/leakcanary/2021-03-02_13-54-08_891.hprof
Heap dump timestamp: 1614664454960
Heap dump duration: 1642 ms
====================================
原理
● RefWatcher.watch()创建了一个KeyedWeakReference用于去观察对象。
● 然后,在后台线程中,它会检测引用是否被清除了,并且是否没有触发GC。
● 如果引用仍然没有被清除,那么它将会把堆栈信息保存在文件系统中的.hprof文件里。
● HeapAnalyzerService被开启在一个独立的进程中,并且HeapAnalyzer使用了HAHA开源库解析了指定时刻的堆栈快照文件heap dump。
● 从heap dump中,HeapAnalyzer根据一个独特的引用key找到了KeyedWeakReference,并且定位了泄露的引用。
● HeapAnalyzer为了确定是否有泄露,计算了到GC Roots的最短强引用路径,然后建立了导致泄露的链式引用。
● 这个结果被传回到app进程中的DisplayLeakService,然后一个泄露通知便展现出来了。
https://www.yuque.com/u21424344/kcxtu4/nv8n22
最后
如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。
如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。
全套视频资料:
一、面试合集
二、源码解析合集
三、开源框架合集
欢迎大家一键三连支持,若需要文中资料,直接点击文末CSDN官方认证微信卡片免费领取
以上是关于Android内存优化详解的主要内容,如果未能解决你的问题,请参考以下文章
Android 内存优化dumpsys meminfo PID 查看单进程内存信息详解