Android稳定性(一):内存使用指南
本文同步发布于公众号:移动开发那些事:Android稳定性(一):内存使用指南

1 背景
团队内目前使用Flutter来开发移动端的应用,不可避免会涉及到一些原生代码的编写,而团队内有好些iOS出身的同学在写Android应用时,由于不熟悉Android的内存管理,写的代码一眼看上去没啥问题,但细看就会发现有很多的内存问题。故在优化Android的稳定性的过程中,整理了一下Android的内存使用指南。
2 概述
在稳定性优化过程中,可以简单把Android内存问题,可以分为以下几类:
- 内存泄露,导致应用内存无法释放;
- 图片缓存过多,导致内存占用过高;
- 数据结构使用不当,导致占用内存过高;
- 线程过多,导致虚拟内存耗尽;
接下来会按照内存问题的分类来分别讲述如何写于高质量的内存代码;
3 内存泄露
3.1 问题
在Android中内存泄露主要发生在
- 静态变量或单例里引用了
Activity,Fragment,View的引用; - 延迟任务(
Handler或线程)里的任务未正常取消; - 资源未正常关闭,如文件,io流等;
在这几类场景中,导致该被回收的内存不能被jvm回收;
3.2 优化指南
3.2.1 慎用匿名内部类
匿名内部类默认会持有外部类的类的引用。如果外部类是一个Activity或者Fragment,就有可能会导致内存泄漏(在单例中要特别注意):
kotlin中接口回调中不调用的外部类,那么生成的匿名内部类不会持有外部类的引用,也就不会造成内存泄漏,反之则会;- 在
java中,不论接口回调中是否调用到外部类,生成的匿名内部类都会持有外部类的引用
匿名内部类的使用在那些回调接口中特别容易出现,如果在业务中确实需要初始化一个回调的话,建议
- 实例化匿名内部类时,使用一个成员变量去引用;
- 其他地方使用到这个匿名内部类时,里面使用弱引用去持有,这样匿名内部类的生命周期就与外部类的生命周期相一致
例:
class AEngine {
// 使用弱引用持有一个回调监听实例
private var aEngineListener: WeakReference<AEngineListener>? = null
}
class AActivity {
// 使用成员变量存储回调实例
private var aEngineListener: AEngineListener? = null
aEngineListener = object:AEngineListener {****}
内存
AEngine.setCloudGameEngineListener(aEngineListener)
}
3.2.2 避免循环引用
对于无法避免的相互引用,需要其中一方的引用要变成弱引用,另一方则通过成员变量去存储;
比如 AActivity 类里有一个变量loading, 变量loading里面又通过初始化或接口等其他方式强持有了AActivity的引用;比如
class AActivity{
// 初始化时,会实例这个对象
private var loading: ALoadingLayout? = null
// 设置一个接口的实现
.....
loading.setOnEnterBgQueueClickedListener(object:xxxx)
}
class ALoadingLayout{
// 不推荐写法
// View.OnClickListener enterBgClickListener
// 如果无法避免这种相互要引用到的关系,则 其中一方的引用要变成弱引用,另一方则通过成员变量去存储;
WeakReference<View.OnClickListener> exitIconClickListener;
}
4 图片优化
4.1 图片加载框架
Glide是Android中常用的用于加载图片的框架,它可以很方便的帮我们加载图片,一个典型的使用为:
// 从网络上加载一张图片并显示到imageView
Glide.with(context).load(url).into(imageView);
这里有一个点需要特别注意的是context的取值,如果这个图片不是全局都需要使用的,只要某个场景下才需要显示的话,这个context一个要用当前Activity的,不能用Application的,因为Glide的图片生命周期与这个context相关的,如果context是Actitity的,则Glide会根据Activity的生命周期来在合适时机决定图片的加载,显示,避免内存问题。
在实际使用过程中,会出现在某些回调(异步)里去加载图片的逻辑,这有可能会出现context已经 destroy了,还在调用加载图片的方法,此时应用会crash;因此如果确实需要在回调里加载图片的话,需要保证当前context有效的情况(非destroy,非finish)下,再去加载图片;
4.2 图片资源优化
- 对放进应用内的本地资源,可进行压缩后,再放到工程内,压缩工具可参考tinify
- 只加载控件大小的图片:
- 只下载对应大小的图片(需要对应的图片后台支持裁剪对应尺寸的图片资源)
- 利用
BitmapFactory.Options的参数, 本地只解码对应大小的bitmap;
- 选择合适的图片格式,如
webp等 - 合理设置图片缓存,尽可能减小网络请求时间,节省CPU和GPU的负担(每个应用需要根据自身的特点合理选择图片缓存策略)
5 数据结构优化
5.1 慎用枚举
枚举最大的优点是类型安全,但在Android平台上,枚举的内存开销是直接定义常量的三倍以上。每一个枚举值都是一个单例对象,在使用它时会增加额外的内存消耗,所以枚举相比与Integer和 String 会占用更多的内存。
可以使用注解来替换:
// java的注解
int SUCCESS = 1;
int FAILED = 2;
@IntDef({SUCCESS,FAILED})
public @interface ResultCode {
}
// 在使用时,可通过@ResultCode 来限定参数的范围,超过这个范围,编译器会提示
private static void reportResult(@ResultCode int resulCode)
// kotlin 的注解也比较简单,使用时,与java的注解一致
@IntDef(SUCCESS, FAILED)
annotation class ResultCode
5.2 使用优化过的数据结构
Android中有针对移动端的场景,设计一些专属的数据结构,这些数据结构在一些场景下,可以优化内存使用和提高性能,如SparseArray,ArrayMap,
SparseArray,避免了自动装箱,内存利用率高,并且采用了二分查找,查找效率更高,适用于数据量不大(千级以内),频繁插入和删除的场景;ArrayMap与SparseArray类似,(使用两个数组存储key和value)适用于key为对于无法避免的相互引用,需要其中一方的引用要变成弱引用,另一方则通过成员变量去存储;对象的场景做为HashMap的替代;
其他的数据结构,如:ConcurrentHashMap ,CopyOnWriteArrayList ,SparseBooleanArray也可方便;
6 线程优化
其实线程优化本身就比较复杂,整个线程优化主要可围绕以下的一些思路来处理:
- 线程检测
- 线程统计
- 线程和线程池优化,线程数收敛;
- 线程栈裁剪
这些优化内容单拿一篇文章出来写它估计也写不完,这里就不展开来讲了,仅从一些内存角度(线程收敛)出发来讲一些可优化的方向(线程使用不当,会引起fd溢出,创建新线程失败(pthread_create (1040KB stack) failed: Out of memory))。
笔者所在的业务,主要存在几种线程问题
- 各个业务使用自身的
OkhttpClient,每个client不限制最大线程数 - 乱用线程池,出现业务频繁创建线程池(每个线程池都只有一个固定大小的线程)
- 各种
mainHandler的逻辑,需要时就重新创建一个mainHandler - 存在线程对象在已经
shutdown的情况下,其实例还是被某个单例持有; - 。。。。
针对业务存在的线程问题,从这几方面做了限制:
- 统一收敛业务内的
OkhttpClient,并对其线程数进行限制
ExecutorService service = new ThreadPoolExecutor(0, 20, 60, TimeUnit.SECONDS,
new SynchronousQueue<>(), threadFactory("okHttpName Dispatcher", false));
sClient = new OkHttpClient.Builder()
.dispatcher(new Dispatcher(service))
.build();
- 收敛业务内的线程池(包括
mainHandler),提供通用的线程池调度能力; - 对于使用
HandlerThread等方式创建的线程,要在合适的时机调用quit或quitSafely方法来停止线程;
这几个措施都只是尽可能限制创建的线程数,线程不可用时及时回收释放内存,如果要进一步去做线程优化的话,则会涉及线线程栈的裁剪(线程栈的裁剪很复杂,需要根据业务的情况去做定制化的裁剪,什么业务用大的线程栈,什么业务用小的线程栈),这里笔者也还在探索中哈。
6.1 创建线程池的几种方式
- ThreadPoolExecutor
- Executors
- newFixedThreadPool:创建固定线程池
- newCachedThreadPool :创建缓存线程池
- newSingleThreadExecutor 创建一个单线程的线程池
- ScheduledThreadPoolExecutor 创建定时任务线程池,如周期执行
7 参考
Android稳定性(一):内存使用指南的更多相关文章
- 【腾讯开源】Android性能测试工具APT使用指南
[腾讯开源]Android性能测试工具APT使用指南 2014-04-23 09:58 CSDN CODE 作者 CSDN CODE 17 7833 腾讯 apt 安卓 性能测试 开源 我们近日对腾讯 ...
- 系统剖析Android中的内存泄漏
[转发]作为Android开发人员,我们或多或少都听说过内存泄漏.那么何为内存泄漏,Android中的内存泄漏又是什么样子的呢,本文将简单概括的进行一些总结. 关于内存泄露的定义,我可以理解成这样 没 ...
- 使用新版Android Studio检测内存泄露和性能
内存泄露,是Android开发者最头疼的事.可能一处小小的内存泄露,都可能是毁于千里之堤的蚁穴. 怎么才能检测内存泄露呢?网上教程非常多,不过很多都是使用Eclipse检测的, 其实1.3版本以后的 ...
- Android DDMS检测内存泄露
Android DDMS检测内存泄露 DDMS是Android开发包中自带工具,可以测试app性能,用于发现内存问题. 1.环境搭建 参考之前发的Android测试环境搭建相关文章,这里不再复述: 2 ...
- Atitit.提升稳定性-----分析内存泄漏PermGen OOM跟解决之道...java
Atitit.提升稳定性-----分析内存泄漏PermGen OOM跟解决之道...java 1. 内存区域的划分 1 2. PermGen内存溢出深入分析 1 3. PermGen OOM原因总结 ...
- android 图片占用内存与什么有关
android 图片占用内存与什么有关 原文链接:http://blog.csdn.net/zjl5211314/article/details/7041813 在开发手机应用的时候,内存是有限的,那 ...
- 【文章内容来自《Android 应用程序开发权威指南》(第四版)】如何设计兼容的用户界面的一些建议(有删改)
最近一直在看的一本书是<Android 应用程序开发权威指南>(第四版),十分推荐.书中讲到了一些用户界面设计的规范,对于初学者我认为十分有必要,在这里码给大家,希望对我们都有用. 在我们 ...
- 《大话移动APP测试:Android与iOS应用测试指南》
<大话移动app测试:android与ios应用测试指南> 基本信息 作者: 陈晔 出版社:清华大学出版社 ISBN:9787302368793 上架时间:2014-7-7 出版日期:20 ...
- 推荐——Monkey《大话 app 测试——Android、iOS 应用测试指南》
<大话移动——Android与iOS应用测试指南> 京东可以预购啦!http://item.jd.com/11495028.html 当当网:http://product.dangdang ...
- Android -- 系统信息(内存、cpu、sd卡、电量、版本)获取
内存(ram) android的总内存大小信息 ...
随机推荐
- 轻松玩转pandas
文章目录 1.pandas简介 2.pandas应用 3.pandas安装 4.Pandas 数据结构 - Series 5.Pandas 数据结构 - DataFrame 6.Pandas CSV ...
- Python实现微博舆情分析的设计与实现
引言 随着互联网的发展,社交媒体平台如微博已经成为公众表达意见.分享信息的重要渠道.微博舆情分析旨在通过大数据技术和自然语言处理技术,对微博上的海量信息进行情感分析.热点挖掘和趋势预测,为政府.企业和 ...
- CF1487-B Cat Cycle
一个规律题目要多做多积累 , 脑子不太灵活 CF1487 Cat Cycle 题目大意: 两只猫A,B, A猫从n -> n-1 -> n-2 ... -> 1 -> 2 .. ...
- 0.1 Introduction to the tenth anniversary edition
此序作于2010年 1970s&1980s, 除了将量子系统仅仅视为一种自然界中需要解释的现象,大家开始将其视为可以设计的系统. 这种新的观点引起了物理,计算机科学和信息理论等领域交叉融合之后 ...
- 基于Java+SpringBoot心理测评心理测试系统功能实现九
一.前言介绍: 1.1 项目摘要 心理测评和心理测试系统在当代社会中扮演着越来越重要的角色.随着心理健康问题日益受到重视,心理测评和心理测试系统作为评估个体心理状态.诊断心理问题.制定心理治疗方案的工 ...
- 微软憋大招:SQL Server + Copilot = 地表最强AI数据库!
微软憋大招:SQL Server + Copilot = 地表最强AI数据库! 微软布局代码AI霸主地位 微软在人工智能领域的布局引人注目,尤其在代码生成领域,微软通过Copilot展现出了强大的竞争 ...
- Java真的没出路了吗?
Java从1991年由James Gosling和他的同事们开发, 至今已经三十多年, 我们知道,任何产品都有生命周期, 都要经历从诞生.发展.成熟.消亡四个阶段, 目前的Java已经处在成熟阶段, ...
- 617. 合并二叉树 Golang实现
题目描述: 给你两棵二叉树: root1 和 root2 . 想象一下,当你将其中一棵覆盖到另一棵之上时,两棵树上的一些节点将会重叠(而另一些不会).你需要将这两棵树合并成一棵新二叉树.合并的规则是: ...
- Java多线程设计模式(6)两阶段终止模式
一 Two-Phase Termination Pattern Two-Phase Termination Pattern,指的就是当希望结束一个线程的时候,送出一个终止请求,但是不会马上停止,做一些 ...
- Converter Tutorial
Setting up a simple example This is the most basic converter... let's start with a simple Person: pa ...