浅聊Android性能优化~

news2024/11/19 3:48:29

作者:一只修仙的猿

前言

关于性能优化,可能我们的第一反应是这是高手做的事情,一直以来我也是这样认为的。但在最近一段时间,在公司项目上做了一些框架的性能优化,让我初步掀开了性能优化的面纱,也对他有了进一步的认识。所以这篇文章结合我做的一些优化,做一些相关经验的分享。

性能优化一般情况下分为两类:时间优化与空间优化。前者是降低处理器处理时间,后者是降低内存使用量。归根到底都是降低对硬件资源的使用,来提高程序的性能,从而提高程序运行流畅性、降低功耗等。今天也主要从这两个方面来展开聊一聊。性能优化一般需要针对具体的代码、或者场景,所以后续的内容,我都会结合具体的场景来展开讲述,从0开始去讲述在项目中的思考过程,并总结一些性能优化的通用思路。因此,在讲性能优化之前,需要先向你介绍一下我们项目的大概内容。

了解项目

首先需要简单来了解一下我们工作的项目框架流程。我们项目的框架是一个图像处理框架,其功能如下图:

接收上层传下来的图像帧,进行渲染处理之后,将处理完成的图像返回给上层。

这是宏观上对于框架的描述。但由于这套框架,他是纯c++开发,且接口复杂,较难以理解,导致接入成本过高。因此,在原有的框架上,我们进行再一次封装,如下图:

c++接口主要对接native接入业务;对于android,则封装java接口,让接入方减少编写jni的成本。 需要注意的是,图中的Java接口以及c++接口,都是完整封装的框架层,具有自己的数据结构,向上屏蔽底层细节。

现在来观察一下整体的处理流程:

  1. 业务层使用相机捕获数据帧,并将数据帧传递给Java接口层
  2. Java接口层将数据帧,通过JNI调用,把数据传递到c++层
  3. JNI接口层将数据封装后传递给c++接口层
  4. 最后,c++接口层将数据封装后传递给底层SDK。
  5. 处理完成之后则反过来走完上述流程

如下图:

大概了解到这个程度就可以了。接下来详细展开我所做的几个性能优化。

优化一

由于整个框架主要是对帧数据进行处理,因此我们需要关注帧数据的处理流程。我们从相机采集开始来分析:

  1. 相机采集数据,并将帧数据存放在缓冲区中,这是最原始的数据
  2. 我们不能直接使用缓冲区中的数据,因为缓冲区是复用的,因此需要拷贝出来。这里发生一次数据拷贝,以及一帧数据内存的分配
  3. 业务方将数据封装到我们的java接口数据结构,并传递到我们的框架java接口层。伪代码如下面的代码:
// 相机数据帧回调
void onCameraFrameArrive(byte[] data,int width,int hight) {
    // 创建框架数据结构,Frame内部拷贝一次相机帧数据
    Frame frame = new Frame(data,width,hight);
    // 调用接口传递数据
    sendFrame(frame);
}
  1. java接口通过jni,将数据传递给c++层。JNI层需要对java数据进行一次拷贝,c++不能直接持有并处理java内存数据。c++持有java内存会让内存管理变得复杂,且无法直接对java内存进行数据处理。JNI层再将数据封装后,传递给c++接口层。伪代码如下
// java代码
void sendFrame(Frame frame){
    sendFrameToNative(frame);
}

private native void sendFrameToNative(Object frame);

// c++代码
void copyByteArray(jbyteArray value) {
    jsize arraySize = jniEnv->GetArrayLength(value);
    // 创建c++数组,并拷贝一份新的数据
    auto* array = new int8_t[size];
    jniEnv->GetByteArrayRegion(value, 0, arraySize, array);
}

好了,流程上先了解到这里。流程中一共发生了两次内存的分配与拷贝:

  1. 相机缓存不能直接使用,java业务层发生了一次拷贝;
  2. c++不能直接使用java内存,发生了一次从java内存到c++内存的拷贝

这里的优化思路是:我们可以让JNI直接从相机缓存中进行拷贝,而没有必要拷贝一份中间数据,这样可以减少一次内存拷贝与内存分配。我们发生拷贝的地方在于Frame类的构造函数中,优化伪代码如下:

class Frame {
    private byte[] mData;
    
    // 优化前:
    public Frame(byte[] data,int width,int height) {
        mData = new byte[data.length];
        ...
    }
    
    // 优化后
    public Frame(byte[] data,int width,int height) {
        mData = data;
        ...
    }
}

通过持有内存的引用,来代替拷贝内存。那么可能有读者有疑问:那我们是不是以后都通过持有引用的方式就可以了?并不是的,还是得根据业务的内容来决定。我们这里直接持有了相机缓存的引用,那么我们必须将数据处理操作设置为同步操作,并在处理结束后解除引用,否则会造成数据错误

在本流程中,Frame属于框架层接口。对于框架的设计,我们可以将业务层传递的byte数组,做一次拷贝,这是最安全的。不管上层传递的byte数组是否复用、是否释放等,都不会造成错误,但同时会带来一定的性能损耗。而设计为直接持有上层byte数组,意味着业务方必须了解接口参数的意义,懂得byte数组参数是被框架直接持有,如有必要,需要在外部做数据拷贝。这降低了一定的接口易用性,但也带来了更好地性能表现。另一种折中的解决方案,是创建两个不同的接口:拷贝与不拷贝,让用户决定使用哪个接口,但这也会为接口带来更高的复杂性。

最后我们来总结一下:

  1. 跟踪核心数据的处理流程,例子中是视频图像帧,记录数据发生拷贝、内存分配、运算等地方,重新思考是否有更好的解决方案,来减少计算和内存成本。
  2. 如果发生在接口层的优化,需要考虑优化的成本,是否符合场景需求。在易用性、复杂性、高性能等因素中找到平衡点。

接下来我们看第二个优化点。

优化二

android studio有一个非常好用的性能分析工具:android profile,他可以帮助我们分析运行中的cpu占用情况,以及内存的分配情况。从这些数据中我们可以去分析,我们的程序是否存在问题,是否有优化的空间。

继续案例一中的场景,在开发中,利用这个工具完成了许多性能优化,或者说是bug的排查。这里主要讲两个:内存抖动和内存泄露。具体工具的使用方式可以移步官方文档 Android Profiler ,这里我主要介绍使用这个工具解决问题的思路。

首先第一个:内存抖动。android profile可以在运行时,查看内存的占用情况,在开发过程中我使用工具查看了一下运行时内存情况,类似下图:(图源网络)

ps:下面相关的图像我均采用网络图片代替,嗯,,因为我懒得去重现一次场景再记录图像(手动狗头)

内存出现频繁增长与垃圾回收,图像呈现锯齿状。内存的频繁申请与释放,会损耗大量的性能,最终导致的结果就是我们的应用卡顿。android profile具有的另一个功能是记录函数的内存申请大小,如onCreate函数申请了多少内存,剩余多少内存等。通过这个功能,我查询了一下其在运行时内存分配所在的函数以及对象情况,如下:(图源网络)

工具详细记录了所创建的对象,如上面两个图:byte数组占大多数内存分配;createSubDecor函数占大多数的内存分配。那么我们拿到这些信息之后,就可以去到对应的方法进行排查。

在我的项目,我的原因主要是,在java层每帧都拷贝一次数据,导致不断开辟内存,但是却使用一次就丢弃。场景一的优化之后,减少了这次拷贝即解决了这个问题。

第二,内存泄露。检测内存泄露最好的方式就是:不断重进场景,观察内存增量。如果每次进入、退出之后,内存都有增量,则非常可能发生了内存泄露。在当时的检测中,发现运行时内存不断增长,即使手动垃圾回收也无济于事,最后导致OOM程序崩溃。这很明显就是发生了内存泄露。通过记录内存申请记录,发现是在jni,在c++线程中创建了java对象,使用完成后未删除局部引用,导致对象无法被虚拟机回收(在c++线程结束后,对象才会被回收)。

借助类似的类似的工具,可以查看程序对于资源的使用情况,也是非常方便帮助我们做性能优化的。

优化三

第三个优化,是学习了android在屏幕刷新机制上的思路,来提升整体的帧率性能。还回到我们工作的项目中来。我们的项目框架主要的能力就是渲染数据帧,但其对输入有一个要求:在上一帧渲染完成之前,不允许下一帧输入,否则会被限流节点丢弃数据。因此在原来的程序是这样的:

横向代表时间线,竖直线代表帧的输入,蓝色箭头代表框架sdk正在渲染数据帧,此时无法接受新的帧输入

观察上面的图像,框架处理数据的时间比帧输入更长,因此当第二帧输入的时候,第一帧还没处理完成,此时第二帧被丢弃。但是,当第一帧处理完成的时候,此时第三帧数据尚未到达,框架sdk进入空闲状态。

为了提高整体的处理帧率,我们需要让框架sdk时刻保持运行状态。因此我在这里学习android的渲染机制,加上双缓冲。

  1. 输入的数据缓存到双缓冲中进行保存,直接覆写到back内存中,如果front内存没有数据,则交换前后缓冲区
  2. 框架sdk从front区读取数据,并交换前后缓冲区

增加了缓存之后,可以保证框架sdk时刻处于运行当中,提高整体输出帧率,如下图:

这次优化的核心思路在于:充分利用cpu、内存等资源,来实现我们需要的效果。前面我们说的,都是如何节省资源,降低消耗,但都是在保证相同的输出效果,或者可接受损失的情况下。而这次性能优化,则是充分利用我们的硬件资源,实现更好的表现效果。对于渲染库而言,帧率表现,也是其性能表现的一个方面。

但此类型的优化也有他的代价:增加cpu与内存的负载。当评估下来之后,觉得这些资源的付出,值得换来帧率的提升,那么这次优化就是有意义的。反之,在一些低性能低下的机器,内存本身就非常紧张,双缓冲需要的内存代价就太大了。需要结合具体的业务情况来做判断。

优化四

做android开发的读者都知道,我们不能在主线程做耗时操作,会直接导致界面卡顿。因此大多数的操作我们会选择放在子线程去运行。多线程并行处理数据,可以提高整体的处理效率。在我们的项目中,对数据帧的处理通常是多节点的,如下:

节点的处理之间,有严格的先后关系,也有无关的可并行关系。对于并行关系的节点,我们可以创建多个线程,进行并行处理,提高处理时间,如下:

将处理2和处理3进行并行处理,再分别输出。这是最基本的优化方案,但事实上,在实际开发中还会遇到一些其他的问题。

处理2节点与处理3并行之后,需要付出的代价有:增加一个线程,处理增加线程切换的代价;增加数据占用的内存;增加对cpu的负载等等。换来的效率提升是否值得也是需要综合评估。

在项目中,我处理成并行是有效果的。原因是处理2与处理3耗时在几十ms,而线程切换带来的损耗小于1ms,内存占用低,因此对于数据的传递我是采用指针传递的,不涉及内存的拷贝。综合评估下,这是一个值得的优化,可以降低整体流程几十ms的时间。

你以为这就结束了吗?在测试中又发现了新的问题。在低端机器中,本身cpu已经跑满了,增加了线程之后,非但没有提升效率,反而线程切换的损耗带来了效率的下降。cpu负载这一代价,导致此优化是无效的。因此,该优化仅在中高端机器上开启。

线程是一把双刃剑,运用得好,可以为我们带来很大的效率提升,但同时也要注意需要付出的代价,避免反向优化。

总结

好,以上就是近段时间,我的一些关于性能优化的经验,咱们再来总结一下性能优化的思路:

  1. 性能优化是跟着具体的业务场景去实现的,跟踪核心数据、核心流程,如图像帧处理流程,分析每个步骤的处理是否合理,是否有优化的空间。
  2. 利用工具分析运行时的内存与cpu等资源情况,发现可能存在的内存泄露、内存抖动、cpu占用过高等问题。
  3. 性能优化可以是保证效果降低资源的使用,也可以是充分利用资源,提高程序的表现效果。
  4. 在做出优化的决策前,要综合评估当前的环境因素,分析优化需要付出的代价,避免反向优化。

性能优化过程中,可以参考以下的具体建议:

  1. 尽量减少数据拷贝
  2. 尽量减少内存申请,使用缓存池来代替反复申请与释放
  3. 平衡线程的数量与并行处理的任务耗时之间的关系,找到最佳平衡点
  4. 注意代码细节的性能问题,如遍历次数、内存占用量等,不同的编程语言也会有不同的特性,通过练习算法题目可以增强性能意识

性能优化并不是一个高深莫测的技能,而是需要我们在开发过程中时刻注意的问题,少一次拷贝、少一次遍历,都能让我们的程序性能更好。但我们无法在开发初期则达到最佳的性能表现,需要我们阶段性进行整体的性能检测与优化,来排查代码中存在的性能问题。

练习算法题目是个不错的方式,能够提高自己对性能的意识,增强自己性能优化能力,写出更加强壮的代码。不同的编程语言有自己的特性,需要结合自己的语言去学习,如java的垃圾回收、c++的主动释放等。我认为,面向api写代码大家都会,而真正决定差距的,是代码的设计与性能。


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

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

启动优化

内存优化

UI优化

网络优化

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

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

体积包优化
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D7Xf3HIR-1688019747949)(https://upload-images.jianshu.io/upload_images/28895723-7e80e0711187dbe2.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)]

《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/699789.html

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

相关文章

VirtualBox 部署 KVM 虚拟化

什么是KVM技术? KVM(Kernel-based Virtual Machine)是一种开源的虚拟化技术,它是Linux内核的一部分。KVM通过将Linux内核转换为Hypervisor,允许在同一物理主机上运行多个虚拟机实例,每个实例可以独享一部分…

JAVA学习之String学习

1.底层是用什么实现的? JDK8用的char数组,JDK9开始使用byte数组,而且都是final型,所以不同字符串(值)的地址必然不同。 char和byte的区别:char是2个字节表示,而byte是一个字节。 JDK17中&…

Vue3解决:[Vue warn]: Failed to resolve component: el-table(或el-button) 的三种解决方案

1、问题描述&#xff1a; 其一、报错为&#xff1a; [Vue warn]: Failed to resolve component: el-table If this is a native custom element, make sure to exclude it from component resolution via compilerOptions.isCustomElement. at <App> 或者&#xff1a; …

网络安全(黑客)自学

建议一&#xff1a;黑客七个等级 黑客&#xff0c;对很多人来说充满诱惑力。很多人可以发现这门领域如同任何一门领域&#xff0c;越深入越敬畏&#xff0c;知识如海洋&#xff0c;黑客也存在一些等级&#xff0c;参考知道创宇 CEO ic&#xff08;世界顶级黑客团队 0x557 成员&…

kubectl-ai:K8S资源清单的GPT助手

琦彦&#xff0c;在 **云原生百宝箱 **公众号等你&#xff0c;与你一起探讨应用迁移&#xff0c;GitOps&#xff0c;二次开发&#xff0c;解决方案&#xff0c;CNCF生态&#xff0c;及生活况味。 kubectl-ai 项目是一个kubectl使用 OpenAI GPT 生成和应用 Kubernetes 清单的插件…

【APP自动化测试必知必会】Appium之微信小程序自动化测试

本节大纲 H5 与小程序介绍 混合 App 元素定位环境部署 混合 App 元素操作 Airtest 测试 App 01.H5与小程序介绍 H5概述 H5 是指第 5 代 HTML &#xff0c;也指用 H5 语言制作的一切数字产品。 所谓 HTML 是“超文本标记语言”的英文缩写。我们上网所看到网页&#xf…

Oculus创始人谈Vision Pro:苹果在硬件设计、营销都做对了选择

早在Vision Pro正式发布之前&#xff0c;Oculus创始人Palmer Luckey就已经体验过早期版本&#xff0c;并给出了极高的评价。Luckey指出&#xff0c;苹果在XR头显上的策略是明智的&#xff0c;先打造出每个人预期中的头显&#xff0c;然后再去考虑如何让大家买得起。 Vision Pro…

远程控制电脑软件VNC安装使用教程:Windows系统

什么是VNC&#xff1f; VNC (Virtual Network Console)&#xff0c;即虚拟网络控制台&#xff0c;它是一款基于 UNIX 和 Linux 操作系统的优秀远程控制工具软件&#xff0c;由著名的 AT&T 的欧洲研究实验室开发&#xff0c;远程控制能力强大&#xff0c;高效实用&#xff…

【python】python编程基础

基础工具包 python 原生数据结构元组 Tuple列表 list集合 set字典 dictionary NumPy 数据结构数组 Ndarray矩阵 Matrix Pandas 数据结构序列 Series &#xff08;一维&#xff09;数据框 DataFrame &#xff08;二维&#xff09; Matplotlib 数据可视化绘制饼图绘制折线图绘制直…

《Linux操作系统编程》 第六章 Linux中的进程监控: fork函数的使用,以及父子进程间的关系,掌握exec系列函数

&#x1f337;&#x1f341; 博主 libin9iOak带您 Go to New World.✨&#x1f341; &#x1f984; 个人主页——libin9iOak的博客&#x1f390; &#x1f433; 《面试题大全》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33…

一文读懂高分文章必备分析-GSEA

Gene Set Enrichment Analysis 或称 GSEA&#xff0c;是一种常用于转录组基因表达分析的数据挖掘技术&#xff0c;已经在《nature》、《Cell》、《ISME》、《Molecular Cell》、《Bioactive Materials》等高分杂志中发表多篇文章&#xff0c;涉及转录组及多组学内容。 凌恩生物…

yxcms存储型XSS至getshell 漏洞复现

为方便您的阅读&#xff0c;可点击下方蓝色字体&#xff0c;进行跳转↓↓↓ 01 环境部署02 漏洞配置03 利用方式04 修复方案 01 环境部署 &#xff08;1&#xff09;yxcms yxcms 基于 PHPMySQL 开发&#xff0c;这是一个采用轻量级 MVC 设计模式的网站管理系统。轻量级 MVC 设…

13-Cookie、Session、Token

目录 1.前置知识——HTTP协议 1.1.HTTP 的主要特点有以下 5 个&#xff1a; 1.2.HTTP 组成 1.3.为什么会有Cookie、Session、Token&#xff1f; 2.Cookie 3.Session PS&#xff1a;Cookie 和 Session 的联系与区别 4.Token 4.1.token的组成 4.2.token是如何生成的&am…

【广州华锐互动】VR航天航空体验展厅提供沉浸式的展示效果

VR航天航空体验展厅是一种基于虚拟现实技术的在线展览形式&#xff0c;它通过模拟真实的太空环境&#xff0c;为用户提供了一种身临其境的参观体验。与传统的线上展览相比&#xff0c;VR航天航空体验展厅具有以下几个特色&#xff1a; 1.沉浸式体验&#xff1a;VR航天航空体验…

史上最详细的webrtc-streamer访问摄像机视频流教程

目录 前言 一、webrtc-streamer的API 二、webrtc-streamer的启动命令介绍 1.原文 2.译文 三、webrtc-streamer的安装部署 1.下载地址 https://github.com/mpromonet/webrtc-streamer/releases 2.windows版本部署 3.Linux版本部署 四、springboot整合webrtc-streamer …

技术不断变革,亚马逊云科技中国峰会引领企业重塑业务

过去十年&#xff0c;数字化转型的浪潮携带着机遇和挑战席卷而来&#xff0c;几乎每个企业都在做数字化转型&#xff0c;开始向大数据、人工智能等新技术寻求生产力的突破。但随着数字化转型深入&#xff0c;很多企业开始感受到数字化投入的成本压力&#xff0c;加之新技术正带…

Alibi:Attention With Linear Biases Enables Input Length Extrapolation

Alibi:Attention With Linear Biases Enables Input Length Extrapolation IntroductionMethodResult参考 Introduction 假设一个模型在512token上做训练&#xff0c;在推理的时候&#xff0c;模型在更长的序列上表现叫做模型的外推性。作者表明以前的位置编码如Sin、Rotary、…

JS 数据变化监听函数封装

文章目录 监听函数使用用例重复添加函数&#xff0c;只有最后一个监听函数有效 监听函数 /*** 监听函数* param {对象} vm * param {键值} key * param {触发函数} action */ function WatchValueChange(vm, key, action) {var val vm[key]Object.defineProperty(vm, key, {e…

阿里内部流传出来的《1000 道互联网大厂 Java 工程师面试题》来袭,面试必刷,跳槽大厂神器

眼看着"金九银十"也快到来了&#xff0c;很多小伙伴都蠢蠢欲动想要刚给自己涨一波薪资&#xff1b;面试作为涨薪最直接最有效的方式&#xff0c;我们需要花费巨大的精力和时间来准备。除了自身的技术积累之外&#xff0c;掌握一定的面试技巧和熟悉最常见的面试题&…

掌握imgproc组件:opencv-图像轮廓与图像分割修复

图像轮廓与图像分割修复 1.查找并绘制轮廓1.1 寻找轮廓&#xff1a;findContours()函数1.2 绘制轮廓&#xff1a;drawContours()函数1.3 案例程序&#xff1a;轮廓查找 2. 寻找物体的凸包2.1 凸包2.2 寻找凸包&#xff1a;convexHull()函数2.3 案例&#xff1a;寻找和绘制物体的…