最近看了谷歌官方关于Android性能最佳实践的部分,感觉应该要记下来才行。这里面有很多不看不知道的东西,我也为一些比较难懂的点增加了一些额外阅读的链接。刚总结完了JNI的小贴士,往后几天会陆续在这篇文章中把性能最佳实践这一部分补充完整。
后面还有安全最佳实践,权限最佳实践等部分,每一部分一篇文章,都会分开记录下来。
性能小贴士
这里介绍了一些小优化,可以提升app的整体性能。但是不一定会带来性能的飙升。选择正确的算法和数据结构是获得良好性能的首要任务。这里的小优化都是一些通用的编码实践,实现高效代码。
实现高效代码有两个基本规则:
不要重复造轮子
不要无谓分配内存
不要创建不必要的对象
创建对象总是要付出代价的。当我们创建越来越多的对象的时候,我们其实在强制地让垃圾回收器更加频繁地工作。这会造成类似于『打嗝』一样的波动,影响用户体验。
因此,要避免任何不必要的对象创建。以下是一些建议:
比如说,如果一个方法要返回String对象。Java内部实现的时候,String最后都是会被加到StringBuffer中的,因此不需要在代码中创建一个临时的StringBuffer或者StringBuilder对象来append字符串,直接用String就好了。
从字符串中截取子字符串的时候,不要创建新的String对象去存放原字符串的拷贝,可以选择直接return substring就好。
更加影响性能的一些点在于数组的使用上:
- int类型的array比Integer类型的array性能更好;同时,想方设法避免(int, int)这样多维数组的使用,将多维数组降成一维从而获取性能提升;
总结一下,尽量避免临时对象的创建,这样能减少垃圾回收器的运行次数,提升用户体验。
尽可能使用static而不是virtual
如果一个方法不需要访问这个类的成员,那么用static修饰这个方法。这样一来,大致上会有15%-20%的访问速度提升。
这里stackoverflow有个针对于这个问题的很好的解释
使用static final修饰常量
使用statis final修饰常量,能提高访问速度。
这里涉及到java的编译原理,做不了深入解释了。
要注意的是,这只对原始类型数据以及String常量有效。
避免在类的内部使用Getter/Setter方法
在C++等语言中,(i = getCount())这样的代码是很好的编码习惯,编译器会提升执行效率。
但是在安卓中,使用这样的方式是很糟糕的想法。在安卓中调用Virtual Method
差不多就像寻找成员变量一样消耗性能。在类中,直接访问成员变量而不要使用Getter方法。
没有JIT编译的情况下,直接访问成员的速度将3倍快于调用Getter方法。拥有JIT编译的情况下,直接访问成员变量变得和访问局部变量一样快捷,将达到7倍快于调用Getter方法。
使用增强for循环
除了ArrayList的遍历之外,增强for循环的性能是最好的。无论有没有JIT编译支持,在ArrayList的遍历上使用手写for循环,都将3倍快于增强for。
但是在其他容器上,增强for的性能在没有JIT的情况下快于手写for循环,在有JIT的情况下等同于手写for循环。
鉴于更少的代码,在除了ArrayList的地方,都使用增强for。
看下面的情形:
static class Foo {
int mSplat;
}
Foo[] mArray = ...
public void zero() {
int sum = 0;
for (int i = 0; i < mArray.length; ++i) {
sum += mArray[i].mSplat;
}
}
public void one() {
int sum = 0;
Foo[] localArray = mArray;
int len = localArray.length;
for (int i = 0; i < len; ++i) {
sum += localArray[i].mSplat;
}
}
public void two() {
int sum = 0;
for (Foo a : mArray) {
sum += a.mSplat;
}
}
zoo()方法是最慢的,使用了手写for循环,并且每次循环都要获取array长度;
one()方法快一些,不用每次都获取array长度了;
two()方法最快,使用了增强for(这里不是ArrayList)。
记住在非ArrayList的情况下用增强for。
使用默认包访问权限而不是私有private权限
考虑如下情形:
public class Foo {
private class Inner {
void stuff() {
Foo.this.doStuff(Foo.this.mValue);
}
}
private int mValue;
public void run() {
Inner in = new Inner();
mValue = 27;
in.stuff();
}
private void doStuff(int value) {
System.out.println("Value is " + value);
}
}
内部类Inner
访问了外部类的成员变量mValue
和成员方法run
,语法上是没有问题的,运行结果也正确。
但是,因为mValue
和run
方法被private
修饰,VM是禁止直接访问一个类的私有成员的,因为Foo
和
Inner
是两个不同的类。
为了能使内部类访问外部类的私有成员,编译器生成了如下两个方法:
/*package*/ static int Foo.access$100(Foo foo) {
return foo.mValue;
}
/*package*/ static void Foo.access$200(Foo foo, int value) {
foo.doStuff(value);
}
因此,无论是访问mValue
还是run
,都调用了其中一个方法。
在前面讲到了,在同一个类中要访问成员或者方法最好的方式是直接访问,而不是通过方法调用。因此,这里就是性能损耗的地方。
最好将private
去掉,也就是使用默认权限。但是这样一来其他同一包中的类就能访问这些成员了。
最好的办法就是不要写这样的代码了。
避免使用浮点
最佳实践的一条是,浮点操作在安卓设备上,2倍慢于整型操作。
速度层面上说,float和double的速度差不多,其实就是一样的慢!表述范围上来说,double是2倍于float的,那么在没有内存烦恼的台式电脑上,double是首选。
另外要注意的是,有些处理器支持硬件乘操作,但是不支持硬件除操作。因此,在设计有大量除或者模运算的时候,要特别注意性能问题。
尽量使用标准库
Java有的方法,就不要自己去实现了。Java库的代码是针对于虚拟机进行过优化的。System.arrayCopy()f案发9倍快于手写的循环拷贝。
关于性能要知道的地方
没有JIT编译支持的设备上,直接确定一个引用的类型会使调用更加高效。比如,定义HashMap的时候,直接定义为:
HashMap hashMap = new HashMap();
而相较于:
Map hashMap = new HashMap();
对hashMap调用方法,会有大概6%的性能提升。
性能优化之前
在进行优化之前,先测试并确定我们确实有性能问题,并且尽量量化这个性能问题,这样,我们才能在性能优化之后测试优化的效果。
Traceview工具很好用,但是要记住,使用Traceview将会关闭JIT功能。这意味着使用Traceview的时候性能的下降可能在JIT功能恢复的时候就能弥补。
JNI小贴士
JavaVM和JNIEnv
JNI定义了两种最关键的数据结构:
- JavaVM
- JNIEnv
在源码中,C部分定义为两个结构体:
- struct _JavaVM
- struct _JNIENV
在C++部分中,可以看做定义了两个类
- typedef _JNIENV JNIENV
- typedef _JavaVM JavaVM
数据结构的定义是这样的:
struct _JNIEnv {
const struct JNINativeInterface* functions;
#if define(__cplusplus)
jclass FindClass(const char* name) {
return functions->FindClass(this, name);
}
...
}
数据结构如果是C实现的代码:
JNIEnv即struct JAVANativeInterface*,在传递JNIEnv* env的时候,其实就传递了struct JAVANativeInterface** functions,从上面数据结构的定义看出,如果要在C代码中调用FindClass方法,必须先对env做一次解引用,才能得到指向functions具体函数的指针,即,(*env)->FindClass(this, name)。
而如果是C++实现的代码:
传递JNIENV* env,其实相当于传递了struct _JNIEnv* env。从上面的定义可以看出,C++部分对JNINativeFunctions做了封装。因此要调用函数,只需要用env直接调用即可,即,env->FindClass(this, name)
JNIEnv是threadlocal
的,因此不能在线程间进行传递。
如果一段代码无从获取JNIEnv,那么应该共享JavaVM,然后使用GetEnv来获取JNIEnv。
在jni.h中,C和C++部分的定于是不同的,通过typedef来区分。因此,如果一个header file会被两种语言包含,就不要把涉及JNIEnv的部分放进去。
线程
所有线程都是Linux线程,由内核管理。
线程可以通过pthread_create
另外创建,并通过AttachCurrentThread
或者AttachCurrentThreadAdDaemon
,来创建一个java.lang.Thread对象,添加到主线程组(ThreadGroup)中。
一个线程在attach
之前无法进行任何JNI操作。
对一个已经attach
的线程做attach
操作产生一个no
操作,可以看做操作被忽略。
调用AttachCurrentThread
的线程在退出前必须调用DetachCurrentThread
。也可以使用pthread_key_create
创建析构函数,然后在那里调用DetachCurrentThread
。
jclass,jmethodID,jfieldID
如果想在native代码中操作java对象的成员,通过如下方式进行:
- 通过FindClass方法获取对象引用
- 通过GetFieldID获取成员ID
- 通过相应的方式获取成员的值,例如GetIntField
找到成员ID和方法ID的过程就是通过字符串比对查找,如果考虑性能问题,可以通过下面的方式在类加载的时候缓存一份ID,已经查找过的就不再查找了。
private static native void nativeInit();
static {
nativeInit();
}
在native代码中实现这个方法,然后就会在每次类加载的时候自动执行ID的差找工作。
局部和全局引用
JNI中所有参数以及所有方法的返回都是局部引用。就算对象继续存在,只要方法已结束,局部引用就不可用了。
这适用于多有jobject的子类。
唯一保存局部引用的方式是通过NewGlobalRef
或NewWeakGlobalRef
方法来创建全局引用。
这通常用于缓存一个FindClass返回的jclass对象:
jclass localClass = env->FindClass("MyClass");
jclass globalClass = reinterpret_cast<jclass>(env->NewGlobalRef(localClass));
值得注意的是,对于同一个对象的引用的值可以是不同的。例如,连续对同一个对象调用NewGlobalRef
方法而返回值可以是不同的。因此,千万不要用==
来判断两个引用是否相等,而应该使用IsSameObject
方法。另外,不要用jobject当索引。
JNI的实现中只允许16个局部引用,如果要创建超过这个数量的引用,使用EnsureLocalCapacity/PushLocalFrame
来获取更多数量的预留空间。创建的引用应该使用DeleteLocalRef
来手动回收。
jfieldIDs
和jmethodIDs
,以及GetStringUTFChars
和 GetByteArrayElements
的返回值,都不能作为NewGlobalRef
的参数,因为他们是非object类型。
最后,任何通过AttachCurrentThread
添加的线程中的局部引用,都应该手动释放。
UTF-8和UTF-16字符串
Java语言使用的是UTF-16字符串。JNI提供了方法来兼容Modified UTF-8。这中编码的好处是我们可以继续使用C风格的\0
结尾的字符串,但是坏处是我们不能给JNI的方法传递任意UTF-8字符串,因为有可能出错。
不要忘记release
从get
方法获得的字符串。这些get
方法获取的字符串只当在release
调用之后才保证有效。
传递给NewStringUTF
方法的数据必须是Modified UTF-8
编码。常见的一个错误是,从一个文件或者网络读取字符串然后直接传递给NewStringUTF
方法而不进行过滤。
这里有一篇关于如果和在JNI中使用正确编码的文章,有兴趣的同学可以看一看~
原始数组
JNI中,Object数组只能一次访问一个条目,但是原始数据类型的数组能像在C中定义的那样读和写。
使用Get<PrimitiveType>ArrayElements
能获取到指向实际数据的指针或者开辟新的内存空间来拷贝数据。注意,只有当release
方法调用了,get
方法返回的数据才能被使用,也就是说,如果调用了get
方法但是没有调用release
方法,那一块堆内存就无法被其他指令使用了。因此必须release
每个get
到的数组,并且确保如果get
失败了,我们没有去release
一个NULL
指针。
异常
大多数的JNI函数都不能在有异常发生而没有解决的时候调用。
在异常发生的时候可以调用的JNI函数如下:
- DeleteGlobalRef
- DeleteLocalRef
- DeleteWeakGlobalRef
- ExceptionCheck
- ExceptionClear
- ExceptionDescribe
- ExceptionOccurred
- MonitorExit
- PopLocalFrame
- PushLocalFrame
- ReleaseArrayElements
- ReleasePrimitiveArrayCritical
- ReleaseStringChars
- ReleaseStringCritical
- ReleaseStringUTFChars
在调用诸如CallObjectMethod
的方法时,必须检查是否有异常发生。
使用ExceptionCheck
或ExceptionOccurred
检查是否有异常,使用ExceptionClear
清除异常。
JNI并没有提供操作Throwable
类的方法。因此,如果要获取异常信息的字符串,只能找到Throwable
类,找到
getMessage "()Ljava/lang/String;"
方法的methodID
,然后调用这个方法;如果返回了NON-NULL的值,则再使用GetStringUTFChars
获取一些信息来打印。
扩展检查
JNI的错误检查机制很薄弱,因此Android提供了一种叫做CheckJNI
的机制来让JavaVM和JNIEnv完成更多的错误检查工作。
检查的范围不限于:
- 数组:试图创建负数大小的数组
- 非可用指针:传递不可用的jarray/jobject/jstring给JNI函数,或者传递NULL给non-nullableJNI函数
- 字符串编码错误
等等…
如果我们使用的是虚拟机,这个模式默认是打开的。
如果是在ROOT设备上,可以使用如下方式重启运行环境并打开CheckJNI:
adb shell stop
adb shell setprop dalvik.vm.checkjni true
adb shell start
除了上述两种情况,使用如下方式:
adb shell setprop debug.checkjni 1
打开CheckJNI模式之后,在logcat中可以看到如下输出:
D AndroidRuntime: CheckJNI is ON
已经在运行的app不会受到任何影响,但是从这时起运行的app都会在CheckJNI模式下。
修改CheckJNI模式到任何其他值或者重启运行时环境都会关闭CheckJNI模式。关系CheckJNI之后的输出如下:
D Late-enabling CheckJNI
64位系统需要考虑的
Android当前被设计运行在32位平台,当然,也可以在64位平台编译运行,但不是当前的设计目的。大部分时间我们不需要担心这个情况,但当遇到在对象中往整数类型成员中储存指针的时候,要做一些处理。为了支持那些使用64位指针的的平台,我们需要把指针存放在long类型中,而不是int类型。
不支持的特性/向上兼容
除了下面这个特性,JNI1.6所有的特性Android都能支持:
- DefineClass 这个特性没有实现。由于Android不使用Java字节码或class文件,因此传递二进制class数据不会起作用。
向上兼容需要考虑的情况如下:
动态获取方法ID
直到Android 2.0(Eclair),在搜索
methodID
的过程中,$
符号不能被正确的转换为_00024
。解决方案可以是显式地注册这个方法,或者从内部类中移出来。分离线程(Detach)
直到Android 2.0(Eclair),不能使用
pthread_key_create
的析构函数来防止thread must be detached before exit
。(因为运行时环境也使用了pthread key destructor function
,会造成看谁先调用的情况)、弱全局引用(Weak global references)
直到Android 2.2 (Froyo),没有weak global references可以使用。
直到Android 4.0 (Ice Cream Sandwich),weak global references只能被传递给
NewLocalRef
,NewGlobalRef
,以及DeleteWeakGlobalRef
方法。Android 4.0 (Ice Cream Sandwich)之后, weak global references就能像其他引用一样被使用。
局部引用(Local references)
直到Android 4.0 (Ice Cream Sandwich),local references事实上是直接指针(direct pointers),看Quora上的问答什么是direct pointer和indirect pointer。4.0增加了indirect pointer特性是为了更好地支持垃圾回收。这里有JNI Reference变动的详细说明
通过GetObjectRefType确定引用类型
直到Android 4.0 (Ice Cream Sandwich),由于使用direct pointer,造成了无法使用GetObjectRefType获取引用类型。取而代之的,使用了一种先验式的机制,按照弱全局引用表(weak globals table),参数(arguments),局部引用表(locals table)以及全局引用表(globals table )这样的顺序查找这个引用的类型。因此,有可能我们使用GetObjectRefType去确定一个全局的jclass(global jclass)的类型,而这个全局的jclass就是那个参数jclass(把local jclass做成全局的),会返回JNILocalRefType,而不是JNIGlobalRefType。