1、避免创建不必要的对象。
2、尽量使用 for-each 循环,对于 ArrayList,请使用普通的 for,如:
- int len = list.size();
- for (int i = 0; i < len; ++i) {
- //todo somgthing
- }
3、使用系统库函数。
;
- System.arraycopy()
等。
- String.indexOf()
,
- jsoup
等)。如解析 HTML 可以使用系统的
- htmlparser
。
- XmlPullParser
ArrayMap 和 HashMap 的在内存的使用上,更加高效。
ArrayMap 实现上有两个数组,一个数组是保存 key hash,另一个数组保存 value,ArrayMap 通过二分法 (binary search) 来进行查找的。
HashMap 通过一个数组来实现的,key hash 作为数组的索引,这样就需要更大的内存来减少 key hash 的冲突,key hash 就是数组的索引,所以查找效率很高。
使用建议:
使用方法:
- arrayMap.keyAt(0);
- arrayMap.valueAt(0);
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 >
SparseArray 和 ArrayMap 的
和
- 使用建议
都是一样的。
- 使用方法
在 android 开发中,一些耗时的操作都会放到后台线程去执行,比如:网络、本地文件、数据库等。
- new Thread(new Runnable() {
- @Override
- public void run() {
- //do something...
- }
- }).start();
每个线程至少消耗 64k 的内存,如果你在某个时间点,迅速开启了很多线程 (比如加载列表图片,然后用户滑动列表),这个时候可能内存使用量就会飙升。
这时候 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); //执行任务
下面是官方对 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.
核心线程数,核心线程会一直存活,即使没有任务需要处理。当线程数小于核心线程数时,即使现有的线程空闲,线程池也会优先创建新线程来处理任务,而不是直接交给现有的线程处理。核心线程在 allowCoreThreadTimeout 被设置为 true 时会超时退出,默认情况下不会退出。
- corePoolSize
线程池允许最大的线程数量。
- maxPoolSize
当线程空闲时间达到 keepAliveTime,该线程会退出,直到线程数量等于 corePoolSize。如果 allowCoreThreadTimeout 设置为 true,则所有线程均会退出直到线程数量为 0。
- keepAliveTime
是否允许核心线程空闲 keepAliveTime 退出,默认值为 false。
- allowCoreThreadTimeout
任务队列。pool.execute(runnable) 提交的 task 都会放到 workQueue。
- 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();
- }
- }
- }
- }
上面的代码很简单:创建了一个 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
从测试结果来看,我们打印 pool.getCorePoolSize()+"-"+pool.getActiveCount() + "-" + pool.getMaximumPoolSize() 的值是正常的。但是只创建了 4 个线程:
- pool-1-thread-1
- pool-1-thread-2
- pool-1-thread-3
- pool-1-thread-4
我们设置了线程池的最大数为 7,我们提交了 10 个任务,但是为什么只创建了 corePoolSize=4 个线程?
查看官方文档可以找到答案:
所以上面的例子,只创建了 4 个线程,因为虽然我们提交了 10 个任务,但是构建
时候没有传入队列大小,默认大小是 Integer.MAX_VALUE,所以 workQueue 是不会满的。所以最多就创建了 4 个线程。
- workQueue
据此,我把
队列容量改成 4:
- workQueue
- pool = new ThreadPoolExecutor(4, 7, 60, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(4));
测试结果:
- 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
发现创建了 6 个线程,大于上一次的测试结果(上一次是创建了 4 个线程),可是我们设置的 maximumPoolSize 为 7,按道理应该是创建 7 个线程才对呀,这是为什么呢?
这需要了解下
队列的策略了。我们上面的列子使用的是 LinkedBlockingQueue。
- workQueue
下面来看看官方文档对 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.
主要意思就是:
这样就能解释为什么只创建 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)
或
- for (int i = 0; i < 11; i++) {
- myClass.pool.execute(new MyRunnable(myClass));
- }
效果如下:
- 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
- .....
Java 提供了 3 种策略的 Queue
Java 给我们提供了一些工厂方法来来创建线程池 ():
这些方法都是通过构建 ThreadPoolExecutor 来实现的,具体的细节可以去看看文档,如果都不满足你的需求,可以自己构造 ThreadPoolExecutor。
一般我们在 app 里的版本更新逻辑在 Service 里起一个线程来检测。
为了避免 Service 一直存在,减少内存消耗,检测版本后,还需要手动 stopSelf,略麻烦。
这时候用 IntentService 就比较合适了,默认就给你启动了一个线程来执行耗时操作,完成自动关闭 service。
Service 和 IntentService 主要区别:
多次,IntentService 会执行多次
- startService(intent)
,且必须等本次的 onHandleIntent(Intent intent) 执行完,才会执行下一次 onHandleIntent(Intent intent),说白了就是如果正在执行任务,会把后面启动的命令放到队列里。而多次调用 startService(intent),Service 仅仅会多次调用
- onHandleIntent(Intent intent)
方法。
- onStartCommand
在项目中,我们常常可能要做活动倒计时的功能,我是用 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) {
- //每间隔固定时间执行一次
- //再次处理倒计时逻辑
- }
- }
如下图所示:
因为要在 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);
- }
- }
- }
我们知道,使用 WeakReference 包装 TextView,在发生 GC 的时候,TextView 就会被回收。
需要注意的是,只有 WeakReference 所引用的对象没有被其他对象引用,当发生了 GC,WeakReference 所引用的对象才会被回收的。
例如,A 界面有个倒计时功能,然后把 TextView 传给了上面的 TimeCounter,然后在 A 界面的基础上启动了其他界面,这时候假如发生了 GC,这时候 TextView 是不会被回收的。
因为还有 A 界面对其还有引用。所以只有把 A 界面关闭了,且发生 GC 了 TextView 才会被收回。
在网上也看到一些,加上了 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);
- 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);
- }
- }
上面的
和
- Timer
都可以实现诸如每隔 1 秒执行的功能,都会导致内存泄露的问题。
- Handler
解决方案:
在项目常常要做一些耗时操作,可以起一个 Thread 来做,如:
- new Thread(new Runnable() {
- @Override
- public void run() {
- Log.e("Log", "执行耗时操作");
- }
- }).start();
如果在 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------");
- }
- }
运行起来后,接着按返回键,测试效果如下:
- 执行耗时操作------com.chiclaim.twitter.ThreadActivity@2e92d7ee
- onDestroy------
- 耗时操作执行完成------com.chiclaim.twitter.ThreadActivity@2e92d7ee
从上面的代码可以看出,耗时任务我们定为 60 秒,启动了 Activity 后,立马输出了:
- 执行耗时操作------com.chiclaim.twitter.ThreadActivity@2e92d7ee
紧接着按了返回键,输出了:
- onDestroy------
过了大概 60 秒,输出了:
- 耗时操作执行完成------com.chiclaim.twitter.ThreadActivity@2e92d7ee
从上面的例子中我们得出了 2 个结论:
解决方案:
和
- 成功
的情况,比如说网络请求,如果一个界面有多个请求,那么就会有很多内部类 (回调) 嵌套了,我的做法是使用者实现一个回调接口,该接口定义了成功和失败的两个方法,Activity onCreate 的时候,把回调注册到一个容器内,在 onDestroy 方法里从容器中移除该回调。这样也不会对 Activity 造成内存泄露。
- 失败
千万不要是有 static 来修饰 activity、View 对象,这种内存泄露更加严重,因为它将贯穿程序的生命周期。
为了更好的管理 Activity 常常把 Activity 放进容器中,如 Stack。如果忘记了移除,也会造成内存泄漏。
onTrimMemory 回调是 Android 4.0 之后提供的一个 API。
它的主要用来提示开发者在系统内存不足的时候,根据当前内存情况 (level),释放相关资源以减小系统内存压力,这样可以减少 app 进程被系统杀死的可能性。
尽可能的保存 app 进程,等到用户在下一次使用的时候,启动速度就会比较快。
在 Android 4.0 之前都是 onLowMemory 来处理这类逻辑的,onLowMemory 和 onTrimMemory 中的 TRIM_MEMORY_COMPLETE 级别相同。如果想兼容 Android4.0 之前的系统可以实现该方法,否则只需要处理 onTrimMemory 方法。
下面来聊一聊 onTrimMemory(int level) 回调 level 的常量:
表示应用程序的所有 UI 界面被隐藏了,即用户点击了 Home 键或者剩下最后一个界面,按返回键后,Application 的 onTrimMemory 回调也会调用。
- TRIM_MEMORY_UI_HIDDEN = 20
下面三个等级是当我们的应用程序正在前台运行时的回调:
表示应用程序正常运行,并且不会被杀掉。但是目前手机的内存已经有点低了,你的正在运行的进程需要释放一些不需要的内存资源。
- TRIM_MEMORY_RUNNING_MODERATE = 5
表示应用程序正常运行,并且不会被杀掉。但是目前手机的内存已经非常低了,你的正在运行的进程需要释放一些不需要的内存资源。
- TRIM_MEMORY_RUNNING_LOW = 10
表示应用程序仍然正常运行,但是系统内存已经极度低了,即将不能保留任何后台进程 了。这个时候我们应当尽可能地去释放任何不必要的资源,下一步 onLowMemory 将会被调用,这样的话,后台将不会保留任何进程。
- TRIM_MEMORY_RUNNING_CRITICAL = 15
当 app 进程不可见处于 LRU list 中,则会收到以下常量的回调:
app 进程不可见,处于 LRU 列表中,这时候是个释放资源的好时机。
- TRIM_MEMORY_BACKGROUND = 40
系统目前内存已经很低了,并且我们的程序处于 LRU 缓存列表的中间位置。腾出一些内存让系统运行其他的进程。
- TRIM_MEMORY_MODERATE = 60
系统目前内存已经很低了,并且我们的程序处于 LRU 缓存列表的最边缘位置,如果系统找不到更多可能的内存,我们的 app 进程很快将被杀死。
- TRIM_MEMORY_COMPLETE = 80
从上面可以看出,这些常量大致可以分为两类,一类是大于
,这类表示进程不可见。一类是小于
- TRIM_MEMORY_UI_HIDDEN = 20
,这类表示 app 进程正在前台运行。并且常量值越大,说明系统内存越紧张。
- TRIM_MEMORY_UI_HIDDEN = 20
下面是:
- /**
- * 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;
- }
如何针对上面的这些常量,分别释放 app 里哪些资源呢?(目前我只处理 app 在后台的情况)
当回调的参数 level=TRIM_MEMORY_BACKGROUND(40) 或 TRIM_MEMORY_MODERATE(60) 或 TRIM_MEMORY_COMPLETE(80) 时,
相关注意事项:
参考链接:
来源: