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

1 背景

团队内目前使用Flutter来开发移动端的应用,不可避免会涉及到一些原生代码的编写,而团队内有好些iOS出身的同学在写Android应用时,由于不熟悉Android的内存管理,写的代码一眼看上去没啥问题,但细看就会发现有很多的内存问题。故在优化Android的稳定性的过程中,整理了一下Android的内存使用指南。

2 概述

在稳定性优化过程中,可以简单把Android内存问题,可以分为以下几类:

  • 内存泄露,导致应用内存无法释放;
  • 图片缓存过多,导致内存占用过高;
  • 数据结构使用不当,导致占用内存过高;
  • 线程过多,导致虚拟内存耗尽;

接下来会按照内存问题的分类来分别讲述如何写于高质量的内存代码;

3 内存泄露

3.1 问题

Android中内存泄露主要发生在

  • 静态变量或单例里引用了Activity,FragmentView的引用;
  • 延迟任务(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 图片加载框架

GlideAndroid中常用的用于加载图片的框架,它可以很方便的帮我们加载图片,一个典型的使用为:

// 从网络上加载一张图片并显示到imageView
Glide.with(context).load(url).into(imageView);

这里有一个点需要特别注意的是context的取值,如果这个图片不是全局都需要使用的,只要某个场景下才需要显示的话,这个context一个要用当前Activity的,不能用Application的,因为Glide的图片生命周期与这个context相关的,如果contextActitity的,则Glide会根据Activity的生命周期来在合适时机决定图片的加载,显示,避免内存问题。

在实际使用过程中,会出现在某些回调(异步)里去加载图片的逻辑,这有可能会出现context已经 destroy了,还在调用加载图片的方法,此时应用会crash;因此如果确实需要在回调里加载图片的话,需要保证当前context有效的情况(非destroy,非finish)下,再去加载图片;

4.2 图片资源优化

  • 对放进应用内的本地资源,可进行压缩后,再放到工程内,压缩工具可参考tinify
  • 只加载控件大小的图片:
    • 只下载对应大小的图片(需要对应的图片后台支持裁剪对应尺寸的图片资源)
    • 利用BitmapFactory.Options的参数, 本地只解码对应大小的bitmap;
  • 选择合适的图片格式,如webp
  • 合理设置图片缓存,尽可能减小网络请求时间,节省CPU和GPU的负担(每个应用需要根据自身的特点合理选择图片缓存策略)

5 数据结构优化

5.1 慎用枚举

枚举最大的优点是类型安全,但在Android平台上,枚举的内存开销是直接定义常量的三倍以上。每一个枚举值都是一个单例对象,在使用它时会增加额外的内存消耗,所以枚举相比与IntegerString 会占用更多的内存。

可以使用注解来替换:

// 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,避免了自动装箱,内存利用率高,并且采用了二分查找,查找效率更高,适用于数据量不大(千级以内),频繁插入和删除的场景;
  • ArrayMapSparseArray类似,(使用两个数组存储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等方式创建的线程,要在合适的时机调用quitquitSafely方法来停止线程;

这几个措施都只是尽可能限制创建的线程数,线程不可用时及时回收释放内存,如果要进一步去做线程优化的话,则会涉及线线程栈的裁剪(线程栈的裁剪很复杂,需要根据业务的情况去做定制化的裁剪,什么业务用大的线程栈,什么业务用小的线程栈),这里笔者也还在探索中哈。

6.1 创建线程池的几种方式

  • ThreadPoolExecutor
  • Executors
    • newFixedThreadPool:创建固定线程池
    • newCachedThreadPool :创建缓存线程池
    • newSingleThreadExecutor 创建一个单线程的线程池
  • ScheduledThreadPoolExecutor 创建定时任务线程池,如周期执行

7 参考

Android稳定性(一):内存使用指南的更多相关文章

  1. 【腾讯开源】Android性能测试工具APT使用指南

    [腾讯开源]Android性能测试工具APT使用指南 2014-04-23 09:58 CSDN CODE 作者 CSDN CODE 17 7833 腾讯 apt 安卓 性能测试 开源 我们近日对腾讯 ...

  2. 系统剖析Android中的内存泄漏

    [转发]作为Android开发人员,我们或多或少都听说过内存泄漏.那么何为内存泄漏,Android中的内存泄漏又是什么样子的呢,本文将简单概括的进行一些总结. 关于内存泄露的定义,我可以理解成这样 没 ...

  3. 使用新版Android Studio检测内存泄露和性能

    内存泄露,是Android开发者最头疼的事.可能一处小小的内存泄露,都可能是毁于千里之堤的蚁穴.  怎么才能检测内存泄露呢?网上教程非常多,不过很多都是使用Eclipse检测的, 其实1.3版本以后的 ...

  4. Android DDMS检测内存泄露

    Android DDMS检测内存泄露 DDMS是Android开发包中自带工具,可以测试app性能,用于发现内存问题. 1.环境搭建 参考之前发的Android测试环境搭建相关文章,这里不再复述: 2 ...

  5. Atitit.提升稳定性-----分析内存泄漏PermGen OOM跟解决之道...java

    Atitit.提升稳定性-----分析内存泄漏PermGen OOM跟解决之道...java 1. 内存区域的划分 1 2. PermGen内存溢出深入分析 1 3. PermGen OOM原因总结 ...

  6. android 图片占用内存与什么有关

    android 图片占用内存与什么有关 原文链接:http://blog.csdn.net/zjl5211314/article/details/7041813 在开发手机应用的时候,内存是有限的,那 ...

  7. 【文章内容来自《Android 应用程序开发权威指南》(第四版)】如何设计兼容的用户界面的一些建议(有删改)

    最近一直在看的一本书是<Android 应用程序开发权威指南>(第四版),十分推荐.书中讲到了一些用户界面设计的规范,对于初学者我认为十分有必要,在这里码给大家,希望对我们都有用. 在我们 ...

  8. 《大话移动APP测试:Android与iOS应用测试指南》

    <大话移动app测试:android与ios应用测试指南> 基本信息 作者: 陈晔 出版社:清华大学出版社 ISBN:9787302368793 上架时间:2014-7-7 出版日期:20 ...

  9. 推荐——Monkey《大话 app 测试——Android、iOS 应用测试指南》

    <大话移动——Android与iOS应用测试指南> 京东可以预购啦!http://item.jd.com/11495028.html 当当网:http://product.dangdang ...

  10. Android -- 系统信息(内存、cpu、sd卡、电量、版本)获取

    内存(ram)                                                                              android的总内存大小信息 ...

随机推荐

  1. CSS动画(波光粼粼登录页面)

    1.整体效果 https://mmbiz.qpic.cn/sz_mmbiz_gif/EGZdlrTDJa4AbemkU3vLRIDzTIgPHSjicia97wfvMVAhqZL4lsGbQQCbsV ...

  2. att&ack框架学习笔记5

    深度解读ATT&CK框架前言:在上一篇文章中,我们简单介绍了这个由美国研究机构MITRE于2014年推出的新型攻击框架ATT&CK的相关概念.ATT&CK是将已知攻击者的行为汇 ...

  3. 题解:P6672 [清华集训2016] 你的生命已如风中残烛

    题解:P6672 [清华集训2016] 你的生命已如风中残烛 标签 组合数学 分析 首先引入一个引理. Raney 引理 对于一个长度为 \(n\) 的序列 \(a\),如果 \(\sum\limit ...

  4. Docker配置Trojan代理

    1.遇到的问题 在做云计算作业,使用阿里云的ECS服务器尝试使用docker拉取镜像的时候,发现一直无法从仓库拉取,更换了多个镜像源也没有解决问题,于是决定学会去配置linux的代理,记录过程. 2. ...

  5. VUE3刷新页面报错:Uncaught SyntaxError: Unexpected token ‘<‘

    今天用vue3配置嵌套路由时,发现刷新页面后,页面变为空白,打开控制台发现报错: Uncaught SyntaxError: Unexpected token '<' 解决方法: 修改vue.c ...

  6. Django Admin之常用功能汇总

    1.字段支持下拉搜索框 1)在admin中新增字段autocomplete_fields autocomplete_fields = ("field1","field2& ...

  7. django性能分析工具之silk

    在开发过程中,有很多时候需要了解程序性能瓶颈,比如程序执行时间.网络耗时.数据库连接时间等:那接下来,django silk就派上用场了! 安装 https://github.com/jazzband ...

  8. PowerShell 重命名文件夹及删除空文件夹

    数据来源 $urldata 中的倒数第2列(子文件夹名称列)包含 /.\ 等特殊字符 某个文件夹重命名脚本 foreach ($i in 0..100) { # 提取路径部分 $basePath = ...

  9. AspNetCore全局异常处理

    在开发ASP.NET Core应用程序时,全局异常处理是一个重要的概念.它允许我们集中处理应用程序中未捕获的异常,确保应用程序的稳定性和用户体验. 1. 为什么需要全局异常处理 全局异常处理的目的是为 ...

  10. Java和.Net互相使用RSA加密时的问题和处理方法

    前言 我们产品是使用JAVA语言开发的,有个供第三方获取Token的接口,过程大概就是第三方先调一个注册接口,获取一个RSA公钥,然后用通过公钥加密后的一些认证信息调用获取Token的接口,如果信息无 ...