本文同步发布于公众号:移动开发那些事: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. 用ffmpeg压缩视频、提高声音、加水印 命令记录

    ffmpeg -i in.mp4 -i LOGOW.png -filter_complex overlay=W-w:56 -c:v libx264 -x264-params "crf=35& ...

  2. WPF学习-布局

    1.  Grid布局 ,(Table 布局) 两行两列布局, Border  0 行 0 列默认开始 <Window x:Class="WpfApp.MainWindow" ...

  3. 如果让你处理hbase 怎么保证数据的安全性可靠性 不需要具体的设置 要一套方案

    有关数据安全及可靠我们认为大体上分为存储安全和使用安全 1 数据存储安全 hbase是基于hdfs的一种数据存储解决方案,所以有关数据的安全性可靠性可以利用hdfs自身的副本机制保障.另外原生的hba ...

  4. mysql8创建用户

    create user test_user@'%' identified by 'test2022@'; grant all privileges on test.* to test_user@'%' ...

  5. 在哪里可以找到官方的mysql容器图像?

    如果您在容器上部署MySQL,那么首要任务之一就是找到正确的镜像. 有一定程度的混乱,尤其是当我们试图帮助部署有问题的人时. 例如,当人们说我使用的是官方的docker镜像- 这到底意味着什么?Doc ...

  6. Java GC 调试手记

    摘要 本文记录GC调试的一次实验过程和结果. GC知识要点回顾 问题1:为什么要调试GC参数?在32核处理器的系统上,10%的GC时间导致75%的吞吐量损失.所以在大型系统上,调试GC是以小博大的不错 ...

  7. Python:pygame游戏编程之旅四(游戏界面文字处理)

    本节讲解游戏界面中字体的处理,以在界面中实时显示当前时间.小球位置为例进行实验,具体见代码. 一.代码 # -*- coding:utf-8 -*- import os import sys impo ...

  8. 抓包工具之Charles(mac)

    下载地址:https://www.charlesproxy.com/download/ 因为软件是收费的,所以破解方式可以参考:https://www.zzzmode.com/mytools/char ...

  9. Numpy本征值求解

    技术背景 Numpy是一个Python库中最经常被用于执行计算任务的一个包,得益于其相比默认列表的高性能表现,以及易用性和可靠性,深受广大Python开发者的喜爱.这里介绍的是使用Numpy计算矩阵本 ...

  10. Nuxt.js 应用中的 request 事件钩子

    title: Nuxt.js 应用中的 request 事件钩子 date: 2024/12/4 updated: 2024/12/4 author: cmdragon excerpt: 在构建现代 ...