在一个风和日丽的下午,女朋友拿出手机,只听到一声“TiMi~”,她便愉快的打起了王者荣耀。
在经过几波激烈的团战后,我耳边就听到了女朋友的吐槽:“我怎样一打团就掉帧卡顿。网络460,这Android手机真是卡的不行,是不是该换手机了”, 然后她对我使了使眼色,对我进行了暗示。
我这人打小就聪明,这么简略的暗示我怎样或许会不明白呢?所以我便坐了起来说道:“这或许还真不是手机问题,有或许是它们的软件需求做功用优化了”。
女朋友白了我一眼说:“功用优化?那是什么东西”。成果我一下来了兴致,必然要和女朋友唠唠功用优化的那些事儿!
怎样做功用优化?
“你快说啊,功用优化是什么,该怎样去做功用优化呢?”女朋友嘟囔着。
“汽车有三大件,电脑和手机有五大件,即:输入、输出、运算器、操控器、存储器。 仅仅运算器和操控器都被集成在一块芯片上了,叫做CPU。所谓功用优化,其实便是让咱们的App能够合理且充沛的运用五大件,来让App能更稳定且高效的运转,首要优化点仍是在CPU和存储器上
”,
“我仍是不太理解,为什么优化这两个点后,App就能稳定高效的运转了呢?”女朋友挠着脑袋问道。
“手机软件上的每个界面,每个功用,每个交互行为最终都会转换成为一条条机器指令,而这些机器指令是要依托CPU去核算烘托履行的,假如CPU运用不合理则会呈现卡顿,发动或许运转速度缓慢甚至呈现ANR(Application Not Responding)反常。但是履行这些功用又离不开数据存储,假如存储器运用不合理,轻则存储空间糟蹋,重则呈现OOM反常而导致运用崩溃”
“听起来好高档,快快,那我该怎样针对这两个硬件去做优化呢?”女朋友忽然振奋了起来。
我才不会告诉她最好的办法便是买更好的CPU和存储器呢,不然几个月工资又没了。所以只好说道:“咱们无法直接优化这两个硬件,但在硬件层和运用层,中间还隔了一个操作体系层,这个操作体系的效果便是给咱们的软件分配运转时所需求的硬件资源,所以现在方针就很清晰了,想要做好功用优化,就需求咱们分别在操作体系层和运用层对怎样运用CPU和存储器进行优化
”
由于篇幅原因,这一篇文章仅仅只介绍CPU优化了,假如对咱们有协助,文章受欢迎的话(暗示三连
),后续我持续更新内存优化或许其他方面的优化。
一、CPU 优化 (速度优化)
“优化CPU能够加快发动速度、添加翻开页面速度,削减卡顿,有这么奇特吗?那这底层原理是什么呀?”女朋友又好奇的问道。
“难得你这么好奇,那我就来给你说道说道,我前面不是说了吗,App的UI界面烘托,行为交互,功用运算等等操作最终都会转换成为一条条机器指令,而这些机器指令是要依托CPU去履行的,所以在面临同一功用时,怎样让CPU以最短的时刻去完结功用
便是咱们的首要方针,当咱们想要进步某些场景(如发动、翻开页面、烘托动画等)的速度时,实质上便是下降 CPU 履行完这些场景指令的时刻,这个时刻简称为 CPU 时刻,而程序所耗费 CPU 时刻的核算公式:CPU 时刻 = 程序履行所需的时钟周期数 * 时钟周期时刻,其间的 程序履行所需的时钟周期数 = 程序指令数 * 每条指令的均匀时钟周期”
“哎哎哎,你等等,上面的那几项因子是什么,我不太懂,你给解说解说呗”,女朋友说道。
“好的,这就给你解说解说:“
- 程序履行所需的时钟周期数:CPU的一切操作都是以时钟周期为根底来核算的,每个时钟周期都会履行一个指令或完结一个操作。由于程序有若干代码,所以完结这些代码所需求的时钟周期总数便是程序履行所需的时钟周期数。
- 时钟周期时刻:指CPU完结一次履行所需的时刻,即:一个时钟周期的时刻,它一般以纳秒(ns)或皮秒(ps)为单位来表明,时钟周期时刻越短,CPU的运转速度就越快,它的倒数也便是时钟周期频率,1 纳秒的时钟周期时刻便是 1 GHZ 的时钟周期频率,厂商发布新手机或许咱们购买新手机时,都或多或少会提到 CPU 的时钟频率,比方高通骁龙 888 这款 CPU 的时钟频率是 2.8 GHZ,
这个目标也是衡量 CPU 功用最重要的一个目标
。
- 程序指令数:程序代码编译成机器码指令后的指令数量。
- 每条指令的均匀时钟周期:指令履行完毕所耗费的均匀时刻周期,指令不同所需的时钟周期数也不同。关于一些简略的单字节指令,在取指令周期中,指令取出到指令寄存器后会立即译码履行,不再需求其它的时钟周期。关于一些比较杂乱的指令,例如转移指令、乘法指令,则需求两个或许两个以上的时钟周期。
”所以底层原理就出来啦,针对以上公式,咱们只要对上述的任何一项因子进行优化都能够到达意图,所以咱们的优化办法论便是:①.削减程序指令数、②.下降每条指令的均匀时钟周期、③.下降时钟周期的时刻、④.其他优化计划。
“,我又补充到。
女朋友皱着眉疑惑的说道:“你这些我大致听懂了,可是都太理论了,详细该怎样去做呢?”
“哎,真拿你这个好奇宝宝没办法,好吧,那我就逐一为你翻开解说吧:”
1. 削减程序指令数
咱们的程序由Java代码在经过编译和汇编后,最终会转变成机器指令,当咱们分明能够用更少的机器指令去完结一项使命,却由于一些欠好的代码习气或许常识的缺乏而需求更多机器指令才干完结,这就会使咱们的程序变得愈加缓慢。所以,经过削减程序的指令数来进步运转速度,是咱们最常用和最直接的优化计划,比方下面这些计划都是经过削减指令数来进步速度的:
1.1 运用手机的多核
当咱们即将提速的场景的程序指令交给多个 CPU 一同履行时,关于单个 CPU 来说,需求履行的指令数就变少了,那 CPU 时刻天然就下降了,也便是并发的思维。但要留意的是,并发只要在多核下才干完结。例如我现在有一个8核的Android手机,那怎样才干运用手机的多核呢?答案是:运用多线程,8核的手机,就能一同并发的运转8个线程,当然,假如想要更高效合理的运用多线程,那么就必须学会运用线程池
。
怎样合理的运用线程 —— 线程池
线程池关于每一个开发者都是十分重要的,他除了能更好的运用多核CPU之外,还能够下降虚拟内存的占用,关于Linux体系而言,线程其实便是一个精简的进程,线程的创立最终是调用clone这个内核函数,而这个clone函数实践上创立的是一个进程 (官网描绘:These system calls create a new ("child") process, in a manner similar to fork),
它自身是占用了虚拟内存空间的,假如咱们不合理的处处运用“野线程”,就会形成虚拟内存空间的糟蹋。但是运用线程池,不只能够更好的对线程进行收敛,还能给线程进行分类,既能更好的一致办理运用的线程,又能更好的发挥出CPU的功用。那说了这么多,怎样才算是合理的运用线程呢?详细应该包含以下几个条件:
- 线程不能太多也不能太少: 线程太多会糟蹋 CPU 资源用于使命调度上,而且会削减了中心线程在单位时刻内所能耗费的 CPU 资源。线程太少了则发挥不出 CPU 的功用,糟蹋了 CPU 资源。
- 削减线程创立及状况切换导致的 CPU 损耗: 线程的频频创立毁掉,或许频频的状况切换,如休眠状况切换到运转状况,或许运转状况切换到休眠状况,这些都是对 CPU 资源的损耗。
但是满意以上条件最好的办法便是运用线程池,这要求咱们在运用开发进程中运用的线程最好悉数都是从线程池中创立的,而且还要能正确的运用线程池
,关于怎样正确的运用线程池,咱们能够从以下三个方面下手:
- 怎样创立线程池;
- 线程池的类型和特性;
- 怎样运用线程池;
线程池的类型及特色
线程池的分类办法比较多,假如咱们依照事务中运用的频频程度来做分类,首要分为以下三类线程池:
- CPU密集型线程池:用来处理 CPU 类型使命,如核算,逻辑操作,UI 烘托等。例如:newFixedThreadPool、newWorkStealingPool
- IO密集型线程池:用来处理 IO 类型使命,如拉取网络数据,往本地磁盘、数据读写数据等。例如:newCachedThreadPool
- 其他线程池:自界说用来满意事务独特化需求的线程池。例如:newScheduledThreadPool(守时使命线程池)、newSingleThreadExecutor(单一线程池)等等。
接下来咱们经过检查源码,看到Executor这个目标里就有十多个newXXXThreadPool的静态办法来创立线程池。
下表介绍了上图中呈现的那些线程池:
线程池类型 | 适用场景 | 特色 |
---|---|---|
newSingleThreadExecutor | 适用于需求保证使命依照次序履行的场景,如日志处理、数据库连接池等 | 只要一个线程,使命依照次序履行 |
newFixedThreadPool | 适用于需求操控线程数量的场景,如服务器处理恳求、并发下载等 | 固定数量的线程池,能够操控线程数量 |
newCachedThreadPool | 适用于履行许多短期异步使命的场景,如网络爬虫、推送体系等 | 依据需求创立线程,适用于履行许多短期异步使命的场景 |
newSingleThreadScheduledExecutor | 适用于需求依照次序履行守时使命的场景,如守时备份、守时清理等 | 只要一个线程,能够依照次序履行守时使命 |
newScheduledThreadPool | 适用于需求依照次序履行守时使命,而且需求操控线程数量的场景 | 能够操控线程数量,能够依照次序履行守时使命 |
newWorkStealingPool | 适用于需求履行许多独立使命的场景,如图像处理、视频编码等 | 依据需求创立线程,能够履行许多独立使命 |
不同类型的线程池有不同的责任,专门用来处理对应类型的使命,下面一同来看一下怎样创立不同类型的线程池。
创立线程池
上面虽然呈现了那么多线程池,但它们实践都是经过ThreadPoolExecutor
这个目标创立的,这些线程池其实是ThreadPoolExecutor
不同入参的实例,所以接下来咱们对ThreadPoolExecutor
做进一步的剖析,咱们能够先看一下它的结构函数:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
假如咱们将 ThreadPoolExecutor 结构函数中的入参悉数了解了,咱们也就彻底把握了线程池的用法,下面就详细解说一下该结构函数中每个入参的含义:
入参 | 阐明 |
---|---|
int corePoolSize | 表明中心线程数量:在创立了线程池后,线程池中此刻线程数为 0,当有使命来到需求履行时,就会创立一个线程去履行使命,当线程池中的线程数目到达 corePoolSize 后,就会把后边到来的使命放到缓存行列中。除非手动调用了allowCoreThreadTimeOut(boolean) 这个办法,用来声明中心线程需求退出,不然中心线程发动后便一向是存活不退出的状况。 |
int maximumPoolSize | 表明线程池中最多能创立线程数量:当中心线程全在履行使命时,又有新使命到来,使命会放在缓存行列中,假如缓存行列也满了,才会发动新的线程来履行这些使命,这些线程也成为非中心线程,非中心线程的数量加上中心线程的数量便是线程池最多能创立的线程数量。 |
long keepAliveTime | 表明非中心线程的存活时刻:当线程池中某个非中心线程线程空闲的时刻到达 keepAliveTime,该线程就会退出,直到线程池中的线程数不超越 corePoolSize,所以这个参数对中心线程是无效的,由于中心线程不会退出,只对非中心线程有效。 |
TimeUnit unit | 表明 keepAliveTime 的时刻单位,如秒、毫秒等 |
BlockingQueue workQueue | 表明使命缓存行列:常见的缓存行列有这些:1. LinkedBlockingDeque 是一个双向的并发行列,首要用于 CPU 线程池;2. SynchronousQueue 虽然也是一个行列,但它并不能存储 task,所以每逢这个行列添加一个 task 时,由于超出了存储行列的容量线程,线程池这个时分都会创立一个新线程来履行这个 task,用于 IO 线程池中。 |
ThreadFactory threadFactory | 线程工厂:可自界说创立线程的办法,设置线程称号,能够默许运用Executors.DefaultThreadFactory(“线程名”),在虚拟内存优化时,也提到过能够运用自界说的线程工厂,来创立栈空间只要 512 KB 的线程。 |
RejectedExecutionHandler handler | 反常处理:一切因反常而无法履行的线程,比方线程池现已满了之后,新的使命就无法履行了,都会放在 RejectedExecutionHandler 中做兜底处理。 |
这儿需求特别留意,只要缓存行列容量满了,即缓存行列中缓存的 task 到达上限时,才会开端创立非中心线程。
经过上面临入参的解说,咱们根本能看懂 Executors 目标中创立的线程池代码,也能自己去创立一个线程池了,但线程池的分类许多,且参数也比较杂乱,咱们假如想要更合理的运用线程池,还需求对怎样设置线程池的参数做进一步了解。
怎样合理运用线程池
这儿咱们仅针对CPU密集型线程池
和 IO密集型线程池
去做剖析,怎样去设置以上参数才干更合理更高效的运用线程池, 其他线程池
由于归于自界说用于完结特定事务需求的场景,且不怎样运用,所以这儿暂不剖析,不过咱们看完针对CPU密集型线程池
和 IO密集型线程池
的优化后,应该也能举一反三,自己结合实践场景去优化自界说线程池。
CPU密集型线程池
首先是 corePoolSize
参数(中心线程数)。CPU密集型线程池是用来履行 CPU 类型使命的,所以它的中心线程数量一般为 CPU 的核数,抱负状况下等于核数的线程数量功用是最高的,由于咱们既能充沛发挥 CPU 的功用,还削减了频频调度导致的 CPU 损耗。不过,程序在实践运转进程中无法到达抱负状况,所以将中心线程数设置为 CPU 核数个或许不是最优的,但绝对是最稳妥且相对较优的计划。
接下来是maximumPoolSize
(最大线程数)。一般来说,CPU密集型线程池的最大线程数便是中心线程数
,由于 CPU 的最大运用率便是每个核都满载,想要到达满载只需求核数个并发线程就行了。别的,线程的创立和毁掉都需求耗费体系资源,而且线程数量过多还会导致线程切换的开支添加,从而影响体系的功用,所以咱们这样进行设置,CPU 资源就能被彻底发挥。
既然最大线程数便是中心线程数,那 keepAliveTime
这个非中心线程数的存活时刻便是零了。
然后是workQueue
缓存行列。CPU 线程池中一致运用 LinkedBlockingDeque,这是一个能够设置容量并支持并发的行列。由于 CPU 线程池的线程数量较少,假如较多使命来临的话,就需求放在存储行列中,所以这个存储行列不能太小,不然行列满了之后,新来的使命就会进入到过错兜底的处理逻辑中。所以,咱们能够结合运用的规模和事务需求,将存储行列设置的尽或许大,但为了程序的功用和稳定性,不主张设置为无限大。
假如程序有些反常的死循环逻辑不断地往行列添加使命,而这个行列就能一向接受使命,可是却会导致程序体现反常,由于 CPU 线程池悉数用来履行这个反常使命了。可是当咱们将这个行列设置成有限的,比方 64 个,那这个反常的死循环就会将行列打满,让接下来的使命进入到兜底逻辑中,而咱们能够在兜底逻辑中设置监控,就能及时发现这个反常了。
最终是ThreadFactory
线程工厂和 RejectedExecutionHandler
兜底处理的 handler 逻辑,能够运用默许的,假如咱们有特别的需求,比方经过 ThreadFactory 设置优先级,线程名或许优化线程栈巨细,或许在兜底逻辑中添加监控,都能够经过继承对应的类来进行扩展。
咱们经过检查 Exectors 东西类,就能够发现经过 newFixedThreadPool 创立的线程池实践上便是 CPU 线程池的,经过命名也能够猜到,这是一个线程数固定的线程池,所以契合 CPU 线程池线程数固定是 CPU 核数个这一特性。咱们在运用的时分,还能够经过带 ThreadFactory 入参的这个办法 ,调整 FixedThreadPool 线程池的线程优先级。
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
IO密集型线程池
IO密集型线程池首要用来履行 IO 使命,IO 使命实践上耗费的 CPU 资源是十分少的,当咱们要读写数据的时分,会交给 DMA (直接存储器拜访)芯片去做,此刻调度器就会把 CPU 资源切换给其他的线程去运用。由于 IO 使命对 CPU 资源耗费少,所以每来一个 IO 使命就直接发动一个线程去履行它就行了,不需求放入缓存行列中,即便此刻履行了十分多的 IO 使命,也都是 DMA 芯片在处理,和 CPU 无关。了解了这一特性,咱们再来看看 IO 线程池的入参怎样设置。
首先是corePoolSize
(核线程数)。 这个设置多少没有定性规则,它和咱们 App 的类型有关.
假如 IO 使命比较多,比方新闻咨询类的运用或许大型运用,能够设置得多一些,十几个也能够,太少了就会由于 IO 线程频率创立和毁掉而产生损耗。假如运用较少,IO 使命不多,直接设置为 0 个也没问题。
maximumPoolSize
(最大线程数)。这个能够多设置一些,保证每个 IO 使命都能有线程来履行,毕竟 IO 使命对 CPU 的耗费不高。一般来说,中小型运用设置 60 个左右就足够了,大型运用则能够设置 100 个以上。这儿不主张将数量设置得特别大,是为了防止程序呈现反常 BUG创立许多的 IO 线程(比方某个场景标志位过错导致逻辑不退出,然后一向创立 IO 线程),虽然 IO 使命履行耗费 CPU 资源不多,可是线程的创立和毁掉是需求耗费 CPU 资源的。
然后是workQueue
缓存行列。咱们把它设置为 SynchronousQueue
行列,它是一个容量为0的行列,由于经过上述参数表格介绍,该行列是不能存储task使命的,所以一旦有新的线程使命过来,会立刻创立一个新的线程去履行。这也契合 IO 密集型线程池的理念,首先自身履行不需求运用太多CPU资源,其次 IO 需求高响应。
了解了上面的常识,咱们再来看 Exectors 东西类,发现经过 newCacheThreadPool 创立的线程池实践上便是对应 IO 线程池的,可是经过 newCacheThreadPool 创立出来的 IO 线程池并不是最优的。咱们能够看到,它的中心线程池数量为 0,而且最大线程数量为无限大。咱们彻底能够扔掉Exectors 供给的办法,依照自己的规则去创立 IO 线程池。这儿需求留意的是,咱们在设置 IO 线程池的线程优先级时,需求比 CPU 线程池的线程优先级高一些,由于 IO 线程中的使命是不怎样耗费 CPU 资源的,优先级也更高一些,能够防止得不到调度的状况呈现。
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory);
}
这一段详细介绍了线程池的常识,包括怎样创立线程池,以及线程池的品种和特性,特别是CPU密集型线程池
和IO密集型线程池
的界说,它们是怎样创立的。当咱们能了解线程池的原理、了解怎样合理设置线程池的数据、了解各类线程池的特性后,运用线程池就简略许多了,将特定的使命放入特定的线程池中履行,各司其职即可,接下来咱们持续叙述其他的优化计划。
1.2 养成良好的代码习气使和运用更优的算法或数据结构
这一点很好理解,相同的功用用更简练或更优的代码来完结,指令数也会削减,指令数少了程序的速度天然也就快了。详细落地这一类优化时,咱们能够用抓 trace 或许在函数前后统计耗时的办法去剖析耗时,将这些耗时久的办法用更优的办法完结。至于怎样做好这一点,这儿也给出几个比方:
A. 运用合适的数据结构和算法:选择恰当的数据结构和算法能够削减代码的杂乱性和履行时刻。例如,运用哈希表(HashMap)而不是线性查找列表(ArrayList)能够大大进步查找功率。
// 线性查找列表的示例
ArrayList<String> names = new ArrayList<>();
String searchName = "John";
for (String name : names) {
if (name.equals(searchName)) {
// 履行操作
break;
}
}
// 运用哈希表的示例
HashMap<String, Integer> namesMap = new HashMap<>();
String searchName = "John";
if (namesMap.containsKey(searchName)) {
// 履行操作
}
B. 防止重复核算:假如某个核算量较大的操作成果在多个地方都被运用到,能够考虑将成果缓存起来,防止重复核算。
// 重复核算的示例
int result1 = complexCalculation();
// 履行其他操作
int result2 = complexCalculation();
// 防止重复核算的示例
int result = complexCalculation();
int result1 = result;
// 履行其他操作
int result2 = result;
C. 运用缓存或缓存策略:关于一些需求经过杂乱核算和频频读取的数据,能够考虑运用缓存来防止重复的拜访或核算,这与上面的“优化点B”可结合运用。
// 没有缓存的示例
public int calculateExpensiveOperation(int input) {
// 履行杂乱核算
return result;
}
// 运用缓存的示例
private Map<Integer, Integer> cache = new HashMap<>();
public int calculateExpensiveOperation(int input) {
if (cache.containsKey(input)) {
return cache.get(input);
} else {
int result = performExpensiveOperation(input);
cache.put(input, result);
return result;
}
}
D. 削减不必要的目标创立:防止在循环或频频履行的代码块中创立不必要的目标,尽量重用已有目标。
// 不必要的目标创立示例
for (int i = 0; i < array.length; i++) {
String itemString = new String(array[i]);
// 履行操作
}
// 削减目标创立的示例
String itemString = null;
for (int i = 0; i < array.length; i++) {
if (itemString == null) {
itemString = new String(array[i]);
} else {
itemString = itemString.concat(array[i]);
}
// 履行操作
}
E. 多运用资源引证:当需求运用运用程序资源(例如字符串、图标等)时,运用资源引证而不是硬编码的值,这样的代码习气不只能够进步代码的可维护性,而且还能削减指令数。
// 硬编码的字符串示例
String title = "Welcome to my app";
// 运用资源引证的示例
String title = getResources().getString(R.string.app_title);
F. 运用StringBuilder拼接字符串:在需求频频拼接字符串的状况下,运用StringBuilder类而不是字符串拼接操作符(+),能够进步功用并削减指令数。由于在Java中,字符串是不可变的,这意味着每次对字符串进行拼接、修正或连接操作时,都会创立一个新的字符串目标。这种频频的字符串目标创立和拷贝操作会占用额外的内存和耗费许多的时刻。而StringBuilder类是可变的,它供给了一种高效的办法来构建和修正字符串,而无需每次都创立新的字符串目标。
// 字符串拼接操作符的示例
String result = "Hello, " + name + "!";
// 运用StringBuilder的示例
StringBuilder sb = new StringBuilder();
sb.append("Hello, ");
sb.append(name);
sb.append("!");
String result = sb.toString();
上面提到的这些计划都是我最常用的计划,依据削减程序指令数
这一根本原理,还能衍生出许多计划来进步速度,这儿无法一一列全,咱们也能够自己想一想还能扩展出哪些计划出来。
1.3 运用CPU的搁置时刻
在 CPU 搁置的时分,履行预创立 View,预预备数据等预加载逻辑,将接下来或许会用到的View、布局、或许数据提前处理好
,那么在翻开这些场景时,指令数量由于预加载履行了一部分而变少了,运转速度天然也就变快了。
那为什么预加载使命要放在 CPU 的搁置时刻呢?假如预加载使命不是放在 CPU 的搁置时刻就会和中心场景抢占资源,导致中心场景速度变慢。比方,咱们常常会在发动时预加载一些逻辑以此来进步后边场景的速度,但这样会导致发动变慢。假如把这些使命放在 CPU 搁置后再履行,就能做到既不影响发动的速度,又能进步后边场景的速度了。
那问题又来了,该怎样判别CPU的搁置时刻呢?咱们能够发动一个守时使命,每 5 秒检测一次 CPU 是否现已搁置,假如现已搁置了,则回调告诉各个事务进行预加载使命的履行(5 秒不是固定值,需求依据所开发运用的类型来调整)。这儿推荐运用times函数来判别CPU是否搁置。
times()函数
进程用此函数取得自己和已终止子进程的时钟时刻,用户CPU时刻和体系CPU时刻。回来值clock_t标示经过的墙上时钟时刻。
<sys/times.h>头文件中.
原型: clock_t times(struct tms *buf); 正确回来墙上时钟经过时刻, 犯错回来-1 ;
其间参数类型struct tms为:
struct tms
{
clock_t tms_utime; //用户CPU时刻
clock_t tms_stime; //体系CPU时刻
clock_t tms_cutime; //以终止子进程的用户CPU时刻
clock_t tms_cstime; //已终止子进程的体系CPU时刻
};
由于 times 函数是一个体系函数,咱们需求在 Native 层才干调用。所以咱们直接写一个 Native 办法,然后在 Java 层经过 Jni 履行这个 Native 办法,就能高效获取到进行所耗费的 CPU 时刻了。
#include <sys/times.h>
float getCpuTimes(JNIEnv *env) {
struct tms currentTms;
times(¤tTms);
return currentTms.tms_utime + currentTms.tms_stime;
}
现在咱们现现已过 times 函数拿到了运用耗费的CPU时刻,那该怎样核算CPU的运用率才干判别出CPU是否空闲呢?咱们用这个公式能够进行判别 CPU 速率 = 单位时刻内进程内耗费的 CPU 时刻 / 单位时刻。咱们以 5 秒为例,则CPU速率如下:
//这儿app的cpu时刻都转换成了秒
float cpuSpeed = (beforeAppTime - curAppTime) / 5f;
当运用处于搁置状况时,CpuSpeed 一定在 0.1 以下
现在咱们现已能够判别CPU是否处于搁置状况了,后续就能够在CPU搁置时做预处理作业来进步运用的运转速度了。
1.4 削减 CPU 等候——锁优化和运用协程
假如某个线程或进程具有 CPU 的时刻片,可是 CPU 却在当时指令段停下来,长时刻无法接着履行后边的代码指令的状况,都能够看做是 CPU 的等候。此刻,CPU 之所无法持续履行后边的代码,或许是由于代码堕入了空循环导致 CPU 空转,或许 CPU 被切走去履行其他线程了。 总的来说,有两种状况常常导致 CPU 等候,一是等候锁,二是等候 IO。
等候锁优化
运用 Java 进行运用开发遇到多线程并发使命时,咱们一般都用 synchronize 来对办法或许数据加锁。当这个锁被某一个线程持有时,另一个线程就需求等候锁开释后,才干对办法和数据进行拜访。
恳求 synchronize 锁的流程如下:首先判别这个锁是否被其他线程持有,假如持有则经过屡次的循环来判别锁是否开释,这个进程就会导致 CPU 的空转,假如屡次空转后仍是无法取得锁,恳求锁的线程便会堕入休眠并加入等候行列,待锁开释后被唤醒。从这个流程能够看到,恳求锁时,不管是空转仍是休眠都会导致当时线程无法取得 CPU 资源。假如这个线程是中心线程,比方主线程和烘托线程,就会导致运用的体验速度变慢。
由于锁相关的内容十分巨大,所以这儿首要介绍下锁优化办法:
- 无锁比有锁好:除了不加锁,还有线程本地存储,倾向锁等计划,都归于无锁优化。
- 合理细化锁的粒度:比方将 Synchronize 锁住整个办法细化成只锁住办法内或许会产生线程安全的代码块。
- 防止在主线程或许烘托线程运用锁: 前面提到锁或许会堵塞线程,所以假如非要运用到锁,也应尽量防止在主线程或烘托线程上运用。
削减 IO 等候
当一个线程履行 IO 使命时,比方往内存读写数据,此刻实践是 DMA(直接存储器拜访) 芯片在履行操作,并不关 CPU 什么事。当时就会呈现两种状况:
一是有其他线程履行 CPU 使命,使命调度器会将 CPU 切换给其他线程去运用。
二是没有其他 CPU 相关使命,CPU 就会一向等候,直到 DMA 芯片完结读写内存数据的操作,再接着履行后边的代码逻辑。
但不论是哪种状况,关于当时这个线程来说,履行完一切指令的时刻变长了,也便是指令履行所耗费的均匀时钟周期变长了。假如这个线程是主线程或许烘托线程,相同会导致运用运转速度变慢。
但你或许会有疑问,在一般状况下,咱们需求恳求网络或许等候IO使命的成果才干进行后边的逻辑,那这时分咱们能够开一个子线程去履行IO或许恳求网络呀,相同适当所以并发操作。但你有没有想过,万一你这个线程就现已是子线程了呢,或许现已有十分多的线程在处理使命了,或许此刻线程池现已满了怎样办?抛开这些问题不谈,就连线程切换也是比较耗费资源的。那么有什么好的办法能够在遇到 IO 或许网络恳求时不让这个线程休眠,而让它去做其他事呢?答案便是运用协程。
协程是什么?
Kotlin 官方文档上说,协程实质上是一个轻量级线程。 在上面描绘的状况中,Java线程确实做不到,可是协程能够。由于一个 Java 线程实践上便是一个精简的进程 (这儿咱们在上文叙述线程池的时分有解说过)
,进程的状况在许多时分是受操作体系管控的,比方调度器调度的时分,会切换进程状况;等候 IO 时,进程会堕入休眠等等。但协程不受内存调度器的约束,当你创立协程时,这些协程实践都在同一个进程上运转,Kotlin 内部完结了调度机制,就像CPU的进程调度机制相同,去调度履行这个协程使命。这时协程所在的这个线程就不会由于 IO 使命而被休眠堵塞(官网称为「非堵塞式挂起」)。
咱们在Kotlin上一般是这样运用协程的:
// 运用 coroutineScope 函数创立一个新的协程效果域,并在该效果域中发动两个子协程。
// 在 coroutineScope 函数履行完结后,会等候一切子协程履行完结后再持续履行。
// suspend 关键字是用来标记挂起函数,挂起函数能够在异步操作中暂停协程的履行,等候异步操作完结后再恢复协程的履行。
suspend fun doSomething() {
coroutineScope {
launch {
// 异步操作
}
launch {
// 异步操作
}
}
// 一切子协程履行完结后持续履行
}
好了,什么是协程?现在应该更清楚了,协程便是 launch{} 大括号创立的代码块,创立的一个个 launch{} 便是一个个协程。
协程的优点
- 下降 IO 等候,有更高效的运转功率。
- 消除回调阴间,用同步的办法写异步的代码。
咱们就针对上述两点优点解说一下。协程下降 IO 等候不只仅仅仅上文提到的能够进行非堵塞式挂起
从而让线程一向处于运转状况,防止线程切换形成的资源耗费。更关键它在某些场景下能进步 IO 或许网络恳求速度。例如:有这么一个场景,咱们需求在这多个恳求结束后,将信息进行整理兼并,然后再更新UI。这在Java上完结是很困难的,一般咱们会经过运用先后恳求来替代一同恳求
(没错,我以前便是这么干的),在实践开发中假如这样写,原本能够并行处理的恳求被强制经过串行的办法去完结,或许会导致等候时刻长了一倍,也便是功用差了一倍。就像下面这样,而且假如事务一多,或许会形成回调阴间
:
api.getName(params) { name ->
api.getAvatar(params) { avatar ->
show(merge(name, avatar))
}
}
上述运用Kotlin代码看起来似乎也还好,可是假如运用Java就不相同了:
api.getName(new Callback<String>() {
@Override
public void success(String name) {
api.getAvatar(new Callback<String> {
@Override
public void success(String url) {
User uersInfo = merge(name, url);
runOnUiThread(new Runnable() {
// 主线程中设置UI
@Override
public void run() {
show(userInfo);
}
}
}
@Override
public void failure(Exception e) {
}
});
}
@Override
public void failure(Exception e) {
...
}
});
而假如运用协程,能够直接把两个并行恳求写成上下两行,最终再把成果进行兼并即可:
coroutineScope.launch(Dispatchers.Main) {
val name = async { api.getName(params) } // 经过网络恳求获取用户姓名
val avatar = async { api.getAvatar(params) } // 经过网络恳求获取用户头像
val merged = mergeInfo(name, avatar) // 兼并成果
show(merged) // 更新 UI
}
经过这一个比方咱们能够看到,运用协程不只仅轻松的把串行恳求改为了并行恳求,而且还消除了回调阴间,用同步的办法写异步的代码。进步运转功率的一同还让杂乱的并发代码,变得简略且清晰了。
1.5 经过其他设备来削减当时设备程序的指令数
这一点也衍生许多优化计划,比方 Google 商店会把某些设备中程序的机器码上传,这样其他用户下载这个程序时,便不需求自己的设备再进行编译操作,由于进步了装置或许发动速度。再比方在翻开一些 WebView 网页时,服务端会经过预烘托处理,将 IO 数据都处理完结,直接展现给用户一个静态页面,这样就能极大进步页面翻开速度。
2. 下降时钟周期时刻
想要下降手机的时钟周期,一般只能经过升级 CPU 做到,每次新出一款 CPU,比较上一代,不只在时钟周期时刻上有优化,每个周期内可履行的指令也都会有优化。比方高通骁龙 888 这款 CPU 的大核时钟周期频率为 2.84GHz,而最新的 Gen 2 这款 CPU 则到达了 3.50GHz。
虽然咱们无法下降设备的时钟周期,可是应该防止设备进步时钟周期时刻,也便是降频现象,当手机发热发烫时,CPU 往往都会经过降频来削减设备的发热现象,详细的办法便是经过合理的线程运用或许代码逻辑优化,来削减程序长时刻超负荷的运用 CPU。
3. 下降每条指令的均匀时刻周期
在下降每条指令的均匀时刻周期上,咱们能做的其实也不多,由于它和 CPU 的功用有很大的关系,但除了 CPU 的功用,以下几个方面也会影响到指令的时刻周期。
编程言语:Java 翻译成机器码后有更多的简介调用,所以比 C++ 代码编译成的机器码指令的均匀时刻周期更长。
编译程序:一个好的编译程序能够经过优化指令来下降程序指令的均匀时刻周期。
下降 IO 等候:从严格含义来说,IO 等候的时刻并不能算到指令履行的耗时中,由于 CPU 在等候 IO 时会休眠或许去履行其他使命。可是等候 IO 会使履行完指令的时刻变长,所以这儿仍然把削减 IO 等候算入是下降每条指令的均匀时刻周期的优化计划之一。上述有介绍过怎样下降IO等候的办法。
4. 其他优化计划
关于上述 ①.削减程序指令数、②.下降每条指令的均匀时钟周期、③.下降时钟周期的时刻,
这三种优化计划来看,它们经过对运用层代码的优化完结了功用优化,是归于运用层方面的优化
。而咱们在上文说过优化还能够从操作体系层面去进行优化,接下来的其他优化计划中,就揭示了怎样从操作体系层面去做功用优化的。
4.1 进步缓存的命中率 —— Dex类文件重排序
这一优化计划是从操作体系层面和硬件层去考虑的。咱们的程序的运转进程,其实是CPU在不断读取指令并履行指令的进程,CPU在读取指令时,首先会从寄存器读取,假如寄存器里没有再从高速缓存读取,最终才从主存读取,读取到指令后,也会先从主存加载到高速缓存,再从高速缓存加载到寄存器中。高速缓存其实便是若干个cache line巨细的块,这个cache line巨细和CPU类型有关,主流的是64字节。
高速缓存在读取数据时,假设需求的指令数据只要4个字节,高速缓存也会读满一个 cache line 巨细的数据(也便是64个字节),那么剩余的60个字节实践就用不到了,所以咱们假如能想办法把这剩余的60个字节的数据给用上,这就能够有效的进步缓存命中率,从而下降高速缓存向主存读取数据的次数了,就能够让CPU更快的履行指令了。
“所以说了这么多,究竟怎样把剩余的60个字节给用上呢,那你却是说呀!”女朋友又着急的说道。 “我知道你很急,可是你先别急,那接下来就给你解说下吧,想要有效运用剩余的60字节,就需求用到局部性原理了!”
局部性原理
局部性原理是核算机体系设计中的重要准则,它能够协助进步体系的功用和功率。例如,CPU缓存的设计便是依据局部性原理,它将最近拜访的数据和指令存储在高速缓存中,以便快速拜访。详细来说,局部性原理首要分为两个方面,分别是时刻局部性
和空间局部性
。
时刻局部性
是指在一个时刻段内,假如一个数据或指令被拜访过一次,那么在不久的将来它很或许会再次被拜访。这是由于程序的履行一般具有循环、分支等结构,会重复运用同一段代码或数据。
空间局部性
是指在一个时刻段内,假如一个数据或指令被拜访过一次,那么与它相邻的数据或指令也很或许会被拜访。目前,高速缓存读取数据便是依照空间局部性来读的,也便是读取当时需求被运用的数据,以及在内存上紧挨着的数据,一共凑齐 cache line 巨细的数据后再加载进高速缓存中。
知道了局部性原理后,那咱们该怎样运用呢?由于在咱们的App发动履行时,关于第一次用到的目标,高速缓存中是没有的,所以这时分需求去主存读取数据,但是读取到的不只仅只要这一个目标数据,后边还会紧挨着许多数据,直到数据量到达64字节。这时假如后边紧挨着的目标在接下来立刻就会用到,那么高速缓存下次就不用再去主存读数据了,这样CPU削减等候时刻,就能更快的履行指令,咱们的程序也能运转的愈加流通。
当咱们的项目被编译成Apk包后,一切的class文件会被进行整合,然后放在dex文件中。但是dex文件中的class文件次序并不是依照程序履行次序来存放的,由于咱们也不知道class文件的履行次序,这样就会导致咱们读取了4个字节的数据,那么后边剩余的60字节的数据接下来将不会用到。可是假如咱们能提前将程序运转一遍,把其间class目标的运用次序给搜集起来,再按这个次序从头调整dex文件中class文件的次序,这样在运用发动时,就能把所需求的class文件加载进高速缓存中,运用的发动速度天然就变快了。
上面的流程是美好的,但完结起来仍是很杂乱的,咱们需求对每个目标插桩后才干知道目标的先后运转次序,而且咱们也需求对 dex 文件结构十分了解,这样才干正确重排 dex 文件中类文件的次序。
可是,十分幸运,目前有老练的开源结构能够直接运用,那便是Facebook的开源东西:redex
那么接下来就解说下Redex的运用,还有我踩的一些坑,我运用的是MAC电脑,其他体系的没有尝试过。
Redex 的运用办法
- 装置 Redex 所需依靠
// 下载相关环境,装置这些库时,或许会遇到权限问题,有时需求运用办理员权限或许修正文件夹的一切者和权限
xcode-select --install
brew install autoconf automake libtool python3
brew install boost jsoncpp
- 下载装置 Redx
// 克隆Redex项目
git clone https://github.com/facebook/redex.git cd redex
cd redex
// 对 Redex进行编译和装置
autoreconf -ivf && ./configure && make
sudo make install
- 配置 Redex
// 在 redex/config/default.config 中找到配置文件
{
"redex" : {
"passes" : [
// 默许敞开的配置若干条,这儿我给默许配置悉数移除了,由于会有 ClassNotFoundException 过错
....
....
// 在配置文件添加 InterDexPass 敞开,以及新增 coldstart_classes,指定 class 调用次序
"InterDexPass"
],
"coldstart_classes" : "app_list_of_classes.txt" // class调用次序列表
}
}
- 取得发动class加载次序列表
// 1. 获取你的运用包名 pid
adb shell ps | grep <运用包名>
// 2. 搜集堆内存,release 版别需求 root 权限, debug版别不需求 root
adb root
adb shell am dumpheap <运用Pid> /data/local/tmp/SOMEDUMP.hprof
// 3. 把堆内存文件拉取到本地
adb pull /data/local/tmp/SOMEDUMP.hprof <本地途径>
// 4. 经过redex 供给的 python脚本解析堆内存,生成类加载次序列表
// 留意,该指令我在履行时,呈现空目标过错,细心读了官方文档后,了解有存在堆内存反常状况,所以需求加一个参数进行疏忽
python3 redex/tools/hprof/dump_classes_from_hprof.py --hprof SOMEDUMP.hprof > app_list_of_classes.txt
// 所以,我自己运用的是如下指令,咱们能够依据状况自行调整
python redex/tools/hprof/dump_classes_from_hprof.py --allow_missing_ids --hprof SOMEDUMP.hprof > app_list_of_classes.txt
// 5. 履行 redex 逻辑,生成新的apk包
// 官方供给的指令是
python3 redex.py -c default.config --android-sdk-path path/to/android/sdk path/to/your.apk -o path/to/output.apk
// 以上指令我这边呈现报错,说缺少Proguard混杂规则,所以我运用了如下指令才解决
python3 redex.py -c ./config/default.config --produard-config path/proguard-project.txt --android-sdk-path path/Android/sdk path/your.apk -o output.apk
// 6. 最终对输出的 output.apk 包从头签名即可
- 最终进行优化效果比照
- 装置未启用 interdex pass 的 apk
// 获取运用 pid, 检查内存的运用状况,这儿需求比照 .dex mmap 行
adb shell ps | grep <运用包名>
adb shell dumpsys meminfo <运用 Pid>
- 请留意你的运用程序运用了多少内存 .dex mmap 行
- 履行上述一切进程并在启用 Interdex Pass 并运用你生成的发动class加载次序表的状况下从头运转 redex
- 装置启用 interdex 的 apk
- 发动运用程序并重复该进程以获取 meminfo
- 留意总内存运用量,尤其是 .dex mmap
我的前后比照方下,第一幅图(pid为19125)是未启用优化的,第二幅图(pid为21098)则是优化过的。
从内存运用状况来看,优化效果仍是比较明显的,我也核算了优化后比优化前,在发动速度上大约进步了150 ~ 200ms,这在低端机上,CPU和高速缓存不太多不太好的设备上,优化效果就更为明显,距离能够到达400 ~ 500ms,但在高端机上,距离就十分小了。由于高端机上高速缓存是许多的,相关于低端机而言,命中率更高,不会频频的从主存里读取数据。
当然,Redex的优化项许多,我这儿只研讨了dex类文件重排,这么看来,它算是相比照较简略落地的优化计划了,这个事例首要仍是让咱们知道优化的考虑办法。咱们从了解软件的履行进程和内存模型,再到对这些进程和模型进行剖析,从而得出优化计划,而且这些考虑不只仅能够运用在Android开发,底层原理相同,逻辑思维相通,能够运用到PC端,前端等等各种状况,这个才是咱们需求重点学习的,优化是成体系的,而不是从网络上搜出一堆潦草的,未经过任何考虑的优化计划。
4.2 进步使命调度优先级
在操作体系中有一个使命调度器负责依据使命的优先级、履行时刻、资源需求等要素,决议哪个使命能够取得CPU时刻片。既然是在操作体系层面的优化,那就离不开使命调度,接下来咱们针对这一个办法论来看看怎样进步中心线程的优先级。
咱们先了解一下 Linux 中的优先级原理,Linux 中的进程分为实时进程和一般进程这两类。实时进程一般经过 RTPRI(RealTimeRriority) 值来描绘优先级,取值规模是 0 到 99。一般进程一般运用 Nice 值来描绘进程的优先级,取值规模是 -20 到 19。可是为了架构设计上的一致,Linux 体系会将 Nice 对齐成 Prio 值,即 Nice 取 -20 时,该进程的 Prio 值为 0 ,此刻它的优先级仍然比任何一个实时进程的优先级都要低。经过检查源码得知,线程是一个精简化的进程,因此上述优先级规则也适用于线程。
咱们能够经过履行指令 adb shell top -H -n 1 -p <运用PID> , 来检查该运用进程的Nice值和Prio值,在输出界面中,PR列表明线程的Prio优先级值,NI列表明线程的Nice优先级值,详细如下图所示:
从上面数据能够看到,我的App进程PID为4843,所以主线程的Prio值和Nice值分别为10和 -10。 图中还标识了一个RenderThread
线程,它是烘托线程。咱们需求调整优先级的线程便是主线程和烘托线程。 由于这两个线程对任何运用来说都十分重要。从Android5 开端,主线程只负责布局文件的 measure 和 layout 作业,烘托的作业放到了烘托线程,这两个线程合作作业,才让咱们运用的界面能正常显现出来。所以经过进步这两个线程的优先级,便能让这两个线程取得更多的 CPU 时刻,页面显现的速度天然也就更快了。
在 Android 中只要部分底层中心进程才是实时进程,如 SurfaceFlinger、Audio 等进程,大部分的进程都是一般进程,咱们无法将一般进程调整成实时进程,也无法将实时进程调整成一般进程,只要操作体系有这个权限。但有一个例外,在 Root 手机中,将 /system 目录下的 build.prop 文件中的 sys.use_fifo_ui 字段修正成 1 ,就能将运用的主线程和烘托线程调整成实时进程,不过这需求 Root 设备才干操作,正常设备这个值都是 0,这个计划不具备通用性。由于App运用中的一切线程都归于一般进程的级别,所以针对线程优先级这一点,咱们仅有能操作的便是修正线程的 Nice 值了,但是咱们有两种办法来调整线程的 Nice 值。
调整线程优先级的办法
- Process.setThreadPriority(int priority) / Process.setThreadPriority(int pid,int priority);
- Thread.setPriority(int priority)。
第一种是Android 体系中供给的 API 接口。入参 pid 便是线程 id,也能够不传,会默许为当时线程,入参 priority 能够传 -20 到 19 之间的任何一个值,可是主张直接运用 Android 供给的 Priority 界说常量,这样咱们的代码具有更高的可读性,假如直接传咱们自界说的数字进去,不利于代码的理解。
体系常量 | nice值 | 运用场景 |
---|---|---|
Process.THREAD_PRIORITY_DEFAULT | 0 | 默许优先级 |
Process.THREAD_PRIORITY_LOWEST | 19 | 最低优先级 |
Process.THREAD_PRIORITY_BACKGROUND | 10 | 后台线程主张优先级 |
Process.THREAD_PRIORITY_LESS_FAVORABLE | 1 | 比默许略低 |
Process.THREAD_PRIORITY_MORE_FAVORABLE | -1 | 比默许略高 |
Process.THREAD_PRIORITY_FOREGROUND | -2 | 前台线程优先级 |
Process.THREAD_PRIORITY_DISPLAY | -4 | 显现线程主张优先级 |
Process.THREAD_PRIORITY_URGENT_DISPLAY | -8 | 显现线程的最高档别 |
Process.THREAD_PRIORITY_AUDIO | -16 | 音频线程主张优先级 |
Process.THREAD_PRIORITY_URGENT_AUDIO | -19 | 音频线程最高优先级 |
在不进行调整前,咱们主线程和烘托线程的默许 Nice 值为 -10,这其完成已算比较高优先级了,音频线程主张是最高档别优先级,由于假如音频线程优先级太低,就会呈现音频播映卡顿的状况。但不同的设备和Android渠道的默许配置或许不同,在较低版别的设备上(例如Android 6)主线程默许的 Nice值是0,烘托线程是-4。
第二种是 Java 供给的 API 接口,能设置的优先级较少,不太灵敏,而且由于体系的一个时序问题 Bug,在设置子线程的优先级时,或许由于子线程没创立成功而设置成了主线程的,会导致优先级设置反常,所以这儿主张运用第一种办法来设置线程的优先级,防止运用第二种办法。
找到需求调整优先级的线程
在前面咱们现已说过,咱们需求调整优先级的线程便是主线程和烘托线程。由于这两个线程是相互合作的,一同进行调整才干到达最优效果。 那咱们怎样去调整呢?
主线程的优先级调整,咱们直接在 Application 的 attach 生命周期中用Process.setThreadPriority(-19),将主线程设置为最高优先级即可。
至于 RenderThread 线程的调整,
咱们需求先知道烘托线程的id,然后调用 Process.setThreadPriority 进行设置就能够了。
那咱们怎样去找到烘托线程的线程id呢?下面咱们就来一同看下。
运用中线程的信息记载在 /proc/pid/task 的文件中,能够看到 task 文件中记载了当时运用的一切线程。以 2273 这个进程的数据为例,数据如下:
咱们接着检查该目录里线程的 stat 节点,就能详细检查到线程的详细信息,如 Name、pid 等等。2273 进程的主线程 id 便是 2273,它的stat第一个参数是 pid,第二个参数是线程姓名 (下图中的.winXXXXX.m 便是主线程姓名),详细如下:
所以咱们只需求遍历 task 这个文件,查找称号为 RenderThread
的线程,就能找到烘托线程的pid了,详细代码完结如下:
public static int getRenderThreadTid() {
File taskParent = new File("/proc/" + Process.myPid() + "/task/");
if (taskParent.isDirectory()) {
File[] taskFiles = taskParent.listFiles();
if (taskFiles != null) {
for (File taskFile : taskFiles) {
//读线程名
BufferedReader br = null;
String cpuRate = "";
try {
br = new BufferedReader(new FileReader(taskFile.getPath() + "/stat"), 100);
cpuRate = br.readLine();
br.close();
} catch (Throwable throwable) {}
if (!cpuRate.isEmpty()) {
String param[] = cpuRate.split(" ");
if (param.length < 2) {
continue;
}
String threadName = param[1];
//找到name为RenderThread的线程,则回来第0个数据便是 tid
if (threadName.equals("(RenderThread)")) {
return Integer.parseInt(param[0]);
}
}
}
}
}
return -1;
}
当咱们拿到烘托线程的 pid 后,相同调用 Process.setThreadPriority(pid,-19) 将烘托线程设置成最高优先级即可。
当然,咱们要进步的优先级线程并非只要这两个,咱们能够依据事务需求,来进步中心线程的优先级,一同下降其他非中心线程的优先级,该操作能够在线程池中经过线程工厂来一致调整。进步中心线程优先级,下降非中心线程优先级,两者合作运用,才干更高效地进步运用的速度。
好了,当然,由于常识有限,优化计划也不是很全面,或许还有其他更好的优化办法,欢迎咱们一同沟通,至此CPU优化现已悉数介绍完了,接下来我要开端 总(zhuang)结(bi)了。
二、小结
这篇文章从硬件到操作体系再到软件层,自底向上体系的介绍了怎样去做功用优化,其实不只仅是CPU优化能够这样去考虑解决,还有内存优化、包体积优化、网络优化等等方面都能够选用这套逻辑。回归功用优化的实质是合理且充沛运用硬件资源,让程序的体现更好 ,“充沛”便是将硬件的资源充沛发挥出来。 但要留意,充沛不一定是合理的。比方咱们一下敞开了几百个线程,CPU 被充沛发挥了,却这并不合理,由于此刻主线程无法取得足够的 CPU 资源,那合理便是所发挥出来的硬件资源能给程序体现带来正向的效果。但是功用优化也并不简略,想要做好功用优化,需求巨大的常识储备。
硬件层面:咱们需求对CPU的作业办法有了解,CPU的单核和多核等结构、寄存器、高速缓存、主存的设计等等。
体系层面:对操作体系有一定的了解也是必要的,这儿包括但不只限于进程的办理和调度、内存办理、虚拟内存等等,Andorid体系还有虚拟机、中心服务(ams、wms等)以及中心流程(发动、Activity、包装置)等等。
软件层面:这要求咱们了解自己开发的App,了解App运用缓存的状况,每个线程是干嘛的,运用都合理吗?是否由于如bitmap等不合理的运用或许不良的编程习气而占用许多内存等等状况。
其他方面:除上面三个根本的之外,咱们或许还需求把握更多常识,例如:汇编、编译器、逆向、各种编程言语,比方用 C++ 写代码就比用 Java 写代码运转更快,咱们能够经过将一些事务替换成 C++ 来进步功用,用 C 或许 C++ 编写模块做JNI调用等。
最终我把这一篇的常识,做一个常识结构总结。