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

news2024/9/27 12:15:16

作者: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 源码分析实战

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/637749.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

python数据分析案例

对订单的数据类型进行封装 # data_define.py # 对数据进行封装 ORM class Record:def __init__(self, date, order_id, money, province):self.date dateself.order_id order_idself.money moneyself.province province# 魔术方法&#xff0c;print Record对象时&#xff…

LC-1483. 树节点的第 K 个祖先(树上倍增算法)

1483. 树节点的第 K 个祖先 难度困难134 给你一棵树&#xff0c;树上有 n 个节点&#xff0c;按从 0 到 n-1 编号。树以父节点数组的形式给出&#xff0c;其中 parent[i] 是节点 i 的父节点。树的根节点是编号为 0 的节点。 树节点的第 k 个祖先节点是从该节点到根节点路径上…

开发板安卓主板定制开发-基于MT6765的考核管理终端方案

Mediatek Genio系列平台以其安全、可扩展、强大且优质的解决方案&#xff0c;受到全球设备制造商的信任。这个系列的平台已经被广泛应用&#xff0c;为不同的应用提供了高效、安全、稳定的解决方案。 在Mediatek Genio系列平台中&#xff0c;Mediatek Helio P35 (MT6765)是一款…

企业提升客户体验,得先搭建在线帮助文档

在企业竞争日益激烈的市场中&#xff0c;客户体验成为企业获得成功的关键因素之一。企业需要不断提升客户体验&#xff0c;以增加客户的忠诚度和满意度。而搭建在线帮助文档是提升客户体验的一个重要手段。本文将介绍为什么企业需要搭建在线帮助文档&#xff0c;并提供一些实用…

【Java SE】一文详解next和nextLine的区别

&#x1f389;&#x1f389;&#x1f389;点进来你就是我的人了博主主页&#xff1a;&#x1f648;&#x1f648;&#x1f648;戳一戳,欢迎大佬指点! 欢迎志同道合的朋友一起加油喔&#x1f93a;&#x1f93a;&#x1f93a; 目录 引入 总结:next()和 nextLine()方法的区别 类…

DVWA-5.File upload

前提 1、在实验时&#xff0c;需要在 DVWA Security模块&#xff0c;设置需要实验的级别&#xff0c;对于不同的级别&#xff0c;php会调用不同的代码去执行用户操作。对于low level&#xff0c;系统的安全性低&#xff0c;容易受到攻击。impossible级别&#xff0c;系统的安全…

STM32——06-STM32电动车报警器

项目需求 点击遥控器 A 按键&#xff0c;系统进入警戒模式&#xff0c;一旦检测到震动&#xff08;小偷偷车&#xff09;&#xff0c;则喇叭发出声响报警&#xff0c; 吓退小偷。 点击遥控器 B 按键&#xff0c;系统退出警戒模式&#xff0c;再怎么摇晃系统都不会报警&#xff…

SD va01/02 保存时,产生销售订单号后的增强

业务对接外围系统&#xff0c;在SAP下销售订单&#xff0c;要求实时传递到外围系统。要求先要有销售订单编号 策略 在销售订单保存后找一处增强&#xff0c;并做一个日志。 日志如下&#xff1a; 对象类型&#xff0c;凭证编号序列&#xff0c;外围系统编号&#xff0c;状态…

如何快速完成TensorRT模型生成和加速

0. 简介 之前作者在《深度学习之从Python到C》介绍了一些比较传统的方法&#xff0c;主要侧重介绍了如何将pth和pytorch传统形式文件转化为onnx的文件&#xff0c;这个部分的内容&#xff0c;也可以主要看一下《PyTorch模型部署&#xff1a;pth转onnx跨框架部署详解代码》这个…

江山变压器:以数据驱动决策,CRM铸就智能制造之「变」

浙江江山变压器股份有限公司(以下简称“江变”)&#xff0c;创始于1969年&#xff0c;全国变压器行业协会理事单位、全国输配电“十三五”规划的5家起草单位之一&#xff0c;被列入国家千家名牌培育工程。 公司主要客户为国家电网、南方电网、五大发电集团&#xff0c;产品远销…

微信小程序设置 本地图片为背景图

微信小程序 通过wxss进行设置 背景图报错 经查询&#xff0c;发现微信小程序中&#xff0c;将网络图片或base64图片设置为背景图片可正常显示&#xff0c;将本地图片设置为背景图片则不能显示&#xff0c;解决方法有三种&#xff0c;个人采用的是第三种方法。 1. 本地图片转换…

私有云和公有云是什么?有什么区别?

作者&#xff1a;Insist-- 个人主页&#xff1a;insist--个人主页 作者会持续更新网络知识和python基础知识&#xff0c;期待你的关注 目录 一、私有云和公有云是什么&#xff1f; 1、私有云是什么&#xff1f; 2、公有云是什么&#xff1f; 二、举个例子 1、私有云 2、公…

内网渗透—Linux上线

内网渗透—Linux上线 1. 前言2. 下载插件3. CS配置3.1. 客户端配置3.1.1. 导入插件文件3.1.2. 配置监听 3.2. 服务端配置3.2.1. 导入配置文件 3.3. 生成木马3.3.1. 修改cna文件3.3.2. 修改后效果 3.4. 执行木马 1. 前言 默认情况下CS是不支持上线Linux的&#xff0c;只支持上线…

媒介易教你海外品牌推广:如何选择适合的新闻通稿发布平台?

在进行海外品牌推广时&#xff0c;选择合适的海外新闻通稿发布第三方平台是提高品牌曝光度和影响力的重要一环。这些平台可以帮助企业将新闻内容传播到全球范围内的媒体和受众&#xff0c;为品牌推广提供更广阔的机会。然而&#xff0c;选择合适的发布平台并不容易&#xff0c;…

乐盒开源盲盒uniapp源码系统

源码我也没测试过小白就不要尝试了 下载下来&#xff0c;之后&#xff0c;通过 node 安装如下依赖&#xff0c;即可查看效果&#xff1a; npm install uni-simple-router # 或者&#xff1a;yarn add uni-simple-router npm install uni-simple-router npm install uni-read…

图解数据结构--栈的实现-C语言版本--源码

目录-总 -分- 总结构 图片可视化 总源码1.头文件介绍---分2.节点的实现3.栈顶栈底4.函数的提前声明5. 栈 ---初始化栈6. 栈 ---进栈7.栈 --- 遍历8.栈 --- 是否为空9.栈 --- 出栈10总结 图片可视化 总 源码 /*time 2023年6月12日12:39:06auther yzmcntent stract 栈 */#inclu…

行业报告 | 企业AIGC商业落地应用研究报告

原创 | 文 BFT机器人 01 AIGC&#xff08;生成式人工智能&#xff09;定义 02 洞观&#xff1a;AIGC市场全貌与供应商的摩拳擦掌 生成式人工智能技术的落地已经远远超出了商业化的进程 在企业现有数字化作业体系中切入&#xff0c;成为AIGC率先落地的存量场景 和SaaS同根的生成…

破坏双亲委派机制(自定义类加载器)

jvm中讲过&#xff0c;一个类的创建是要经历类加载器的&#xff0c;那么我们来讲讲如何自定义类加载器。 jvm优先级最高的就是自定义类加载器&#xff0c;为什么这么说呢&#xff1f;我们来看看类加载器的源码&#xff1a; 首先我们解释一下这个方法做了什么&#xff0c;要求返…

网络套接字编程

之前我们粗浅的认识了一下网络的一些知识&#xff0c;比如OSI七层模型&#xff0c;TCP/IP四层模型&#xff0c;那么我们具体怎么实现两台主机的交互呢&#xff1f; 在学习这些之前&#xff0c;我们需要准备一些预备知识。 目录 预备知识 1:认识源IP地址和目的IP地址 2&…

matlab字符串的操作方法

一个字符串是存储在一个行向量中的文本&#xff0c;这个行向量中的每一个元素代表一个字符&#xff0c;字符串可以由0个或多个字符组成。下面是一些字符串的操作方法 &#xff08;1&#xff09;字符串的创建 MATLAB中创建字符串非常简单&#xff0c;将字符串中的字符放到一对…