前语

很高兴见到你。

关于功用优化,或许咱们的榜首反应是这是高手做的作业,一直以来我也是这样以为的。但在最近一段时间,在公司项目上做了一些结构的功用优化,让我初步掀开了功用优化的面纱,也对他有了进一步的知道。所以这篇文章结合我做的一些优化,做一些相关经历的共享。

功用优化一般状况下分为两类:时间优化与空间优化。前者是下降处理器处理时间,后者是下降内存运用量。归根结底都是下降对硬件资源的运用,来进步程序的功用,然后进步程序运转流畅性、下降功耗等。今日也主要从这两个方面来打开聊一聊。功用优化一般需求针对详细的代码、或许场景,所以后续的内容,我都会结合详细的场景来打开叙述,从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写代码大家都会,而真实决议距离的,是代码的规划与功用。

全文到此,原创不易,觉得有协助能够点赞保藏谈论转发。 有任何想法欢迎谈论区沟通指正。 如需转载请谈论区或私信沟通。 另外欢迎光临笔者的个人博客:传送门