一本比较轻量的书,不到一天就可以看完,优化覆盖的面也挺广,会涉及到字节码汇编的一些知识,还有NDK的入门;不足之处里面的API会显得很老,没有收录到最新的优化技巧,提到的点需要自己去深入研究才会更有收获。

[读书笔记] Android应用性能优化

Java代码优化

斐波那契数列优化

0,1,1,2,3,5,8…由前面两个数相加

从递归到迭代,从迭代到减少迭代次数,再到缓存以多次运算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static long computeIterativelyFaster(int n) {
if(n > 1) {
long a, b = 1;
n--;
a = n & 1;
n /= 2;
while(n-- > 0) {
a += b;
b += a;
}
return b;
}
return n;
}

BigInteger的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static BigInteger computeIterativelyFaster(int n) {
if(n > 1) {
BigInteger a, b = BigInteger.ONE;
n--;
a = BigInteger.valueOf(n & 1);
n /= 2;
while(n-- > 0) {
// 不断创建新对象,很影响性能
a = a.add(b);
b = b.add(a);
}
return b;
}
return (n == 0) ? BigIngeter.ZERO : BigInteger.ONE;
}

斐波那契Q-矩阵公式:
$$
F_{2n-1} = F_{n}^{2} + F_{n-1}^2\
F_{2n} = (2F_{n-1}+F_{n})*F_{n}
$$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static long computeRecursively(int n) {
if(n > 1) {
int m = (n / 2) + (n & 1);
int fm = computeRecursively(m);
int fm_1 = computeRecursively(m - 1);
if((n & 1) == 1) {
return fm * fm + fm_1 * fm_1;
} else {
return (2 * fm_1 + fm) * fm;
}

}
return (n == 0) ? 0 : 1;
}

SQLite

字符串的拼接优化

SQLiteStatement实现只编译一次

使用ContentValues灵活实现

使用事务

FTS全文检索

高效使用内存

内存的使用主要涉及的两个因素:

  1. 物理内存大小
  2. 虚拟内存交换能力

数据类型的长度——字节码层面

两个64位证书相加的字节码

1
2
448: e0944002 adds	r4, r4, r2
44c: e0a55003 adc r5, r5, r3

两个int型则只需要一条字节码

1
16c8: e0810000 add	r0, r1, r0

排序的实现

Array.sort()对于不同的数据类型是采取不同的算法来进行排序

内存泄漏-StrictMode检测

主要有ThreadPolicy和VmPolicy检测,会记录到日志中,需要我们进一步分析日志

多线程和同步

使用线程Thread和AsyncTask

Handler的机制使用

并发类

Sychronized和volatile关键字

使用线程池,并发缓存ConcurrentHashMap

1
2
ExecutorService executorService = Executors.newFixedThreadPool(proc + 2);
executorService.submit();

一个应用默认启动的线程

  • main
  • HeapWorker(执行finalize函数和引用对象清理)
  • GC(垃圾回收)
  • Signal Catcher(捕捉Linux信号进行处理)
  • JDWP(Java Debug wire Protocol,调试协议服务)
  • Compiler(JIT即时编译)
  • Binder Thread #1(Binder通信)
  • Binder Thread #2

图形与UI优化

  • 嵌套过深:使用<include>、<merge>、<viewstub>;扁平化布局

工具:hierarchyviewer

  • 显示当前页面的树结构
  • 对测量、布局和绘制三个步骤使用的时间进行统计

工具:layoutopt

针对单个xml文件给出建议,如下

1
The root-level <FrameLayout/> can be replaced with <merge/>

电池续航

影响电量的主要功能:

  • 屏幕:WakeLock
  • 执行代码:广播,大量运算,定时唤醒
  • 数据传输:网络,Wifi
  • 位置服务:网络,GPS
  • 传感器:加速度计,陀螺仪
  • 渲染图像:GPU渲染,看不见我们一定要控制停止其渲染

性能评测和剖析

时间测量

1
2
3
4
5
6
System.currentTimeMillis
System.nanoTime //精度取决于系统的精度
Debug.threadCpuTimenanos //多线程会将每个线程的时间都加在一起
SystemClock.currentThreadTimeMillis
SystemClock.elapsedRealtime
SystemClock.uptimeMillis

方法跟踪

1
2
3
4
5
Debug.class
startMethodTracing()
stratMethodTracing(String traceName) // 记录的文件名
startMethodTracing(String traceName, int bufferSize)
startMethodTracing(String tarceName, int bufferSize, int flags)

TraceView工具

在SDK的tools目录下

1
tarceview awesometrace.tarce // 启动Traceview工具

包含了所有的函数调用,以及调用执行时间和调用次数等信息;

本地方法调用,利用QEMU模拟器跟踪

  • 通过-trace选项启动模拟器emulator -trace mytrace -avd myavd
  • 调用Debug.startNativeTracing()和Debug.stopNativeTracing()来进行跟踪,也可以使用F9来启动
  • 在AVD中会生成QEMU模拟器跟踪文件

使用tracedmdump命令

定义在build/envsetup.sh文件中,需要下载Android源码才能使用

在AVD的目录下运行tracedmdump mytrace来创建traceview可以打开的跟踪文件

NDK入门和进阶

NDK是为应用开发本地代码的一套工具

HelloWorld

简单实现一个Java调用C/C++的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 1. Java中声明本地方法,不限定一定是static,也不限定一定是基本类型
public static native long recursiveNative(int n);

// 2. JNI粘合层,生成JNI头文件和JNI的C源文件
cd /目录;
mkdir jni;
javah -classpath bin -jni -d jni com.apress.proandroid.Fibonacci;

// 3. 实现自己的头文件和实现函数,粘合层调用本地方法实现操作,为了方便本地函数的复用

// 4. 创建Makefile
// 构建文件
Application.mk(可选)
APP_ABI := armeabi //针对某个处理器生成一个版本的库
// 针对整个应用的共同变量
Android.mk
LOCAL_PATH := $(call my-dir) // Android.md的路径
include $(CLEAR_VARS) // 清除LOCAL_PATH以外的所有LOCAL_XX变量,防出错
LOCAL_MODULE := fibonacci // 生成库的名称
LOCAL_SRC_FILES := com_apress_proandroid_Fibonacci.c // JNI的c文件、我们实现的c文件也要添加
include $(BUILD_SHARED_LIBRARY) // 构件库的规则文件,这里是共享库的规则,还有静态库

// 5. 编译本地库,生成so库
ndk-build

// 6.加载调用,在适合的时候进行加载本地库,然后java调用Native方法即可,加载失败要自行处理
System.loadLibrary("fibonacci");

JNI粘合层,生成JNI的头文件和JNI的C源文件

1
2
3
4
5
6
7
8
9
// 头文件中的方法声明
JNIEXPORT jlong JNICALL
Java_com_apress_proandroid_Fibonacci_recursiveNative(JNIEnv *, jclass, jint);

// 源文件中的实现
jlong JNICALL
Java_com_apress_proandroid_Fibonacci_recursiveNative(JNIEnv *env, jclass clazz, jint n) {
return 0
}

参数说明:

JNIEnv指针:JNIEnv的对象是JNI环境本身,他可以与虚拟机交互

jclass/jobject:方法为静态的则是jclass,否则就是jobject类型

JNI访问Java域、调用方法

主要通过一个id来进行访问调用,这个id可以通过env来进行获取

1
2
3
4
5
6
7
// 获取ID
jfiledID someIntegerID = (*env)->GetStaticFieldID(env, clazz, "someInteger", "I");
jfiledID helloFromJNIId = (*env)->GetStaticMethodID(env, clazz, "helloFromJNI", " ()V");

// 访问调用
(*env)->SetStaticIntField(env, clazz, helloFromJNIId);
(*env)->ClaaStaticVoidMethod(env, clazz, someIntegerId);

实现本地Activity

  1. 通过实现NativeActivity类

    manifest.xml:hasCode可以关掉了;指定本地库和方法

    1
    2
    3
    4
    <meta-data android:name"android.app.lib_name"
    android:value="myapp"/>
    <meta-data android:name"android.app.func_name"
    android:value="ANativeActivity_onCreate" />

    功能通过引入相关的头文件实现

    void android mian(struct android_app* state)开始工作

  2. 实现纯本地Activity

    通过自己实现生命周期的回调(即是native_app_glue模块实现的功能)

NDK支持编译汇编代码

很少使用上,太具有系统架构针对性了,需要我们学习指令,x86和ARM汇编的实现都不一样。

书中有汇编的并行实现功能

JNI和原生实现的区别(性能)

原生是编译成Dalvik字节码,JIT优化带来了很大的性能优化

本地函数是编译成汇编代码,更加紧凑和体积小,但是不一定比原生的快

需要注意我们再Java和本地空间的过渡时间消耗

字符串的性能问题

Java的字符串要提供给本地方法使用的话,必须进行转码

Java使用16位的Unicode字符来进行编码

C/C++则大部分使用char *来做字符串用

C扩展性能优化

内置函数——build-in

也被称为内联intrinsics,是有编译器内部进行特别处理的函数,可以在保持代码通用性的同时,充分利用某些平台上特有优化

直接在调用处用实现替换调用,或者在函数定义前加上inline关键字,小心代码变臃肿

拆循环,拆成switch/case;效果是不稳定的,不要测试才知道,假如循环体太大,对指令缓存也只是负面影响。

向量指令

在CPU支持SIMD指令的情况下,可以大大提高性能;不支持时,则会生成很低性能代码

(vector_size (16))这是一个4个整数的向量

预加载内存

1
2
3
int* dst, const int* src;
__builtin_prefetch(dst + 8, 1, 0);//准备写
__builtin_prefetch(src + 8, 0, 0);//准备读

小心使用,不是必须加载进来是会增加缓存的压力,导致性能降低的

渲染、RenderScript

OpenGL ES

书中这节属于图形这一章,在这里描述会更好一点

这是一个渲染库

纹理压缩:

未压缩的256*256的RGBA8888就会占用256KB的内存

ETC1是一个创建纹理的工具(etc1 tool):舍弃透明度,每个像素使用4位。

Mipmap

通过提供多层次细节的纹理,解决在小像素中显示很大的图片,避免浪费内存。

通过派生出很多种尺寸的图片,所以Mipmap包会比原始图像多用掉33%的存储。

RenderScript

针对高性能3D渲染和计算操作的框架

1
2
3
4
5
6
7
脚本文件
#pragma version(1)
#pragma rs java_package_name(com.apress.proandrdoi.ch9)

void hello_world(){
rsDebug("Hello World", 0);
}

会自动生成:

  • ScriptC_helloworld.java
  • helloworld.d
  • helloworld.bc
1
2
3
4
5
6
7
8
9
// 使用
RenderScript rs = RenderScript.create(this);
// 创建脚本
ScriptC_helloworldScipt helloworldScript = new ScriptC_helloworld(rs, getResources(), R.raw.helloworld);
// 执行,包含反射操作
helloworldScript.invoke_hello_world();

// 还可以使用RSSurfaceView来绑定渲染
使用RenderScriptGL.bindRootScript来进行绑定脚本

总结

这本书里面使用的代码有点老旧了,一些最新优化方法没有提到,但是他不拘细节的描述,可以在时间很短的情况下看完这本书,这些优化方法还需要自己进一步地去进行深究;这本书的一个好处是对检测的工具也有比较广的介绍,并有提到一些比较冷门的技术点,也是开阔视野的一个不错选择。一句总结:广泛且底层,却不拘细节。

另外这本书十分执着于字节码和汇编代码上面的影响,也带给了自身不少启发。

工具的复习:TraceView、hierarchyviewer、layoutopt

NDK的粗略了解:使用步骤,性能的区别