Android内存优化

在实践操作当中,可以从三个方面着手减小内存使用,首先是减小对象的内存占用,其次是内存对象的重复利用,最后是避免对象的内存泄露。
也可以从从设备分级、Bitmap 优化和内存泄露这三个方面入手。

内存错误
如果发生了 OutOfMemoryError 异常,可以首先创建一个内存容量较低的模拟器。 AVD 管理器设置,可以通过这些设置控制设备的内存容量。
[[优化工具使用|工具排查]]

减小内存占用

  1. 资源和图片压缩,对于低端机用户可以关闭复杂的动画、或者某些功能;使用 565 格式的图片
  2. 一个空进程也会占用 10M 的内存,减少应用启动的进程数,减少常驻进程、有节操的保活,对低端机内存优化非常重要。
  3. Serializable全部改成 Parcelable(/ˈpɑːrsl/)[[Serializable 与 Parcelable]]
  4. arraymap,sarparray 代替 hashmap [[SparseArray 和 ArrayMap]]

Bitmap 优化

[[图片、glide优化]]
[[图片低配系统 oom 优化]]

内存对象的重复利用

  1. 使用线程池(对象池)
  2. 避免创建不必要的对象,单例
  3. 合理的使用缓存,比如图片是很耗内存的,使用lrucache
  4. 内存抖动

内存抖动

内存抖动是指在短时间内有大量的对象被创建或者被回收的现象,内存抖动出现原因主要是频繁(在循环里)创建对象(短时间内产生大量对象,需要大量内存,就可能会需要回收内存以用于产生对象,垃圾回收机制就自然会频繁运行了)。频繁内存抖动会导致垃圾回收频繁运行。解决的方法:

  1. 尽量避免在循环体内创建对象,应该把对象创建移到循环体外。
  2. 注意自定义View的onDraw()方法会被频繁调用,所以在这里面不应该频繁的创建对象。
  3. 当需要大量使用Bitmap的时候,试着把它们缓存在数组中实现复用。
  4. 对于能够复用的对象,同理可以使用对象池将它们缓存起来。
  5. 允许复用的情况下,使用对象池进行缓存,如:Handler的Message单链表(obtain);

内存泄漏

根本原因

[[内存泄漏简单问]]

Handler 都不能算是罪魁祸首,罪魁祸首(根本原因)都是他们的头头——线程。内部类引用就引用吧,无所谓,但是这个内部类是长期存在的就不行

unknown_filename.2

handle. post
view 中使用的 Context 就是当前的 Activity,而这个 runnable 一旦被 post,就会一直存在于队列里面,直到时间到了,被执行。
主线程 —> threadlocal —> Looper —> MessageQueue —> Message —> Handler->view —> Activity

常见内存泄漏

匿名内部类

在 Activity 中使用非静态的内部类,并开启一个长时间运行的线程,因为内部类持有 Activity 的引用,会导致 Activity 长期得不到回收,例如 handler(使用静态内部类加上弱引用的方式实现),或者 mHandler.removeCallbacksAndMessages(null);

匿名内部类,例如:AsyncTask 和 Runnable ,那么它们将持有其所在 Activity 的隐式引用。如果任务在 Activity 销毁之前还未完成,那么将导致 Activity 的内存资源无法被回收,从而造成内存泄漏。(将 AsyncTask 和 Runnable 类独立出来或者使用静态内部类)

解决方法:静态+弱引用

  • 内部类 Handler 对象会隐式地持有一个外部类对象(通常是一个 Activity)的引用(不然你怎么可能通过 Handler 来操作 Activity 中的 View?)
  • PS: 在 Java 中,非静态的内部类和匿名内部类都会隐式地持有其外部类的引用,静态的内部类不会持有外部类的引用。静态类不持有外部类的对象,所以你的 Activity 可以随意被回收。
  • 由于 Handler 不再持有外部类对象的引用,导致程序不允许你在 Handler 中操作 Activity 中的对象了。所以你需要在 Handler 中增加一个对 Activity 的弱引用(WeakReference)。

泄漏例子:在 ondestory 里 ref1没问题,ref2就内存泄漏, ref2是匿名内部类
unknown_filename.1

单例

由于单例的静态特性使得其生命周期和应用的生命周期一样长,如果一个对象已经不再需要使用了,而单例对象还持有该对象的引用,就会使得该对象不能被正常回收,从而导致了内存泄漏。(使用 Application 的 context)

资源未关闭造成

BraodcastReceiver,ContentObserver,Cursor,IO,Bitmap(close 或取消注册)、线程池 shutdown、handlerThread quick. 属性动画当设置成无限循环时,需要 cancel

listener 也要置为 null(getViewTreeObserver)

WebView 造成的泄露(当我们不要使用 WebView 对象时,应该调用它的 destory ()函数来销毁它,并释放其占用的内存,否则其长期占用的内存也不能被回收,从而造成内存泄露)。

不要多余的成员变量或临时变量。成员变量(全局变量)。activity 持有成员变量的引用,比如说 arraylist,集合容器中的内存泄露(在退出程序之前,将集合里的东西 clear,然后置为 null,再退出程序。或者创建成局部变量 (短内存泄漏)在堆上,多了也占内存

IntentService

  • 使用 intentservice,普通服务后台任务运行完,即使它不执行任何操作,服务也会一直运行,这些是十分消耗内存的。
  • IntentService 子线程分担部分初始化工作。开启 IntentSerVice 线程,将部分逻辑和耗时的初始化操作放到这里处理,可以减少 application 初始化时间

生命周期

尽量不要用一个生命周期长于 Activity 的对象来持有 Activity 的引用

三种静态
  1. 静态内部类:尽量不要用一个生命周期长于Activity的对象来持有Activity的引用。声明handler为static,这样内部类就不再持有外部类的引用了,就不会阻塞Activity的释放。在Activity中尽量避免使用生命周期不受控制的非静态类型的内部类,可以使用静态内部类加上弱引用的方式实现。
  2. 静态变量:不要直接或者间接引用Activity、Service等。这会使用Activity以及它所引用的所有对象无法释放,然后,用户操作时间一长,内存就会狂升。
  3. 静态引用:应该避免 static 成员变量引用资源耗费过多的实例,比如 Context。尽量使用 getApplicationContext,因为 Application 的 Context 的生命周期比较长,引用它不会出现内存泄露的问题,而不是用 activity 的 context。可以通过调用 Context.getApplicationContext () or Activity.getApplication ()来获得
静态方法

静态对象实例存在于堆中
静态方法在 stack,运行完就出栈了
静态成员变量在方法区
局部变量是不能静态的

类的静态成员变量(static成员变量)存储在方法区(Method Area)中,而非堆内存。静态成员变量独立于对象而存在,它们属于类本身而不是对象的一部分。

为什么 Java 静态方法引用的属性也必须是静态的

  • 静态方法不需要 new 对象,只要 class 文件被 ClassLoader   load 进入JVM 的 stack,该静态方法即可被调用。当然此时静态方法是存取不到 heap 中的对象属性的。  
  • 非静态方法执行前,要先new对象,在heap中分配数据,并把stack中的地址指针交给非静态方法,这样程序计数器依次执行指令,而指令代码此时能够访问到heap数据区了。

和静态方法没关系,主要是静态变量持有activity的引用

1
2
3
4
5
6
7
public static Fragment newInstance(ArrayList<SpeakKey> data, int parentIndex) {
LearnSpeakScoreFragment fragment = new LearnSpeakScoreFragment();
Bundle bundle = new Bundle();
bundle.putInt("parentIndex", parentIndex);
fragment.setArguments(bundle);
return fragment;
}

不会内存泄漏,因为LearnSpeakScoreFragment又没持有activity的引用。方法运行完了就出栈了

标准的 handler 写法

静态内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private static class MyHandler extends Handler {
private WeakReference<Context> reference;
public MyHandler(Context context) {
reference = new WeakReference<>(context);
}
@Override
public void handleMessage(Message msg) {
MainActivity activity = (MainActivity) reference.get();
if(activity != null){
// activity.mTextView.setText("");
}
}
}

//标准的单例
private static AppManager instance;
private Context context;
private AppManager(Context context) {
this.context = context.getApplicationContext();// 使用Application 的context
}
public static AppManager getInstance(Context context) {
if (instance == null) {
instance = new AppManager(context);
}
return instance;
}

(特别对于 Context,View,Fragmet,Activity 对象),如果要将其放进类内部的容器对象或者静态类中引用,请一直用 WeakReference 包装!比如在 TabLayout 的源码中,在 TabLayoutOnPageChangeListener 中,就为 TabLayout 做了 WeakReference wrap。

unknown_filename

用完后记得 clear 掉

1
2
3
4
if (homeRef != null) {  
homeRef!!.clear()
}
homeRef = null

引用

对象的引用分为四种级别,从而使程序更加灵活的控制对象的生命周期。这四种级别由高到低依次为:强引用、软引用、弱引用和虚引用。

  1. 强引用是Java的默认引用实现, 它会尽可能长时间的存活于 JVM(虚拟机) 内, 当没有任何对象指向它时(显式地将引用赋值为nul)才会在合适的时间,进行垃圾回收。
  2. 软引用 如果内存空间不足了,就会回收这些对象的内存。
  3. 弱引用 WeakReference  弱引用的对象拥有更短的生命周期,只要垃圾回收器扫描到它,不管内存空间充足与否,都会回收它的内存。对 WeakReference 对象标记为可回收,并在下一次垃圾回收时被回收。
  4. 虚引用 PhantomReference  虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。

其他

Android 系统中 GC 内存泄漏的原因

主动回收内存System.gc();、getruntime.runtime.gc
导致内存泄漏主要的原因是,申请了内存空间而忘记了释放。如果程序中存在对象的引用,这个对象就被定义为”有效的活动(引用可达)”,无法让垃圾回收器GC验证这些对象是否不再需要,这些对象就会驻留内存,消耗内存。典型的做法就是把对象数据成员设为null或者从集合中移除该对象。但当局部变量不需要时,不需明显的设为null,因为一个方法执行完毕时,这些引用会自动被清理。

什么是 GC

GC垃圾收集器,它让创建的对象不需要像c/c++那样delete、free掉,GC的时间系统自身决定,时间不可预测。 对超出作用域的对象或引用置为空的对象进行清理,删除不使用的对象,腾出内存空间。

内存溢出和内存泄漏

内存泄露 memory leak,是指程序在申请内存后,忘了释放,就出现了内存泄漏,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。memory leak会最终会导致out of memory!

内存溢出是要求分配的内存超出了系统能给的,系统不能满足需求,于是产生溢出。

Java 带垃圾回收的机制,为什么还会内存泄露呢?

举个例子 当你堆里某个对象没有被引用时,然后再过一段时间,垃圾回收机制才会回收,那么

1
whiletrue){String str=new String("ni hao ni hao ");}

一直循环创建 String对象。。。你觉得堆不会溢出嘛。。。

内存泄露的根本原因就是保存了不可能再被访问的变量类型的引用和回收的不确定性

GC Roots

GC Roots算法判定一个对象需要被回收,GC Roots一般在JVM的栈区域里产生。
GC Root 的分类中:

  1. 运行中的线程,thread就是一种gcroot,gcroot->asyntask->activity(泄露)
  2. 静态变量、静态对象也是一种gcroot,例如单例 (长生命周期)
1
2
3
4
5
Utils.leakviews.add(view);
public class Utils {
public static LinkedList<View> leakviews =new LinkedList<>( );
}

  1. 来自本地代码的引用,JNI的引用是指 那些本地代码执行JNI调用过程中所创建出来的Java对象

unknown_filename.3

线程间的可见性的内存泄漏

  • java 的每个线程有独立的工作内存,他们的工作方式是从主内存将变量读取到自己的工作内存,然后在工作内存中进行逻辑或者自述运算再把变量写回到主内存中。正常情况下各线程的工作内存之间是相互隔离的、不可见的。
  • 主线程和子线程都有一份这个变量,主线程调用置 null,而子线程中的变量没有从主内存中更新,所以对于子线程而言,依然不为 null,解决办法就是对这个变量加上 volatile 关键字,当更新后,使得子线程立即从主内存中更新。

实例

局部变量内存泄漏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
MethodStack.startwithTarget(view);
public class MethodStack implements Runnable{
private Object ref;
MethodStack(Object ref){
this.ref=ref;
}
public static void startwithTarget(Object ref) {
//暂时让成员变量引用到泄漏的目标
new Thread(new MethodStack(ref)).start();
}

@Override
public void run() {
//把成员变量赋值给局部变量,然后让成员变量不在为null
Object reg=this.ref;
this.ref=ref;

while (true){
SystemClock.sleep(1000);
Log.d("Leak",ref+"");
}
}
}

Applicatlion 内存泄漏

((App) getApplication()).leakviews.add(view);

1
2
3
4
public class App extends Application {
public ArrayList<Object> leakviews = new ArrayList<>();
}

外部类 handler 解决方法

handler. removeCallbacksAndMessages (null);
removeCallbacks (Runnable r)和 removeMessages (int what)

1
2
3
4
5
6
7
8
9
10
11
HandlerThread thread = new HandlerThread("xxx");
thread.start();
Handler handler = new Handler(thread.getLooper());
handler.postDelayed(new Runnable() {
    @Override
    public void run() {
view.setVisibility(View.GONE);
    }
}, 10000);

thread.quit();

Android内存优化
http://peiniwan.github.io/2024/04/e3ba049ad0b1.html
作者
六月的雨
发布于
2024年4月6日
许可协议