在安卓平台上开发应用,通用的语言是 Java ,而对于从其它平台迁移到安卓的项目、产品,或者对于惯用 C/C++ 编程的开发人员来讲,会希望复用已有的 C/C++ 代码。安卓平台提供了复用 Native 代码的途径,也提供了编译 C 代码的环境和工具链: NDK 。 NDK 是一套工具链,有了它,在安卓上使用 C 语言成为可能。其实安卓原本是在 Linux 上套了个 Java 环境,要说不能用C 那才是不可思议的事儿,只是 Google 没完全开放而已(话说我到现在都在腹黑,为么不能让 C 程序员在安卓上活得自在些呢,简直是人为制造障碍)。
安卓平台上服用 C 代码有两种方式:
- JNI
- 原生 C 可执行程序
JNI 方式
JNI 原本是Java 提供的一种复用 C 代码的框架,安卓又对此进行了一些扩充,加了个 AIDL 用在服务框架中,搞了一套工具,在使用 Android.mk 编译时可以根据 AIDL 文件自动生成对应的 Java 代码并编译。
使用 JNI 主要是把 C 代码编译成动态库,在 Java 中调用。使用的步骤大概是这样的:
- 在 Java 代码中声明 native 方法
- 在 JNI(桥接 C 代码的这部分 C 代码称之为 JNI 层)层按照命名规则实现与 Java 层对应的本地方法
- 在 Java 层加载 C 动态库
JNI 方式的例子,如 Qt on Android ,Vitamio ,还有安卓框架本身中的一些例子,如 ServiceManager , android.util.Log 。
原生 C 可执行程序
安卓本是 Linux ,调用 Native 可执行程序是自然而然的途径。 Java 也提供了语言层面的支持,Runtime.exec() 函数就是干这个的。通过 exec() 启动进程,可以读取 Native 进程的标准输出,可以向 Native 进程的标准输入写入数据。
调用原生可执行程序的方式的例子, WifiManager 在连接无线网络时会通过控制接口和 wpa_supplicant (用于无线连接的 wpa_suppliacant ,是原生C可执行程序)通信传递诸如扫瞄接入点、选择网络、连接网络等指令。
还有,有些安卓系统机顶盒上自带的宽带拨号(PPPoE)程序,也是 C 可执行程序, Java 层的 PPPoEService 最终通过调用pppd/ppppoe 这样一些程序来执行实际的拨号过程。
还有,我们常说的 root ,其实也是通过调用一个叫 su 的程序来实现的。
我们在实际开发中也可以这么用,比如想看某个目录下都有什么文件,可以直接调用 ls 命令,读取它的标准输出。
Java 和 C 程序通信问题
我们在安卓上通过 Java 调用 C 代码时还会遇到进程或线程间通信的问题。
这里专门说下 Java 进程和 C 进程、Java 进程和 C 共享库的通信问题。
有时我们的需求很简单,阻塞式调用 C 代码,拿到计算结果就达到目的了。比如你通过 JNI 调用 C 共享库的一个 Hash 函数,又比如你通过 Runtime.exec() 调用一个 Native 可执行程序来计算 Hash 读取其标准输出获得结果。这些场景足够简单,你可以不考虑 Java 和 C 共享库或者 C Native 可执行程序间的通信,反正是一锤子买卖也没啥状态要维护的。
但是还有一些复杂的应用,我们必须在 Java 和 C 之间建立一种长期的通信机制。
还是举无线连接的例子好了,有兴趣的读者可以浏览 wpa_supplicant 的源代码,它提供了基于 dbus、unix domain socket 两种方式的控制接口。
其实 Linux 常用的进程间通信机制,如 管道pipe 、socket 、信号,也都可以用于 Java 层和 C 层的通信,而且安卓框架就这么用了。举个例子,我们都很熟悉的安卓世界的第一个进程 Zygote(实际是 app_process ,启动后更名为 Zygote ),有一个功能就是启动 Java 进程,当我们要启动一个 APK ,该 APK 的进程几经辗转最终是由 Zygote 启动的(可以进程间共享 Java 类库、C 共享库,大大节省资源也加快进程启动过程),而 Zygote 正是通过 socket 接受命令的。
再举个信号的例子,我们看到很多进程管理类的应用,其实都是使用 android.os.Process 来杀进程,而 Process.kill() ,实际上就是给目标进程发送了一个信号,非常标准的 Linux 机制。
还有一个常用的 Java 和 C Native 可执行程序进程间通信的机制:标准输入输出。我们可以向一个进程的标准输入写入数据,也可以从它的标准输出读取数据。那么我们就可以定义一套控制协议用来通信。
像管道 pipe、socket 既可以用于进程间通信(Java 和 C Native 可执行程序),也可以用于进程内通信( Java 和 C 共享库)。那么什么场景下我们会这么用呢?前面的无线连接服务程序 wpa_supplicant 可以给我们一些提示。
假如我们用 C 实现了一个服务,而 Java 层需要经常访问这个服务并且需要得到反馈,那么就可以这么干。
试着说一个场景,我们用 C 实现了一个可以支持并行下载的模块(单线程 select 模型),而 Java 层会频繁地下载图片(安卓上 Java 层貌似没有简单易用占用资源又少的http 下载库),比如一个云相册类的应用或视频类应用,我们就可以把下载动作委托给 C 进程。
进程间通信,安卓框架中还有一个在 Linux IPC 框架之上扩展出来的新框架 Binder ,很多 Native 系统服务使用 Binder 框架,比如 AudioFlinger ,我们在 Java 层调用 AudioManager 设置音量的功能时,最终就是通过 Binder 框架使用了 Native 系统服务 AudioFlinger 的功能,是典型的跨进程调用,但是作为 Java 程序,完全不用关心这个。
想详细了解 Binder 的读者,可以进一步学习。这里提一下 Binder 的限制:不是所有 C 程序都可以使用 Binder 来注册服务,只有被授权的服务如 media.player ,media.camera 之类的 Native 系统服务才可以,如果你是系统开发,可以通过修改授权列表(通过 UID 控制)来允许你的 C 程序注册服务,而作为应用开发者,就别妄想了,还是考虑使用管道pipe、socket 或者标准输入输出稳妥些。
编译 C 可执行程序
为安卓平台编译 C 可执行程序,有几个方法:
- 使用 NDK ,使用 Android.mk
- 手动使用预编译的 gcc 编译
- 使用 Qt 5.2 ,参考《Windows下Qt 5.2 for Android开发入门》和《Windows下Qt for Android 编译安卓C语言可执行程序》
在 APK 中集成和使用 C 可执行程序
怎样在 APK 中集成和使用一个 C 可执行程序呢?遵循下列步骤即可:
- 可以把可执行程序放在 assets 目录下,这样打包成 APK 时会自动打包
- APK 运行时访问 assets 文件夹内的资源,释放可执行程序,添加可执行权限
- 使用 Runtime.exec() 启动可执行程序
好啦,总算说了个大概,希望对你有所帮助。