2024年最全浅聊Android性能优化~_android应用在低性能设备的优化,2024年最新2024最新百度、头条等公司Android社招面试题目

总结

首先是感觉自己的基础还是不够吧,大厂好像都喜欢问这些底层原理。

另外一部分原因在于资料也还没有看完,一面时凭借那份资料考前突击恶补个几天居然也能轻松应对(在这里还是要感谢那份资料,真的牛),于是自我感觉良好,资料就没有怎么深究下去了。

之前的准备只涉及了Java、Android、计网、数据结构与算法这些方面,面对面试官对其他基础课程的考察显得捉襟见肘。

下一步还是要查漏补缺,进行针对性复习。

最后的最后,那套资料这次一定要全部看完,是真的太全面了,各个知识点都涵盖了,几乎我面试遇到的所有问题的知识点这里面都有!希望大家不要犯和我一样的错误呀!!!一定要看完!

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

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

  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的负载等等。换来的效率提升是否值得也是需要综合评估。

总结

作为一名从事Android的开发者,很多人最近都在和我吐槽Android是不是快要凉了?而在我看来这正是市场成熟的表现,所有的市场都是温水煮青蛙,永远会淘汰掉不愿意学习改变,安于现状的那批人,希望所有的人能在大浪淘沙中留下来,因为对于市场的逐渐成熟,平凡并不是我们唯一的答案!

资料.png
资料图.jpg

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

g-jYJTgDyO-1715899581092)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 8
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值