本文同步发布于公众号:移动开发那些事: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. ansible开局配置-openEuler

    ansible干啥用的就不多介绍了,这篇文章主要在说ansible的安装.开局配置.免密登录. ansible安装 查看系统版本 cat /etc/openEuler-latest 输出内容如下: o ...

  2. 题解:CF687C The Values You Can Make

    CF687C The Values You Can Make 题解 题目翻译感觉不明不白的(至少我看了几遍没看懂),这里给个较为清晰的题面. 题目描述 给你 \(n\) 个硬币,第 \(i\) 个硬币 ...

  3. .NET 9 发布 性能提升、AI 支持与全方位改进

    前言 .NET 9 正式发布,这是迄今为止最高效.现代.安全.智能且高性能的 .NET 版本. 新版本凝聚了全球数千名开发者的共同努力,包含了数千项性能.安全性和功能性改进. 主要亮点 性能提升:全面 ...

  4. C# 串口读取并转换字符串

    public string ReadString() { ASCIIEncoding ascii = new ASCIIEncoding(); byte[] readBuffer = new byte ...

  5. 在Keil中使用ST-LINK烧录STM32程序指南

    前言 之前玩STM32都是用J-LINK烧录程序,不仅便捷,而且烧录的速度比用串口快好多. 最近我接了几个32单片机的毕设单子,便买了几块C8T6的最小系统板用来开发.最初我还是用J-LINK烧录C8 ...

  6. ubuntu安装fish

    换新电脑后需要安装fish命令行工具,发现总是apt install不成功,后来挂了代理才成功. 然后我想让这个fish的命令能自动导入我以前写的alias命令(点击这里),可是发现网上人家都说fis ...

  7. JAVA MemCache 史无前例的详细讲解!看完包精通MEMCACHE!

    Memcach什么是Memcache Memcache集群环境下缓存解决方案 Memcache是一个高性能的分布式的内存对象缓存系统,通过在内存里维护一个统一的巨大的hash表,它能够用来存储各种格式 ...

  8. 成为Java GC专家系列(3) — 如何优化Java垃圾回收机制

    本文是成为Java GC专家系列文章的第三篇.在第一篇<成为JavaGC专家Part I - 深入浅出Java垃圾回收机制>中我们学习了不同GC算法的执行过程,GC是如何工作的,什么是新生 ...

  9. 二、STM32F103C8T6-定时器

    STM32F103C8T6 定时器概述 STM32F103C8T6 作为一款广泛使用的微控制器,内置多个定时器,能够支持多种计时和控制功能,如精确延时.脉冲宽度调制(PWM).捕获比较(Capture ...

  10. php之Opcache深入理解

    PHP项目中,尤其是在高并发大流量的场景中,如何提升PHP的响应时间,是一项十分重要的工作.而Opcache又是优化PHP性能不可缺失的组件,尤其是应用了PHP框架的项目中,作用更是明显. 1. 概述 ...