10分钟学会安卓高阶代码优化技巧

?一、减少调用

A.缓存类、方法ID、字段IDethodId()和GetStaticMethodID()。对于GetFieldID()、GetMethodID()和GetStaticMethodID(),为特定类返回的ID不会在JVM进程的生存期内发生变化。但是,获取字段或方法的调用有时会需要在JVM中完成大量工作,因为字段和方法可能是从超类中继承而来的,这会让JVM向上遍历类层次结构来找到它们。由于ID对于特定类是相同的,因此您只需要查找一次,然后便可重复使用。同样,查找类对象的开销也很大,因此也应该缓存它们。使用缓存的字段ID

jclasscls=(*env)-GetObjectClass(env,allValues);

staticjfieldIDa=(*env)-GetFieldID(env,cls,a,I);

staticjfieldIDb=(*env)-GetFieldID(env,cls,b,I);

staticjfieldIDc=(*env)-GetFieldID(env,cls,c,I);

staticjfieldIDd=(*env)-GetFieldID(env,cls,d,I);

staticjfieldIDe=(*env)-GetFieldID(env,cls,e,I);

staticjfieldIDf=(*env)-GetFieldID(env,cls,f,I);

intsumValues2(JNIEnv*env,jobjectobj,jobjectallValues)

{

jintavalue=(*env)-GetIntField(env,allValues,a);

jintbvalue=(*env)-GetIntField(env,allValues,b);

jintcvalue=(*env)-GetIntField(env,allValues,c);

jintdvalue=(*env)-GetIntField(env,allValues,d);

jintevalue=(*env)-GetIntField(env,allValues,e);

jintfvalue=(*env)-GetIntField(env,allValues,f);

returnavalue+bvalue+cvalue+dvalue+evalue+fvalue;

}

技巧:查找并全局缓存常用的类、字段ID和方法ID。

B.减少本机代码与JAVA代码之间的调用。本机代码和Java代码之间的界限是由开发人员定义的。界限的选定会对应用程序的总体性能造成显著的影响。从Java代码中调用本机代码以及从本机代码调用Java代码的开销比普通的Java方法调用高很多。此外,这种越界操作会干扰JVM优化代码执行的能力。举例来说,随着Java代码与本机代码之间互操作的增加,实时编译器的效率会随之降低。经过测量,我们发现从Java代码调用本机代码要比普通调用多花5倍的时间。同样,从本机代码中调用Java代码也需要耗费大量的时间。

技巧:定义Java代码与本机代码之间的界限,最大限度地减少两者之间的互相调用。

因此,在设计Java代码与本机代码之间的界限时应该最大限度地减少两者之间的相互调用。消除不必要的越界调用,并且应该竭力在本机代码中弥补越界调用造成的成本损失。最大限度地减少越界调用的一个关键因素是确保数据处于Java/本机界限的正确一侧。如果数据未在正确的一侧,则另一侧访问数据的需求则会持续发起越界调用。

C.直接传值而不是对象的实例或回访。在调用某个方法时,您经常会在传递一个有多个字段的对象以及单独传递字段之间做出选择。在面向对象设计中,传递对象通常能提供较好的封装,因为对象字段的变化不需要改变方法签名。但是,对于JNI来说,本机代码必须通过一个或多个JNI调用返回到JVM以获取需要的各个字段的值。这些额外的调用会带来额外的开销,因为从本机代码过渡到Java代码要比普通方法调用开销更大。因此,对于JNI来说,本机代码从传递进来的对象中访问大量单独字段时会导致性能降低。

检查以下示例两个方法,不难发出那个性能高。第二个方法假定我们缓存了字段ID,两个方法版本:

intsumValues(JNIEnv*env,jobjectobj,jinta,jintb,jintc,jintd,jinte,jintf)

{

returna+b+c+d+e+f;

}

intsumValues2(JNIEnv*env,jobjectobj,jobjectallValues)

{

jintavalue=(*env)-GetIntField(env,allValues,a);

jintbvalue=(*env)-GetIntField(env,allValues,b);

jintcvalue=(*env)-GetIntField(env,allValues,c);

jintdvalue=(*env)-GetIntField(env,allValues,d);

jintevalue=(*env)-GetIntField(env,allValues,e);

jintfvalue=(*env)-GetIntField(env,allValues,f);

returnavalue+bvalue+cvalue+dvalue+evalue+fvalue;

}

技巧:如果可能,将各参数传递给JNI本机代码,以便本机代码回调JVM获取所需的数据。

二、及时释放资源

A.释放引用的JAVA对象。使用大量本地引用,而未通知JVMDeleteLocalRefJNI函数返回的任何对象都会创建本地引用。举例来说,当您调用GetObjectArrayElement()时,将返回对数组中对象的本地引用。每次调用GetObjectArrayElement()时都会为元素创建一个本地引用,并且直到本机代码运行完成时才会释放。数组越大,所创建的本地引用就越多。

技巧:当本机代码造成创建大量本地引用时,在各引用不再需要时删除它们。

B.未正确使用全局引用。创建全局引用时,JVM会将它添加到一个禁止垃圾收集的对象列表中。当本机返回时,它不仅会释放全局引用,应用程序还无法获取引用以便稍后释放它—因此,对象将会始终存在。不释放全局引用会造成各种问题,不仅因为它们会保持对象本身为活动状态,还因为它们会将通过该对象能接触到的所有对象都保持为活动状态。在某些情况下,这会显著加剧内存泄漏。

本机可以创建一些全局引用,以保证对象在不再需要时才会被垃圾收集器回收。常见的缺陷包括忘记删除已创建的全局引用,或者完全失去对它们的跟踪。考虑一个本机创建了全局引用,但是未删除它或将它存储在某处:

lostGlobalRef(JNIEnv*env,jobjectobj,jobjectkeepObj)

{

jobjectgref=(*env)-NewGlobalRef(env,keepObj);

}

技巧:始终跟踪全局引用,并确保不再需要对象时删除它们。

三、防止跨线程JNIEnv

A.跨线程使用JNIEnv。执行本机代码的线程使用JNIEnv发起JNI方法调用。但是,JNIEnv并不是仅仅用于分派所请求的方法。JNI规范规定每个JNIEnv对于线程来说都是本地的。JVM可以依赖于这一假设,将额外的线程本地信息存储在JNIEnv中。一个线程使用另一个线程中的JNIEnv会导致一些小bug和难以调试的崩溃问题。

线程可以调用通过JavaVM对象使用JNI调用接口的GetEnv()来获取JNIEnv。JavaVM对象本身可以通过使用JNIEnv方法调用JNIGetJavaVM()来获取,并且可以被缓存以及跨线程共享。缓存JavaVM对象的副本将允许任何能访问缓存对象的线程在必要时获取对它自己的JNIEnv访问。要实现最优性能,线程应该绕过JNIEnv,因为查找它有时会需要大量的工作。

技巧:仅在相关的单一线程中使用JNIEnv。

B.如何获得本线程的JNIEnv。JNIEnv指针会传给每个JAVA指向native方法。JNIEnv指针方便JNI环境与Native函数互相调用。你可以保留一个指向JNIEnv的指针,但只能在本线程中使用。如果需要在其它线程中使用,你必需调用AttachCurrentThread()方法获得当前线程的JNIEnv的指针。这样你的navtive线程等同于某个JAVA线程里包涵了navtive线程。navtive线程会在你调用了DetachCurrentThread()之后,结束自己。

JAVA的VM会在nativelibrary加载时调用JNI_OnLoad(注:你调用System.loadLibrary时)。JNI_Load同时会返回JNI的版本。此时就可以获得JavaVM的指针。

JNIEXPORTjintJNI_OnLoad(JavaVM*vm,void*reserved)

然后就可以通过它可以获得当前线程的

JNIEnvJavaVM*jvm;/*alreadyset*/

f()

{

JNIEnv*env;

(*jvm)-AttachCurrentThread(jvm,(void**)env,NULL);

.../*useenv*/

(jvm)-DetachCurrentThread();

}

四、争取使用数组

GetXXXArrayElements()和ReleaseXXXArrayElements()方法允许您请求任何元素。同样,GetPrimitiveArrayCritical()、ReleasePrimitiveArrayCritical()、GetStringCritical()和ReleaseStringCritical()允许您请求数组元素或字符串字节,以最大限度降低直接指向数组或字符串的可能性。这些方法的使用存在两个常见的缺陷。

其一,忘记在ReleaseXXX()方法调用中提供更改。即便使用Critical版本,也无法保证您能获得对数组或字符串的直接引用。一些JVM始终返回一个副本,并且在这些JVM中,如果您在ReleaseXXX()调用中指定了JNI_ABORT,或者忘记调用了ReleaseXXX(),则对数组的更改不会被复制回去。

举例来说,考虑以下代码:

voidmodifyArrayWithoutRelease(JNIEnv*env,jobjectobj,jarrayarr1)

{

jbooleanisCopy;

jbyte*buffer=(*env)-(*env)-GetByteArrayElements(env,arr1,isCopy);

if((*env)-ExceptionCheck(env))return;

buffer[0]=1;

}

技巧:不要忘记为每个GetXXX()使用模式0(复制回去并释放内存)调用ReleaseXXX()。在提供直接指向数组的指针的JVM上,该数组将被更新;但是,在返回副本的JVM上则不是如此。这会造成您的代码在一些JVM上能够正常运行,而在其他JVM上却会出错。您应该始终始终包括一个释放(release)调用以下所示:包括一个释放调用

voidmodifyArrayWithRelease(JNIEnv*env,jobjectobj,jarrayarr1)

{

jbooleanisCopy;

jbyte*buffer=(*env)-(*env)-GetByteArrayElements(env,arr1,isCopy);

if((*env)-ExceptionCheck(env))return;

buffer[0]=1;

(*env)-ReleaseByteArrayElements(env,arr1,buffer,JNI_COMMIT);

if((*env)-ExceptionCheck(env))return;

}

第二个缺陷是不注重规范对在GetXXXCritical()和ReleaseXXXCritical()之间执行的代码施加的限制。本机可能不会在这些方法之间发起任何调用,并且可能不会由于任何原因而阻塞。未重视这些限制会造成应用程序或JVM中出现间断性死锁。举例来说,以下代码看上去可能没有问题:

voidworkOnPrimitiveArray(JNIEnv*env,jobjectobj,jarrayarr1)

{

jbooleanisCopy;

jbyte*buffer=(*env)-GetPrimitiveArrayCritical(env,arr1,isCopy);

if((*env)-ExceptionCheck(env))return;

processBufferHelper(buffer);

(*env)-ReleasePrimitiveArrayCritical(env,arr1,buffer,0);

if((*env)-ExceptionCheck(env))return;

}

技巧:确保代码不会在GetXXXCritical()和ReleaseXXXCritical()调用之间发起任何JNI调用或由于任何原因出现阻塞。

但是,我们需要验证在调用processBufferHelper()时可以运行的所有代码都没有违反任何限制。这些限制适用于在Get和Release调用之间执行的所有代码,无论它是不是本机的一部分。

五、异常处理

本机能调用的许多JNI方法都会引起与执行线程相关的异常。当Java代码执行时,这些异常会造成执行流程发生变化,这样便会自动调用异常处理代码。当某个本机方法调用某个JNI方法时会出现异常,但检测异常并采用适当措施的工作将由本机来完成。一个常见的JNI缺陷是调用JNI方法而未在调用完成后测试异常。这会造成代码有大量漏洞以及程序崩溃。

举例来说,考虑调用GetFieldID()的代码,如果无法找到所请求的字段,则会出现NoSuchFieldError。如果本机代码继续运行而未检测异常,并使用它认为应该返回的字段ID,则会造成程序崩溃。举例来说,如果Java类经过修改,导致charField字段不再存在,则清单10中的代码可能会造成程序崩溃—而不是抛出一个NoSuchFieldError:

未能检测异常

jclassobjectClass;

jfieldIDfieldID;

jcharresult=0;

objectClass=(*env)-GetObjectClass(env,obj);

fieldID=(*env)-GetFieldID(env,objectClass,charField,C);

result=(*env)-GetCharField(env,obj,fieldID);

技巧:在发起可能会导致异常的JNI调用后始终检测异常。

添加异常检测代码要比在事后尝试调试崩溃简单很多。经常,您只需要检测是否出现了某个异常,如果是则立即返回Java代码以便抛出异常。然后,使用常规的Java异常处理流程处理它或者显示它。举例来说,将检测异常:

jclassobjectClass;

jfieldIDfieldID;

jcharresult=0;

objectClass=(*env)-GetObjectClass(env,obj);

fieldID=(*env)-GetFieldID(env,objectClass,charField,C);

if((*env)-ExceptionOccurred(env))

{

return;

}

result=(*env)-GetCharField(env,obj,fieldID);

不检测和清除异常会导致出现意外行为。您可以确定以下代码的问题吗?

fieldID=(*env)-GetFieldID(env,objectClass,charField,C);

if(fieldID==NULL)

{

fieldID=(*env)-GetFieldID(env,objectClass,charField,D);

}

return(*env)-GetIntField(env,obj,fieldID);

问题在于,尽管代码处理了初始GetFieldID()未返回字段ID的情况,但它并未清除此调用将设置的异常。因此,本机返回的结果会造成立即抛出一个异常。未检测返回值。许多JNI方法都通过返回值来指示调用成功与否。与未检测异常相似,这也存在一个缺陷,即代码未检测返回值却假定调用成功而继续运行。对于大多数JNI方法来说,它们都设置了返回值和异常状态,这样应用程序更可以通过检测异常状态或返回值来判断方法运行正常与否。

技巧:始终检测JNI方法的返回值,并包括用于处理错误的代码路径。

您可以确定以下代码的问题吗?

clazz=(*env)-FindClass(env,







































江苏白癜风医院
北京正规的治疗白癜风医院



转载请注明:http://www.gslnbdf.com/azyx/1510.html