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的onMeasureonLayoutonDraw等方法中调用invalidate()方法(通知主线程当前画面已过期,再线程空闲时重绘),且不要创建大量对象。

  • 当需要大量使用 Bitmap 的时候,试着把它们缓存在数组中实现复用。

  • 对于能够复用的对象,同理可以使用对象池将它们缓存起来。

  • 字符串拼接,改用StringBufferStringBuilder来执行。

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 查看单进程内存信息详解

Android 内存优化dumpsys meminfo PID 查看单进程内存信息详解

Android内存优化—dumpsys meminfo详解

LruCache详解之 Android 内存优化

求助 解决png图片 锯齿边缘 的优化

Google官方 详解 Android 性能优化史诗巨著之内存篇