Android 性能优化(一)内存篇

欢迎转载,转载请标明出处:
http://blog.csdn.net/johnny901114/article/details/54377370
本文出自:【余志强的博客】
本博客同时也发布在 Hoolay Team

一、android官方一些内存方面的内存tips

1、避免创建不必要的对象。

  • 如尽量避免字符串的加号拼接,可以使用StringBuilder来拼接。
  • 如果需要TextView设置多个字符串片段,可以使用textView.append方法,不要直接用加号拼起来。

2、尽量使用for-each循环,对于ArrayList,请使用普通的for,如:

int len = list.size();
for (int i = 0; i < len; ++i) {
    //todo somgthing
}
  • 1
  • 2
  • 3
  • 4

3、使用系统库函数。

  • 使用系统库函数,并且还有汇编级别的优化,他们通常比带有JIT的Java编译出来的代码更高效 如:System.arraycopy(); String.indexOf()等。
  • 如果系统函数能够解决的,不要加入第三方库,可能第三方库使用起来比较简单(jsoup,htmlparser等)。如解析HTML可以使用系统的XmlPullParser

二、使用 ArrayMap、SparseArray代替HashMap

ArrayMap 和 HashMap的在内存的使用上,更加高效。

ArrayMap实现上有两个数组,一个数组是保存key hash,另一个数组保存value,ArrayMap通过二分法(binary search)来进行查找的。

HashMap通过一个数组来实现的,key hash作为数组的索引,这样就需要更大的内存来减少key hash的冲突,key hash就是数组的索引,所以查找效率很高。

使用建议:

  • 当数据量比较小的时候(小于1000),优先使用ArrayMap,否则使用HashMap。

  • map里嵌套map。

使用方法:

  • ArrayMap和HashMap的使用方法都是一样的,ArrayMap也实现了Map接口。

  • 另外,ArrayMap可以通过keyAt(index)方法来获取第index位置的key,keyValue(int index)同理。但是HashMap是不可以的。

  arrayMap.keyAt(0);
  arrayMap.valueAt(0);
  • 1
  • 2

SparseArray和ArrayMap非常像,它们都是通过两种紧密包装的数组,而不是一个大的哈希散列,从而减少了整个内存的覆盖区。但是查询的速度就慢了。

只不过SparseArray和ArrayMap最大的区别是SparseArray的key是一个基本类型。

SparseArray的key是int类型,而不是Integer。像以前使用HashMap的时候,如果key是整形,必须是Integer。

Integer占16个字节,int只占4个字节,如果元素比较多,从而可以很好的减少内存的占用。

除了SparseArray类还有如下类可供使用:

SparseBooleanMap <boolean,Object>
SparseIntMap <int,Object>
SparseLongMap <long,Object>
  • 1
  • 2
  • 3

SparseArray和ArrayMap的使用建议使用方法都是一样的。

三、Thread与Thread Pool

在android开发中,一些耗时的操作都会放到后台线程去执行,比如:网络、本地文件、数据库等。

    new Thread(new Runnable() {
        @Override
        public void run() {
            //do something...
        }
    }).start();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

每个线程至少消耗64k的内存,如果你在某个时间点,迅速开启了很多线程(比如加载列表图片,然后用户滑动列表),这个时候可能内存使用量就会飙升。

  • 会出现内存抖动(memory churn),因为短时间开启了很多线程,完成任务后,这些线程都会被回收。内存表现为:低-高-低。甚至可能出现OOM。

  • 一个系统所能处理的线程数量是有限的,如果超多了最大承载量,性能会受到很大的影响。而且可能还会影响用户的后续操作。

这时候Thread Pool线程池的作用就凸显出来了。

Java为我们提供了操作线程池的api ThreadPoolExecutor ,ExecutorService是一个接口,相关的线程池的类都实现了该接口,如 ThreadPoolExecutor 。

创建一个线程池可以通过 ThreadPoolExecutor类来实现。

ThreadPoolExecutor pool = new ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue);//新建一个线程池
pool.execute(Runnable);//执行任务
  • 1
  • 2

下面是官方对ThreadPoolExecutor的参数说明:

Parameters:
    corePoolSize - the number of threads to keep in the pool, even if they are idle, unless allowCoreThreadTimeOut is set
    maximumPoolSize - the maximum number of threads to allow in the pool
    keepAliveTime - when the number of threads is greater than the core, this is the maximum time that excess idle threads will wait for new tasks before terminating.
    unit - the time unit for the keepAliveTime argument
    workQueue - the queue to use for holding tasks before they are executed. This queue will hold only the Runnable tasks submitted by the execute method.
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • corePoolSize 核心线程数,核心线程会一直存活,即使没有任务需要处理。当线程数小于核心线程数时,即使现有的线程空闲,线程池也会优先创建新线程来处理任务,而不是直接交给现有的线程处理。核心线程在allowCoreThreadTimeout被设置为true时会超时退出,默认情况下不会退出。

  • maxPoolSize 线程池允许最大的线程数量。

  • keepAliveTime 当线程空闲时间达到keepAliveTime,该线程会退出,直到线程数量等于corePoolSize。如果allowCoreThreadTimeout设置为true,则所有线程均会退出直到线程数量为0。

  • allowCoreThreadTimeout 是否允许核心线程空闲keepAliveTime退出,默认值为false。

  • workQueue 任务队列。pool.execute(runnable)提交的task都会放到workQueue。

下面来一个简单的sample:

public class MyClass {

    private ThreadPoolExecutor pool ;

    private MyClass(){
        //创建线程池
        pool = new ThreadPoolExecutor(4, 7, 60, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
    }

    public static void main(String[] args) {
        MyClass myClass = new MyClass();
        for (int i = 0; i < 10; i++) {
            //提交任务
            myClass.pool.execute(new MyRunnable(myClass));
        }
        myClass.pool.shutdown();
    }

    private String getCount() {
        return pool.getCorePoolSize()+"-"+pool.getActiveCount() + "-" + pool.getMaximumPoolSize();
    }

    private static class MyRunnable implements Runnable {
        MyClass myClass;

        MyRunnable(MyClass myClass) {
            this.myClass = myClass;
        }

        @Override
        public void run() {
            System.out.println("thread name:" + Thread.currentThread().getName() + " start " + myClass.getCount());
            try {
                //模拟耗时任务
                Thread.sleep(3000L);
                System.out.println("thread name:" + Thread.currentThread().getName() + " end " + myClass.getCount());

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
  • 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
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

上面的代码很简单:创建了一个corePoolSize为4,maxPoolSize为7的线程池。
然后往线程池里提交10个任务,每个任务打印pool.getCorePoolSize()+”-“+pool.getActiveCount() + “-” + pool.getMaximumPoolSize(),即corePoolSize(核心线程数),activeCount(正在活动的线程总数)和maximumPoolSize(线程池允许的最大线程数)值。

测试结果如下:

thread name:pool-1-thread-1 start 4-2-7
thread name:pool-1-thread-2 start 4-4-7
thread name:pool-1-thread-3 start 4-4-7
thread name:pool-1-thread-4 start 4-4-7
thread name:pool-1-thread-1 end 4-4-7
thread name:pool-1-thread-2 end 4-3-7
thread name:pool-1-thread-4 end 4-4-7
thread name:pool-1-thread-1 start 4-4-7
thread name:pool-1-thread-4 start 4-4-7
thread name:pool-1-thread-2 start 4-4-7
thread name:pool-1-thread-3 end 4-4-7
thread name:pool-1-thread-3 start 4-4-7
thread name:pool-1-thread-2 end 4-4-7
thread name:pool-1-thread-4 end 4-4-7
thread name:pool-1-thread-3 end 4-4-7
thread name:pool-1-thread-4 start 4-4-7
thread name:pool-1-thread-1 end 4-3-7
thread name:pool-1-thread-2 start 4-4-7
thread name:pool-1-thread-4 end 4-2-7
thread name:pool-1-thread-2 end 4-2-7

Process finished with exit code 0
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

从测试结果来看,我们打印pool.getCorePoolSize()+”-“+pool.getActiveCount() + “-” + pool.getMaximumPoolSize()的值是正常的。但是只创建了4个线程:

pool-1-thread-1
pool-1-thread-2
pool-1-thread-3
pool-1-thread-4
  • 1
  • 2
  • 3
  • 4

我们设置了线程池的最大数为7,我们提交了10个任务,但是为什么只创建了corePoolSize=4个线程?

查看官方文档可以找到答案:

  • 当通过execute(Runnable)提交一个新任务,并且小于corePoolSize正在运行的线程数,将会创建一个新的线程来处理这个任务,不管线程池里有没有空闲的线程。

  • If there are more than corePoolSize but less than maximumPoolSize threads running, a new thread will be created only if the queue is full.
    大于corePoolSize小于maximumPoolSize,workQueue队列满了,才会创建新的线程。

  • 如果corePoolSize和maximumPoolSize值设置成一样的,相当于创建了一个固定数量的线程池。

  • 多数情况下,都是通过构造方法来设置corePoolSize和maximumPoolSize,但是也可以通过setCorePoolSize和setMaximumPoolSize来动态设置。

所以上面的例子,只创建了4个线程,因为虽然我们提交了10个任务,但是构建workQueue时候没有传入队列大小,默认大小是Integer.MAX_VALUE,所以workQueue是不会满的。所以最多就创建了4个线程。

据此,我把workQueue队列容量改成4:

pool = new ThreadPoolExecutor(4, 7, 60, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(4));
  • 1

测试结果:

thread name:pool-1-thread-1 start 4-2-7
thread name:pool-1-thread-2 start 4-2-7
thread name:pool-1-thread-3 start 4-3-7
thread name:pool-1-thread-4 start 4-4-7
thread name:pool-1-thread-5 start 4-6-7
thread name:pool-1-thread-6 start 4-6-7
thread name:pool-1-thread-1 end 4-6-7
thread name:pool-1-thread-2 end 4-6-7
thread name:pool-1-thread-2 start 4-5-7
thread name:pool-1-thread-1 start 4-6-7
thread name:pool-1-thread-3 end 4-6-7
thread name:pool-1-thread-3 start 4-6-7
thread name:pool-1-thread-4 end 4-6-7
thread name:pool-1-thread-5 end 4-6-7
thread name:pool-1-thread-4 start 4-6-7
thread name:pool-1-thread-6 end 4-5-7
thread name:pool-1-thread-1 end 4-4-7
thread name:pool-1-thread-2 end 4-4-7
thread name:pool-1-thread-3 end 4-2-7
thread name:pool-1-thread-4 end 4-1-7

Process finished with exit code 0
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

发现创建了6个线程,大于上一次的测试结果(上一次是创建了4个线程),可是我们设置的maximumPoolSize为7,按道理应该是创建7个线程才对呀,这是为什么呢?

这需要了解下workQueue队列的策略了。我们上面的列子使用的是 LinkedBlockingQueue。

下面来看看官方文档对 BlockingQueue的描述:

Any link BlockingQueue may be used to transfer and hold
submitted tasks.  The use of this queue interacts with pool sizing:
If fewer than corePoolSize threads are running, the Executor
always prefers adding a new thread
rather than queuing.

If corePoolSize or more threads are running, the Executor
always prefers queuing a request rather than adding a new
thread.

If a request cannot be queued, a new thread is created unless
this would exceed maximumPoolSize, in which case, the task will be
rejected.
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

主要意思就是:

  • 如果运行的线程少于 corePoolSize,Executor会创建新线程来执行任务,不会把任务放进queue。

  • 如果运行的线程等于或多于 corePoolSize,Executor将请求加入队列,而不是创建新的线程。

  • 如果队列已满,无法将请求加入队列,则创建新的线程,除非创建此线程超出 maximumPoolSize,在这种情况下,任务将被拒绝。

这样就能解释为什么只创建6个线程了。

总共有10个task,核心线程corePoolSize=4,所以3个任务是不会放进queue的,直接创建3个新线程来处理task了,然后再执行execute(Runnable)的时候,就会大于等于corePoolSize,所以就会把接下来的4个任务放进queue(容量为4),然后就剩下3个task了,发现队列已经满了,创建3个线程来处理这剩下的3个task,所以总共只创建6个线程了。

maximumPoolSize=7,我就是想让它创建7个线程,我们知道了上面的原理就很简单了,可以把队列的最大容量改成3或者添加11个任务就可以了:

new LinkedBlockingQueue<Runnable>(3)
  • 1

for (int i = 0; i < 11; i++) {
    myClass.pool.execute(new MyRunnable(myClass));
}
  • 1
  • 2
  • 3

效果如下:

thread name:pool-1-thread-1 start 0-1-7
thread name:pool-1-thread-2 start 0-2-7
thread name:pool-1-thread-3 start 1-4-7
thread name:pool-1-thread-4 start 4-5-7
thread name:pool-1-thread-5 start 4-7-7
thread name:pool-1-thread-6 start 4-7-7
thread name:pool-1-thread-7 start 4-7-7
.....
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

Java提供了3种策略的Queue

  • SynchronousQueue 直接传送task(Direct handoffs)

  • LinkedBlockingQueue 无边界队列(Unbounded queues)

  • ArrayBlockingQueue 有边界队列(Bounded queues)
    更多详细信息可以查看官方文档。

Java给我们提供了一些工厂方法来来创建线程池():

  • Executors.newFixedThreadPool(int threads);

  • Executors.newCachedThreadPool();

  • Executors.newSingleThreadExecutor();

  • Executors.newScheduledThreadPool(int threads);

这些方法都是通过构建ThreadPoolExecutor来实现的,具体的细节可以去看看文档,如果都不满足你的需求,可以自己构造ThreadPoolExecutor。

四、IntentService与Service

一般我们在app里的版本更新逻辑在Service里起一个线程来检测。

为了避免Service一直存在,减少内存消耗,检测版本后,还需要手动stopSelf,略麻烦。

这时候用IntentService就比较合适了,默认就给你启动了一个线程来执行耗时操作,完成自动关闭service。

Service和IntentService主要区别:

  • IntentService执行完会自动关闭(stopSelf),而Service不会。

  • IntentService会启动一个线程来执行耗时操作,把耗时操作放到onHandleIntent(Intent intent)方法里。而Service需要自己new Thread。

  • 如果调用startService(intent)多次,IntentService会执行多次onHandleIntent(Intent intent),且必须等本次的onHandleIntent(Intent intent)执行完,才会执行下一次onHandleIntent(Intent intent),说白了就是如果正在执行任务,会把后面启动的命令放到队列里。而多次调用startService(intent),Service仅仅会多次调用onStartCommand方法。

五、避免常见的内存泄露

1、CountDownTimer、TimerTask、Handler导致的内存泄露

在项目中,我们常常可能要做活动倒计时的功能,我是用CountDownTimer来做的。如:

public static class TimeCounter extends CountDownTimer {
    public TimeCounter(long millisInFuture, long countDownInterval) {
        super(millisInFuture, countDownInterval);
    }

    @Override
    public void onFinish() {
        //倒计时结束
    }

    @Override
    public void onTick(long millisUntilFinished) {
        //每间隔固定时间执行一次
        //在此处理倒计时逻辑
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

如下图所示:

倒计时
因为要在TimeCounter内部要修改View的显示,所以要把TextView传递进来,使用WeakReference来引用TextView避免内存泄露,如:

public static class TimeCounter extends CountDownTimer {

    private WeakReference<TextView> weakText;

    public TimeCounter(TextView textView, long millisInFuture, long countDownInterval) {
        super(millisInFuture, countDownInterval);
        weakText = new WeakReference<>(textView);
    }

    @Override
    public void onFinish() {
        //倒计时结束
    }

    @Override
    public void onTick(long millisUntilFinished) {
        //每间隔固定时间执行一次
        //再次处理倒计时逻辑
    }

    private void setText(long millisUntilFinished){
        if (weakText.get() != null) {
            weakText.get().setText(xxx);
        }
    }
}
  • 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

我们知道,使用WeakReference包装TextView,在发生GC的时候,TextView就会被回收。

需要注意的是,只有WeakReference所引用的对象没有被其他对象引用,当发生了GC,WeakReference所引用的对象才会被回收的。

例如,A界面有个倒计时功能,然后把TextView传给了上面的TimeCounter,然后在A界面的基础上启动了其他界面,这时候假如发生了GC,这时候TextView是不会被回收的。

因为还有A界面(Activity)对TextView还有引用(强引用)。所以只有把A界面关闭了(也就是Activity对TextView的强引用没有了),且发生GC了 TextView才会被收回。也就是说 当一个对象仅仅被弱引用引用的时候, 发生GC该对象才会被回收

在网上也看到一些,加上了WeakReference在GC的时候也没有释放。可能的原因是WeakReference所引用的对象被其他对象强引用着,所以发生GC了,该对象还是没被回收。

类似这样的内存泄露除了CountDownTimer还有Timer、Handler 等,表现如下:

Timer

//设置为每秒执行一次(TimerTask的run方法是在后台线程执行的)
new Timer().scheduleAtFixedRate(
        new TimerTask() {
            @Override
            public void run() {
                Log.e("Timer", "timer==========");
            }
        }, 0, 1000);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

Handler

//设置为每秒执行一次
handler = new android.os.Handler() {
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        Log.e("Handler", "Handler");
        sendEmptyMessageDelayed(1, 1000);
    }
};
handler.sendEmptyMessage(1);

//不可见时 移除消息
@Override
protected void onStop() {
    super.onStop();
    handler.removeMessages(1);
}
//可见的时 发送消息
@Override
protected void onResume() {
    super.onResume();
    if (!handler.hasMessages(1)) {
        sendEmptyMessageDelayed(1, 1000);
    }
}
  • 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

上面的 TimerHandler 都可以实现诸如每隔1秒执行的功能,都会导致内存泄露的问题。

解决方案:

  • 使用静态内部类(相当于外部类,访问域不一样),如果需要使用外部类的变量 如View,使用WeakReference引用外部的View;
  • 当界面关闭的时候一定要把定时器 Timer或 CountDownTimer关闭掉(cancel),如果是Handler使用removeMessages(int what)方法;

2、内部类(如Thread)导致的内存泄露

在项目常常要做一些耗时操作,可以起一个Thread来做,如:

new Thread(new Runnable() {
    @Override
    public void run() {
        Log.e("Log", "执行耗时操作");
    }
}).start();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

如果在Activity直接这样使用,容易造成activity的内存泄露。

因为上面的Thread代码段,实际上是匿名内部类对象,它会对外部类(Activity)有一个引用。

如果Thread一直在执行,就算用户按了返回键,activity对象会一直存在的。

因为匿名内部类对象一直引用者activity对象。

是否泄露在于Thread执行的耗时任务执行时间,如果Thread执行非常短时间就完毕了,基本上造成不了多大的内存泄露,但是耗时任务执行的时间无法预测的。

下面一个简单的例子来演示下这个问题:

public class ThreadActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activty_count_down_timer);
        new Thread(new Runnable() {
            @Override
            public void run() {
                Log.e("ThreadActivity", "执行耗时操作------" + ThreadActivity.this);
                try {
                    //模拟耗时操作60秒
                    Thread.sleep(1000 * 60);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Log.e("ThreadActivity", "耗时操作执行完成------" + ThreadActivity.this);
            }
        }).start();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Log.e("ThreadActivity", "onDestroy------");
    }
}
  • 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

运行起来后,接着按返回键,测试效果如下:

执行耗时操作------com.chiclaim.twitter.ThreadActivity@2e92d7ee
onDestroy------
耗时操作执行完成------com.chiclaim.twitter.ThreadActivity@2e92d7ee
  • 1
  • 2
  • 3

从上面的代码可以看出,耗时任务我们定为60秒,启动了Activity后,立马输出了:

执行耗时操作------com.chiclaim.twitter.ThreadActivity@2e92d7ee
  • 1

紧接着按了返回键,输出了:

onDestroy------
  • 1

过了大概60秒,输出了:

耗时操作执行完成------com.chiclaim.twitter.ThreadActivity@2e92d7ee
  • 1

从上面的例子中我们得出了2个结论:

  • 匿名内部类对象,它会对外部类有一个引用。
  • Activity执行了onDestroy方法,不见得Activity被销毁了。上面的例子中,过了60秒依然可以输出外部类(Activity)对象。

解决方案:

  • 尽量减少非静态内部类的使用,上面的例子可以使用静态匿名内部类对象或者使用静态内部类,这样就不会对外部类对象由引用了。
  • 在app中的这种耗时任务,一般我们处理逻辑的时候,只需要处理成功失败的情况,比如说网络请求,如果一个界面有多个请求,那么就会有很多内部类(回调)嵌套了,我的做法是使用者实现一个回调接口,该接口定义了成功和失败的两个方法,Activity onCreate的时候,把回调注册到一个容器内,在onDestroy方法里从容器中移除该回调。这样也不会对Activity造成内存泄露。
  • 另外,如果某个耗时操作,需要传入Context,如果没有特殊的要求,不要传递Activity的Context。传入Application级别的Context,这样不管耗时操作执行多久,都不会导致Activity内存泄露。

3、由static的Activity、View、List导致的内存泄露

千万不要是有static来修饰activity、View对象,这种内存泄露更加严重,因为它将贯穿程序的生命周期。
为了更好的管理Activity常常把Activity放进容器中,如Stack。如果忘记了移除,也会造成内存泄漏。

六、onTrimMemory(int level)与onLowMemory()

onTrimMemory 回调是 Android 4.0 之后提供的一个API。

它的主要用来提示开发者在系统内存不足的时候,根据当前内存情况(level),释放相关资源以减小系统内存压力,这样可以减少app进程被系统杀死的可能性。

尽可能的保存app进程,等到用户在下一次使用的时候,启动速度就会比较快。

在Android 4.0之前都是onLowMemory来处理这类逻辑的,onLowMemory和onTrimMemory中的TRIM_MEMORY_COMPLETE级别相同。如果想兼容Android4.0之前的系统可以实现该方法,否则只需要处理onTrimMemory方法。

下面来聊一聊onTrimMemory(int level)回调level的常量:

TRIM_MEMORY_UI_HIDDEN = 20 表示应用程序的所有UI界面被隐藏了,即用户点击了Home键或者剩下最后一个界面,按返回键后,Application的onTrimMemory回调也会调用。

下面三个等级是当我们的应用程序正在前台运行时的回调:

  • TRIM_MEMORY_RUNNING_MODERATE = 5 表示应用程序正常运行,并且不会被杀掉。但是目前手机的内存已经有点低了,你的正在运行的进程需要释放一些不需要的内存资源。

  • TRIM_MEMORY_RUNNING_LOW = 10 表示应用程序正常运行,并且不会被杀掉。但是目前手机的内存已经非常低了,你的正在运行的进程需要释放一些不需要的内存资源。

  • TRIM_MEMORY_RUNNING_CRITICAL = 15 表示应用程序仍然正常运行,但是系统内存已经极度低了,即将不能保留任何后台进程 了。这个时候我们应当尽可能地去释放任何不必要的资源,下一步onLowMemory将会被调用,这样的话,后台将不会保留任何进程。

当app进程不可见处于LRU list中,则会收到以下常量的回调:

  • TRIM_MEMORY_BACKGROUND = 40 app进程不可见,处于LRU列表中,这时候是个释放资源的好时机。

  • TRIM_MEMORY_MODERATE = 60 系统目前内存已经很低了,并且我们的程序处于LRU缓存列表的中间位置。腾出一些内存让系统运行其他的进程。

  • TRIM_MEMORY_COMPLETE = 80 系统目前内存已经很低了,并且我们的程序处于LRU缓存列表的最边缘位置,如果系统找不到更多可能的内存,我们的app进程很快将被杀死。

从上面可以看出,这些常量大致可以分为两类,一类是大于TRIM_MEMORY_UI_HIDDEN = 20,这类表示进程不可见。一类是小于TRIM_MEMORY_UI_HIDDEN = 20,这类表示app进程正在前台运行。并且常量值越大,说明系统内存越紧张。

下面是官方推荐实现方案

/**
* Release memory when the UI becomes hidden or when system resources become low.
* @param level the memory-related event that was raised.
*/
public void onTrimMemory(int level) {

// Determine which lifecycle or system event was raised.
switch (level) {

    case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:
        /*
           Release any UI objects that currently hold memory.

           The user interface has moved to the background.
        */
        break;

    case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE:
    case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW:
    case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL:
        /*
           Release any memory that your app doesn't need to run.

           The device is running low on memory while the app is running.
           The event raised indicates the severity of the memory-related event.
           If the event is TRIM_MEMORY_RUNNING_CRITICAL, then the system will
           begin killing background processes.
        */
        break;

    case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND:
    case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
    case ComponentCallbacks2.TRIM_MEMORY_COMPLETE:
        /*
           Release as much memory as the process can.

           The app is on the LRU list and the system is running low on memory.
           The event raised indicates where the app sits within the LRU list.
           If the event is TRIM_MEMORY_COMPLETE, the process will be one of
           the first to be terminated.
        */
        break;

    default:
        /*
          Release any non-critical data structures.

          The app received an unrecognized memory level value
          from the system. Treat this as a generic low-memory message.
        */
        break;
}
  • 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
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53

如何针对上面的这些常量,分别释放app里哪些资源呢?(目前我只处理app在后台的情况)

当回调的参数level=TRIM_MEMORY_BACKGROUND(40)或TRIM_MEMORY_MODERATE(60)或TRIM_MEMORY_COMPLETE(80)时,

  • 把图片占用的内存释放掉。

  • 清空缓存数据,例如列表中List的数据; 还可以把动态创建的View或fragment释放掉, 甚至activity的相关资源都释放掉变成空Activity,从新回到这个Activity的时候,重新初始化数据。

  • 我会把所有的activity移除掉,仅留下主Activity。当然如果主Activity比较复杂,占用的资源比较多,可以把资源都释放掉,留下一个空主Activity。当用户回来的时候可以迅速回来,如果内容清空了,重新加载即可。

相关注意事项:

  • 在回调中释放内存,要注意该要释放的界面是否可见,界面如果正在显示,你却把当前的界面的List数据清空了,这样显然是不妥的,系统会通知所有实现了onTrimMemory方法的相关组件(Application、Activity、Fragment、Service、ContentProvider)

  • 还要注意,做好相关数据恢复的逻辑,例如,把相关的数据清空了,该界面重新显示的时候,要把相关释放的数据,重新加载,如果可以的话,尽可能回到用户上一次操作的状态,例如滑动的位置,所填写的数据等。

参考链接

http://blog.csdn.net/zhouhl_cn/article/details/7392607

https://www.youtube.com/watch?v=uCmHoEY1iTM&list=PLWz5rJ2EKKc9CBxr3BVjPTPoDPLdPIFCE&index=6

https://www.youtube.com/watch?annotation_id=annotation_2743589091&feature=iv&src_vid=uCmHoEY1iTM&v=NwFXVsM15Co

http://stackoverflow.com/questions/15524280/service-vs-intentservice

http://www.codeceo.com/article/android-ontrimmemory-mem.html

转自:https://blog.csdn.net/johnny901114/article/details/54377370

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页