本文同步发布于公众号:移动开发那些事: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. CubeIDE 主题美化与颜色设置

    一.主题美化 搜索引擎里很多,这里不必多说. 二.颜色设置 2.1.关于控制台 菜单栏里:window→preference→输入"console"并回车,然后按照下图指示来: 2 ...

  2. 【Playwright + Python】系列(九)Playwright 调用 Chrome 插件,小白也能事半功倍

    哈喽,大家好,我是六哥!今天我来给大家分享一下如何使用playwight调用chrome插件,面向对象为功能测试及零基础小白,我尽量用大白话的方式举例讲解,力求所有人都能看懂,建议大家先收藏,以免后面 ...

  3. MoD:轻量化、高效、强大的新型卷积结构 | ACCV'24

    来源:晓飞的算法工程笔记 公众号,转载请注明出处 论文: CNN Mixture-of-Depths 论文地址:https://arxiv.org/abs/2409.17016 创新点 提出新的卷积轻 ...

  4. vue中去掉地址栏中的#

    mode设置成history就可以了

  5. Linux之JSON处理工具jq

    一个灵活的轻量级命令行JSON处理器 补充说明 jq 是 stedolan 开发的一个轻量级的和灵活的命令行JSON处理器,源码请参考 jq 项目主页 jq 用于处理JSON输入,将给定过滤器应用于其 ...

  6. 浏览器实时查看日志系统-log.io

    log.io 是一个实时日志监控工具,采用 node.js + socket.io 开发,使用浏览器访问,每秒可以处理超过5000条日志变动消息.有一点要指出来的是 log.io 只监视日志变动并不存 ...

  7. git之常见问题

    1. You are in the middle of a merge -- cannot amend 场景:上一次提交,本次提交与上次修改点是同一个, 覆盖是的提交,产生的错误 解决方案: git ...

  8. PythonDay8Advance

    PythonDay8Advance 正则表达式 本身也是一个字符串,其中的字符具有特殊含义,将来我们可以根据这个字符串[正则表达式]去处理其他的字符串,比如可以对其他字符串进行匹配,切分,查找,替换等 ...

  9. 生成条形码二维码DataMatrix条码.EAN码.39码.交叉25码.UPC码.128码.93码.ISBN码.Codabar等

    1.引用Spire.Barcode 在Nuget包中安装Spire.Barcode 2.生成条形码 //创建 BarcodeSettings对象 BarcodeSettings settings = ...

  10. Windows更改远程桌面端口

    为了远程安全,默认在3389改为别的端口. 本示例为3389改为53389 1.步骤:打开"开始→运行",输入"regedit",打开注册表,进入以下路径: [ ...