一,JNI优缺点

JNI 编程在软件开发中运用广泛,其优势可以归结为以下几点:

  • 利用 native code 的平台相关性,在平台相关的编程中彰显优势。
  • 对 native code 的代码重用。
  • native code 底层操作,更加高效。

然而任何事物都具有两面性,JNI 编程也同样如此。程序员在使用 JNI 时应当认识到 JNI 编程中如下的几点弊端,扬长避短,才可以写出更加完善、高性能的代码:

  • 从 Java 环境到 native code 的上下文切换耗时、低效。
  • JNI 编程,如果操作不当,可能引起 Java 虚拟机的崩溃。
  • JNI 编程,如果操作不当,可能引起内存泄漏。

二,JNI Tips

概述自官方JNI Tips

1. JavaVM和JNIEnv

每个进程只有一个JavaVM,每个线程都会有一个JNIEnv,大部分JNIAPI通过JNIEnv调用

2. 多线程

JNIEnv不能跨进程使用,JavaVM里有每个线程对应JNIEnv的table,如果是native线程需要使用JNIEnv,需要调用AttachCurrentThread让JavaVM获取对应线程的JNIEnv才能使用。线程退出也要记得调用DetachCurrentThread.

3. jclass, jmethodID, and jfieldID

对jclass、jmethodId、jfieldId这些值,可以缓存起来,跨线程使用(它们只是一个函数地址或成员地址)。

4. Local and Global Reference

Local Reference会在函数退出时销毁,假如一个变量要跨进程使用,则需要使用Global Reference的方式,做全局引用,但同时也要注意释放。

5. UTF-8和UTF-16

Java语言本身用的是UTF-16,而native需要utf-8则要使用转换接口。性能上,如果native直接使用UTF-16会更快,而UTF-8则需要转换和分配内存。

6. Exception处理

先用HasException判断前一个JNI函数是否JavaVM抛出了Exception,然后可以获取stack信息并处理。

三,SDK实践中的几点思考

1. Java对象与Native对象关系的设计

在项目中,需要从服务器获取照片列表,比如在Java中调用一个获取照片的接口:

Photo[] GetPhotos()

这个接口实际上是通过JNI调用,底层native代码会发协议,更新数据库做增量合并等等操作,最后返回照片列表到Java层。Java层和native层都会有一个Photo对象,这2个对象的关系设计有2种方式:

1) 复制方式

调用GetPhotos接口后返回数据时,把底层的C++对象转换成Java对象,再返回出来。在这种方式下,Java对象拥有所有成员,转换后C++对象即销毁,后续访问只有Java对象。

在设计Photo这种属性多,数据更新频率低,对象较简单的时候,我们采取复制方式,这样做的好处:

  1. 避免属性读取时每次走JNI的消耗
  2. GC计算内存体积能更准确
  3. 设计较为简单

这种方式下的劣势:

  1. 每次调用接口的数据转换有一定的一次性耗时
  2. 底层数据库更新后,再次读取数据无法获得新数据,只能重新调用接口复制

2) 引用方式

在弱引用的方式,Java对象本身只有一个成员,就是C++对象的指针,它只是一个外壳。所有对Photo成员的访问都是通过JNI访问到C++对象的成员,比如:

String getPhotoName() { nativeGetPhotoName(mPhotoPtr); }

在设计Transfer对象时,由于Transfer对象本身属性不多,但是更新频率高(比如进度不停更新,内部状态不停变化),对象操作也较为复杂,所以我们采取引用方式,这样做的好处:

  1. 避免每次创建新的对象
  2. Java对象的状态始终和native对象的状态一致

这种方式下的劣势:

  1. 每次访问接口有一定JNI耗时
  2. 设计稍麻烦,要维护好两个对象的生命周期关系

2. Java对象和Native对象生命周期管理

在引用方式下,Java对象要强引用C++对象,但是由于C++对象也需要在部分情况下通知获调用Java对象的接口,这就需要C++对象弱引用一份Java对象。

在GC情况下,Java对象会被上层销毁,此时析构的时候会调用JNI接口,通知底层C++对象引用计数减一。

3. 多线程下FindClass

调用FindClass获取对应类名的jclass,一般情况下是没有问题的,但是在native线程获取,可能会出现获取失败的问题。

在Android文档中,有叙述了部分为何多线程FindClass失败的原因:

  1. proguard打开后,不能把自定义类名给混淆
  2. 在native线程FindClass可能会失败,这是由于Attach出来的JNIEnv与主线程的JNIEnv context不一致导致,调用FindClass时无法通过上下文找到对应名字的Class

解决这个问题的方案:

  1. 确定proguard没有混淆对应类
  2. 在JNI_Onload中调用FindClass,把对应jclass获取并缓存起来

4. Java和Native职责界线划分

设计SDK接口时,是否Native可以处理一切问题,Java只需要做一层外壳就可以了?

假如我们都暴露最原始的JNI native接口,那这个SDK的接口是十分难以使用的。在我们设计的时候,SDK的Java层的接口除了对Native的JNI封装外,还做了更多事情,比如:

  1. 建立Java类,对JNI接口做面向对象的封装
  2. 添加协议的回调封装,类似于volley的Request和Response
  3. 使用LruCache对本地获取的缩略图Bitmap做缓存

Java层可以对Native接口的封装,面向对象设计,和利用android的一些现有框架辅助做一些通用事情。

而Native层则负责底层的复杂计算逻辑,最后暴露一个简单的API接口到JNI层。



评论需要翻墙 for disqus