最近看了谷歌官方关于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。