Android性能优化大法——内存优化

2023-11-10

作者:layz4android

内存,是Android应用的生命线,一旦在内存上出现问题,轻者内存泄漏,重者直接crash,因此一个应用保持健壮,内存这块的工作是持久战,而且从写代码这块就需要注意合理性,所以想要了解内存优化如何去做,要先从基础知识开始。

1 JVM内存原理

这一部分确实很枯燥,但是对于我们理解内存模型非常重要,这一块也是面试的常客

从上图中,我将JVM的内存模块分成了左右两大部分,左边属于共享区域(方法区、堆区),所有的线程都能够访问,但也会带来同步问题,这里就不细说了;右边属于私有区域,每个线程都有自己独立的区域。

1.1 方法执行流程

class MainActivity : AppCompatActivity() {

 override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
 setContentView(R.layout.activity_main)
 execute()
 }

 private fun execute(){

 val a = 2.5f
 val b = 2.5f
 val c = a + b

 val method = Method()

 val d = getD()
}

 private fun getD(): Int {
 return 0
 }

}

class Method{
 private var a:Int = 0
}

我们看到在MainActivity的onCreate方法中,执行了execute方法,因为当前是UI线程,每个线程都有一个Java虚拟机栈,从上图中可以看到,那么每执行一个方法,在Java虚拟机栈中都对应一个栈帧。

每次调用一个方法,都代表一个栈帧入栈,当onCreate方法执行完成之后,会执行execute方法,那么我们看下execute方法。

execute方法在Java虚拟机栈中代表一个栈帧,栈帧是由四部分组成:

(1)局部变量表:局部变量是声明在方法体内的,例如a,b,c,在方法执行完成之后,也会被回收; (2)操作数栈:在任意方法中,涉及到变量之间运算等操作都是在操作数栈中进行;例如execute方法中:

val a = 2.5f

当执行这句代码时,首先会将 2.5f压入操作数栈,然后给a赋值,依次类推
(3)返回地址:例如在execute调用了getD方法,那么这个方法在执行到到return的时候就结束了,当一个方法结束之后,就要返回到该方法的被调用处,那么该方法就携带一个返回地址,告诉JVM给谁赋值,然后通过操作数栈给d赋值
(4)动态链接:在execute方法中,实例化了Method类,在这里,首先会给Method中的一些静态变量或者方法进行内存分配,这个过程可以理解为动态链接。

1.2 从单例模式了解对象生命周期

单例模式,可能是众多设计模式中,我们使用最频繁的一个,但是单例真是就这么简单吗,使用不慎就会造成内存泄漏!

interface IObserver {

    fun send(msg:String)

}

class Observable : IObserver {

    private val observers: MutableList<IObserver> by lazy {
        mutableListOf()
    }

    fun register(observer: IObserver) {
        observers.add(observer)
    }

    fun unregister(observer: IObserver) {
        observers.remove(observer)
    }

    override fun send(msg: String) {
        observers.forEach {
            it.send(msg)
        }
    }

    companion object {
        val instance: Observable by lazy {
            Observable()
        }
    }
}

这里是写了一个观察者,这个被观察者是一个单例,instance是存放在方法区中,而创建的Observable对象则是存在堆区,看下图

因为方法区属于常驻内存,那么其中的instance引用会一直跟堆区的Observable连接,导致这个单例对象会存在很长的时间

btnRegister.setOnClickListener {
    Observable.instance.register(this)
}
btnSend.setOnClickListener {
    Observable.instance.send("发送消息")
}

在MainActivity中,点击注册按钮,注意这里传入的值,是当前Activity,那么这个时候退出,会发生什么?我们先从profile工具里看一下,退出之后,有2个内存泄漏的地方,如果使用的leakcannary(后面会介绍)就应该会明白

那么在MainActivity中,哪个地方发生的了内存泄漏呢?我们紧跟一下看看GcRoot的引用,发现有这样一条引用链,MainActivity在一个list数组中,而且这个数组是Observable中的observers,而且是被instance持有,前面我们说到,instance的生命周期很长,所以当Activity准备被销毁时,发现被instance持有导致回收失败,发生了内存泄漏。

那么这种情况,我们该怎么处理呢?一般来说,有注册就有解注册,所以我们在封装的时候一定要注意单例中传入的参数

override fun onDestroy() {
    super.onDestroy()
    Observable.instance.unregister(this)
}

再次运行我们发现,已经不存在内存泄漏了

1.3 GcRoot

前面我们提到了,因为instance是Gcroot,导致其引用了observers,observers引用了MainActivity,MainActivity退出的时候没有被回收,那么什么样的对象能被看做是GcRoot呢?

(1)静态变量、常量:例如instance,其内存是在方法区的,在方法区一般存储的都是静态的常量或者变量,其生命周期非常长;
(2)局部变量表:在Java虚拟机栈的栈帧中,存在局部变量表,为什么局部变量表能作为gcroot,原因很简单,我们看下面这个方法

private fun execute() {

    val a = 2.5f
    val method = Method()
    val d = getD()
}

a变量就是一个局部变量表中的成员,我们想一下,如果a不是gcroot,那么垃圾回收时就有可能被回收,那么这个方法还有什么意义呢?所以当这个方法执行完成之后,gcroot被回收,其引用也会被回收。

2 OOM

在之前我们简单介绍了内存泄漏的场景,那么内存泄漏一旦发生,就会导致OOM吗?其实并不是,内存泄漏一开始并不会导致OOM,而是逐渐累计的,当内存空间不足时,会造成卡顿、耗电等不良体验,最终就会导致OOM,app崩溃

那么什么情况下会导致OOM呢?
(1)Java堆内存不足
(2)没有连续的内存空间
(3)线程数超出限制

其实以上3种状况,前两种都有可能是内存泄漏导致的,所以如何避免内存泄漏,是我们内存优化的重点

2.1 leakcanary使用

首先在module中引入leakcanary的依赖,关于leakcanary的原理,之后会单独写一篇博客介绍,这里我们的主要工作是分析内存泄漏

debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.7'

配置依赖之后,重新运行项目,会看到一个leaks app,这个app就是用来监控内存泄漏的工具

那我们执行之前的应用,打开leaks看一下gcroot的引用,是不是跟我们在as的profiler中看到的是一样的

如果使用过leakcanary的伙伴们应该知道,leakcanary会生成一个hprof文件,那么通过MAT工具,可以分析这个hprof文件,查找内存泄漏的位置,下面的链接能够下载MAT工具 www.eclipse.org/mat/downloa…

2.2 内存泄漏的场景

1. 资源性的对象没有关闭

例如,我们在做一个相机模块,通过camera拿到了一帧图片,通常我们会将其转换为bitmap,在使用完成之后,如果没有将其回收,那么就会造成内存泄漏,具体使用完该怎么办呢?

if(bitmap != null){
    bitmap?.recycle()
    bitmap = null
}

调用bitmap的recycle方法,然后将bitmap置为null

2. 注册的对象没有注销

这种场景其实我们已经很常见了,在之前也提到过,就是注册跟反注册要成对出现,例如我们在注册广播接收器的时候,一定要记得,在Activity销毁的时候去解注册,具体使用方式就不做过多的赘述。

3. 类的静态变量持有大数据量对象

因为我们知道,类的静态变量是存储在方法区的,方法区空间有限而且生命周期长,如果持有大数据量对象,那么很难被gc回收,如果再次向方法区分配内存,会导致没有足够的空间分配,从而导致OOM

4. 单例造成的内存泄漏

这个我们在前面已经有一个详细的介绍,因为我们在使用单例的时候,经常会传入context或者activity对象,因为有上下文的存在,导致单例持有不能被销毁;

因此在传入context的时候,可以传入Application的context,那么单例就不会持有activity的上下文可以正常被回收;

如果不能传入Application的context,那么可以通过弱引用包装context,使用的时候从弱引用中取出,但这样会存在风险,因为弱引用可能随时被系统回收,如果在某个时刻必须要使用context,可能会带来额外的问题,因此根据不同的场景谨慎使用。

object ToastUtils {

    private var context:Context? = null

    fun setText(context: Context) {
        this.context = context
        Toast.makeText(context, "1111", Toast.LENGTH_SHORT).show()
    }

}

我们看下上面的代码,ToastUtils是一个单例,我们在外边写了一个context:Context? 的引用,这种写法是非常危险的,因为ToastUtils会持有context的引用导致内存泄漏

object ToastUtils {

    private var context:Context? = null

    fun setText(context: Context) {
        this.context = context
        Toast.makeText(context, "1111", Toast.LENGTH_SHORT).show()
    }

}

5. 非静态内部类的静态实例

我们先了解下什么是静态内部类和非静态内部类,首先只有内部类才能设置为静态类,例如

class MainActivity : AppCompatActivity() {

    private var a = 10

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main2)
    }

    inner class InnerClass {
        fun setA(code: Int) {
            a = code
        }
    }
}

InnerClass是一个非静态内部类,那么在MainActivity声明了一个变量a,其实InnerClass是能够拿到这个变量,也就是说,非静态内部类其实是对外部类有一个隐式持有,那么它的静态实例对象是存储在方法区,而且该对象持有MainActivity的引用,导致退出时无法被释放。

解决方式就是:将InnerClass设置为静态类

class InnerClass {

    fun setA(code: Int) {
        a = code //这里就无法使用外部类的对象或者方法
    }
}

大家如果对于kotlin不熟悉的话,就简单介绍一下,inner class在java中就是非静态的内部类;而直接用class修饰,那么就相当于Java中的 public static 静态内部类。

6. Handler

这个可就是老生常谈了,如果使用过Handler的话都知道,它非常容易产生内存泄漏,具体的原理就不说了,感觉现在用Handler真的越来越少了

其实说了这么多,真正在写代码的时候,不能真正的避免,接下来我就使用leakcanary来检测某个项目中存在的内存泄漏问题,并解决

3 从实际项目出发,根除内存泄漏

1. 单例引发的内存泄漏

我们从gcroot中可以看到,在TeachAidsCaptureImpl中传入了LifeCycleOwner,LifeCycleOwner大家应该熟悉,能够监听Activity或者Fragment的生命周期,然后CaptureModeManager是一个单例,传入的mode就是TeachAidsCaptureImpl,这样就会导致一个问题,单例的生命周期很长,Fragment被销毁的时候因为TeachAidsCaptureImpl持有了Fragment的引用,导致无法销毁

fun clear() {
    if (mode != null) {
        mode = null
    }
}

所以,在Activity或者Fragment销毁前,将model置为空,那么内存泄漏就会解决了,直到看到这个界面,那么我们的应用就是安全的了

2.使用Toast引发的内存泄漏

在我们使用Toast的时候,需要传入一个上下文,我们通常会传入Activity,那么这个上下文给谁用的呢,在Toast中也有View,如果我们自定过Toast应该知道,那么如果Toast中的View持有了Activity的引用,那么就会导致内存泄漏

Toast.makeText(this,"Toast内存泄漏",Toast.LENGTH_SHORT).show()

那么怎样避免呢?传入Application的上下文,就不会导致Activity不被回收。

为了帮助到大家更好的全面清晰的掌握好性能优化,准备了相关的学习路线以及核心笔记(还该底层逻辑):https://qr18.cn/FVlo89 大家可以进行参考学习:

性能优化核心笔记:https://qr18.cn/FVlo89

启动优化

内存优化

UI优化

网络优化

Bitmap优化与图片压缩优化https://qr18.cn/FVlo89

多线程并发优化与数据传输效率优化

体积包优化

《Android 性能监控框架》:https://qr18.cn/FVlo89

《Android Framework学习手册》:https://qr18.cn/AQpN4J

  1. 开机Init 进程
  2. 开机启动 Zygote 进程
  3. 开机启动 SystemServer 进程
  4. Binder 驱动
  5. AMS 的启动过程
  6. PMS 的启动过程
  7. Launcher 的启动过程
  8. Android 四大组件
  9. Android 系统服务 - Input 事件的分发过程
  10. Android 底层渲染 - 屏幕刷新机制源码分析
  11. Android 源码分析实战

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Android性能优化大法——内存优化 的相关文章

  • JAXB 和 complexType 与其元素之一共享名称会生成不正确的代码

    我有这个 xsd 它有点糟糕 但我必须使用它来避免更改我正在编写的 servlet 的接口 请求 响应接口的 xsd 包含以下行
  • 从命令行将 clojure 源代码编译为类(AOT)(不使用 lein)

    我正在尝试将 clojure 源代码编译成类文件 并仅使用命令行运行它 没有 lein 也没有 可能 回复 我有 core cljsrc hello目录 src hello core clj 这是源代码 ns hello core defn
  • 对早期设备使用 Roboto 字体

    我想在我的 Android 应用程序中使用 Roboto 字体 并确保它适用于未安装该字体的早期版本的 Android 我知道我可以通过使用 Typeface createFromAsset 然后手动设置每个 TextViews Butto
  • Android 日期/时间显示 0 而不是 12

    我想知道下面的代码有什么问题 Assign hour set in the picker c set Calendar HOUR selectedHour c set Calendar MINUTE selectedMinute For a
  • 如何提高加速度计和指南针传感器的精度?

    我正在创建一个增强现实应用程序 当手机面向兴趣点 GPS 位置存储在手机上 时 它可以简单地可视化文本视图 文本视图绘制在屏幕中的兴趣点位置上 它工作正常 问题是指南针和加速度计非常 变体 并且文本视图由于传感器的准确性而不断左右上下移动
  • 无法向 openfire 服务器发送消息

    我无法使用 SMACK API 向 openfire 服务器上的 XMPP 客户端发送消息 我不确定我哪里出错了 我在 gtalk 上测试了相同的代码 它工作正常 public class SenderTest public static
  • 安装我的应用程序时的 android 意图

    安装我的应用程序时我需要执行一项操作 我研究过使用 Intent PACKAGE ADDED 但我没有收到正在安装的应用程序中的意图 我想在第一次安装我的应用程序时运行代码 该用例是注册在线服务 我可以列出 BOOT COMPLETED 如
  • 从 MySql 迁移:MariaDB 服务器意外关闭客户端连接

    由于许可 商业使用原因 我们正在从 MySql 迁移到 MariaDB 我们已经成功用 MariaDB 客户端 jar 替换了 MySql 连接器 jar 第一次更改 现在正在尝试用 MariaDB 服务器替换 MySql 服务器而不更改数
  • 字符串包含相同的字符但仍然不同[重复]

    这个问题在这里已经有答案了 我正在尝试读取一个 txt 文件并使用每个句子作为团队的名称 同时使用该名称查找另一个 txt 文件以获取其内容 所有 txt 文件都位于我的资产文件夹的根目录中 第一个 txt 文件工作正常 我使用assetm
  • 创建适配器映像时无法应用对象中的 object()

    我正在创建适配器映像 但遇到以下 2 个错误 这是代码 public class GridViewAdapter private Context mcontext private int layoutResourceId public Gr
  • android:如何将图像添加到相册

    任何人都可以分享代码 或向我指出 Android 示例代码 来帮助我将图像添加到媒体商店 图库 中的相册中 在我的应用程序中 我从服务器下载图像 并使用相机 通过 Intent 拍摄新图像 我想将这些图像组织在特定于应用程序的相册中 类似于
  • Intent.ACTION_DIAL 号码以 # 结尾

    所以我尝试通过以下方式发送号码Intent ACTION DIAL以 结尾 例如 123 但是当Android Dialer应用程序启动时 只有 123 不见了 我正在使用以下代码来触发 Android 的拨号应用程序 Uri number
  • 强制预先加载原本延迟加载的属性

    我有一个 Hibernate 对象 它的属性都是惰性加载的 大多数这些属性是其他 Hibernate 对象或 PersistentSet 现在我想强制 Hibernate 一次性加载这些属性 当然 我可以 触摸 这些属性中的每一个objec
  • 当活动从最近的活动中删除时,优雅地清理绑定服务

    我有一个绑定服务 需要时会转到前台 这是我所拥有的简化版本 class MyService extends Service private static final ServiceConnection serviceConnection n
  • “该选择不能在任何服务器上运行”

    我一直在 Eclipse 中开发一个动态 Web 项目 我收到这个错误 该选择不能在任何服务器上运行 早些时候它工作得很好 但现在我收到了这个错误 我删除了服务器并再次添加 Project gt Right Click gt Propert
  • 从多个通知启动活动会覆盖之前的意图

    public static void showNotification Context ctx int value1 String title String message int value2 NotificationManager no
  • Android 上页面留在后台时会触发“beforeunload”事件

    我正在尝试制作一个在导航时弹出的简单加载微调器 它在导航离开时使用 beforeunload 事件显示 并在完成后使用 load 事件再次隐藏自身 问题是 当我将页面留在手机后台时 例如几个小时后 beforeunload 事件触发并显示微
  • 三角形未在 OSX 上的 OpenGL 2.1 中绘制

    我正在学习有关使用 OpenGL 在 Java 中创建游戏引擎的教程 我正在尝试在屏幕上渲染一个三角形 一切运行良好 我可以更改背景颜色 但三角形不会显示 我还尝试运行作为教程系列的一部分提供的代码 但它仍然不起作用 教程链接 http b
  • Java 中的引用变量里面有什么?

    我们知道对象引用变量保存表示访问对象的方式的位 它不保存对象本身 但保存诸如指针或地址之类的东西 我正在阅读 Head First Java 第 2 版 一书 书中写道 第 3 章第 54 页 在 Java 中我们并不真正知道什么是 在引用
  • Flury 分析可以提供整数信息的平均值吗?

    我需要将 Flurry 与 Android 集成 并想知道用户将在主屏幕上停留多长时间 以分钟为单位 使用 Flurry 可以得到这样的分析吗 当我检查 Flurry 时 它为我提供了特定屏幕上点击计数的统计数据 我想知道的是用户在主屏幕上

随机推荐

  • 【Python爬虫】将爬下来的数据保存到redis数据库里面

    redis库中的Redis类对Hash数据类型操作的常用方法 方法名 具体说明 hset name key value 哈希中添加一个键值对 hmset name mapping 设置哈希中的多个键值对 hmget name keys ar
  • 逻辑架构和物理架构

    逻辑架构和物理架构 理论上划分了5种架构视图 分别是 逻辑架构 开发架构 运行架构 物理架构 数据架构 逻辑架构 逻辑架构关注的是功能 包含用户直接可见的功能 还有系统中隐含的功能 或者更加通俗来描述 逻辑架构更偏向我们日常所理解的 分层
  • HTML学习(二)HTML基础

    以这个为例 h1 我的第一个标题 h1 p 我的第一个段落 p DOCTYPE 前用来申明这是一个html 这里的html不区分大小写 HTML标题 HTML 标题 Heading 是通过 h1 h6 标签来定义的 h1 1级标题 h1 H
  • R语言优雅地修改列名称

    R语言优雅地修改列名称 在R语言中 修改数据框 DataFrame 或矩阵 Matrix 的列名称是一项常见的任务 通过优雅地修改列名称 可以提高代码的可读性和可维护性 在本文中 我将介绍几种优雅的方法来修改列名称 并提供相应的源代码示例
  • GPU计算

    文章目录 GPU计算 1 GPU和CPU的区别 2 GPU的主要参数解读 3 如何在pytorch中使用GPU 4 市面上主流GPU的选择 GPU计算 1 GPU和CPU的区别 设计目标不同 CPU基于低延时 GPU基于高吞吐 CPU 处理
  • 95-34-025-Context-AbstractChannelHandlerContext

    文章目录 1 概述 2 继承体系 3 类签名 4 关键字段 5 构造方法 6 ChannelRead事件 6 1 findContextInbound 7 invokeHandler 1 概述 2 继承体系
  • tensorrt转换模型进行了哪些操作

    对于网络layer graph进行的操作 消除输出未使用的层 消除相当于无操作的操作 卷积 偏置和ReLU运算的融合 具有足够相似参数和相同源张量的运算聚合 例如 GoogleNet v5的初始模块中的1x1卷积 inception结构中同
  • DW9718AF.c

    Copyright C 2015 MediaTek Inc This program is free software you can redistribute it and or modify it under the terms of
  • ERP系统总体解决方案 附下载地址

    企业资源计划即 ERP Enterprise Resource Planning 由美国 Gartner Group 公司于1990年提出 企业资源计划是 MRP II 企业制造资源计划 下一代的制造业系统和资源计划软件 除了MRP II
  • 大数据学习框架及指南

    Hadoop生态圈 一 采集 数据从哪里来 主要包括flume等 一 存储 海量的数据怎样有效的存储 主要包括hdfs Kafka 二 计算 海量的数据怎样快速计算 主要包括MapReduce Spark storm等 三 查询 海量数据怎
  • 数据结构与算法笔记2(线性表)

    1 线性表 1 1线性表是一种逻辑关系 见绪论 1 2定义 是具有相同类型的n个元素的有限序列 其中n为表长 n 0时为空表 关键词 相同类型 一般处理的数据元素都是相同类型 比如一个人那么都是人 而不会把人与车放在一起 关键词 有限序列
  • Java泛型知识点整理

    Java泛型知识点整理 Java泛型 泛型提供了编译时类型安全检测机制 该机制允许程序员在编译时检测到非法的类型 泛型的本质是参数化类型 也就是说所操作的数据类型被指定为一个参数 比如我们要写一个排序方法 能够对整型数组 字符串数组甚至其他
  • ConcurrentHashMap为什么是线程安全的?

    1 ConcurrentHashMap的原理和结构 我们都知道Hash表的结构是数组加链表 就是一个数组中 每一个元素都是一个链表 有时候也把会形象的把数组中的每个元素称为一个 桶 在插入元素的时候 首先通过对传入的键 key 进行一个哈希
  • MapReduce的Job提交流程

    编写一个简单的WordCount程序 Mapper import org apache hadoop io LongWritable import org apache hadoop io Text import org apache ha
  • Tip of the Week #49: Argument-Dependent Lookup

    Tip of the Week 49 Argument Dependent Lookup Originally posted as totw 49 on 2013 07 14 whatever disappearing trail of i
  • 深度学习技术在自动驾驶中的应用与挑战

    导读 深度学习技术经过近几年井喷式的发展 在很多领域都得到了广泛的应用 在自动驾驶系统中 深度学习技术也起到了至关重要的作用 同时也面临着非常多的挑战 我们一直在探索 在一个安全 稳定的自动驾驶产品中 深度学习技术应该有着怎样的作用边界 又
  • Unable to load configuration的解决方法

    最近在学Struts2 5 5 因为喜欢用最新的 并且之前没有学习过的经验 就按照一个网上的博客跟着做一个小实例 里面说直接用 Struts2 5 5中自带例子的struts xml文件 结果我就用了 然后写了一个小程序就一直报 Unabl
  • Java中的对象是什么?

    Java是一种面向对象的编程语言 它将世界视为具有属性和行为的对象的集合 Java的面向对象版本非常简单 它是该语言几乎所有内容的基础 因为它对Java非常重要 所以我将对幕后内容进行一些解释 以帮助任何不熟悉Java的人 遗产 通常 所有
  • 奇安信笔试编程题完整解析附代码

    昨天晚上奇安信笔试 两道编程题做的都不好 有紧张的元素 也有自己实力不够硬的问题 总之把两道编程题又做了一遍 思路屡清楚 下次继续努力 其实两道题非常非常简单 如果放在高中数学 基本就是送分题了 但是最近疫情期间 在家都躺退化了 算了 开搞
  • Android性能优化大法——内存优化

    作者 layz4android 内存 是Android应用的生命线 一旦在内存上出现问题 轻者内存泄漏 重者直接crash 因此一个应用保持健壮 内存这块的工作是持久战 而且从写代码这块就需要注意合理性 所以想要了解内存优化如何去做 要先从