NDK篇 - JNI & NDK 初探

前几天一直在忙项目,所以断更了两天,今天继续写。进入到 NDK 篇了,先来了解下 NDK 与 JNI,后面的文章将带大家来交叉编译一些成熟的开源项目。

 

目录:

  1. 什么是 JNI
  2. 什么是 NDK
  3. Android 7种 cpu 架构
  4. Android.mk 使用说明
  5. Application.mk 使用说明
  6. 交叉编译
  7. CMake 开发 JNI

 

 

1. 什么是 JNI

 

  • 1.1 简介

JNI 是 Java Native Interface 的缩写,它提供了一些 API 让 Java 与其他语言进行交互 (主要是 c/c++)。从 Java 1.1 开始,JNI 成为标准 Java 平台的一部分,它允许 Java 与其他语言代码进行交互。使用 Java 与本地已编译的代码交互,通常会丧失平台的可移植性,但是有些情况下,这是可以接受的,比如:使用旧的库、与硬件和操作系统进行交互、或者为了提高程序的性能。JNI 标准至少可以保证本地代码可以运行在任何 Java 虚拟机环境。

 

  • 1.2 JNI 的缺点

(1) 程序不再跨平台,要想跨平台,那么本地代码必须在该平台上重新编译,才能正常在该平台的 JVM 上运行。

(2) 程序不再绝对安全,本地代码的使用不当可能导致整个系统崩溃。一个使用原则是:应该让本地代码集中在少数的几个类当中,这样就降低了 Java 与本地代码的耦合性。

 

  • 1.3 JNI 的适用场景

(1) 程序中需要使用 Java API 不提供的特殊环境才有的特征。

(2) 需要访问一些已有的本地库。

(3) 部分代码对效率要求非常高,比如图形渲染、算法计算。

 

  • 1.4 JNI 实现步骤

JNI 中一共有三个角色:c/c++ 本地代码,本地方法接口,Java 业务类。

JNI 实现步骤:

  • 1. 在 Java 中先声明一个 native 方法。
  • 2. 编译 Java 源文件 java c 得到 .class 文件。
  • 3. 通过 javah -jni 命令导出 JNI 的 .h 头文件。
  • 4. 使用 Java 需要交互的本地代码,实现在 Java 中声明的 Native 方法 (如果 Java 需要与 c++ 交互,那么就用 c++ 实现 Java 的 Native 方法)。
  • 5. 将本地代码编译成动态库 (Windows 系统下是 .dll 文件,如果是 Linux 系统下是 .so 文件,如果是 Mac 系统下是 .jnilib)。
  • 6. 通过 Java 命令执行 Java 程序,最终实现 Java 调用本地代码。

JNI 例子:JNI详解------完整Demo

 

 

  • 1.5 API 文档

由于篇幅有限,这边不细说 API 了,查看 API 请移步:

英文: https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/jniTOC.html

中文: https://blog.csdn.net/yishifu/article/details/52180448

 

 

2. 什么是 NDK

 

  • 2.1 简介

NDK 全拼是:Native Develop Kit。Android NDK 是一套允许你使用本地代码语言 (例如 c/c++) 实现部分应用的工具集。在开发某些类型应用时,这有助于你重复使用以这些语言编写的代码库。

大家都知道,Android 开发语言是 Java,不过我们也知道,Android 是基于 Linux 的,其核心库很多都是 c/c++ 的,比如  Webkit 等。那么 NDK 的作用,就是 Google 为了提供给开发者一个在 Java 中调用 c/c++ 代码的一个工具集。NDK 其实就是一个交叉工作链,包含了 Android 上的一些库文件,然后 NDK 为了方便使用,提供了一些脚本,使得更容易的编译 c/c++ 代码。一般情况下用 NDK 工具把 c/c++ 编译为 .so 文件,然后在 Java 中调用。

 

  • 2.2 为什么要使用 NDK
  • 1. 在平台之间移植应用。
  • 2. 重复使用现在库,或者提供其自己的库重复使用。
  • 3. 在某些情况下提性能,特别是像游戏这种计算密集型应用。
  • 4. 使用第三方库,现在许多第三方库都是由 c/c++ 库编写的,比如 Ffmpeg。
  • 5. 不依赖于 Dalvik Java 虚拟机的设计。
  • 6. 代码的保护。由于 APK 的 Java 层代码很容易被反编译,而 c/c++ 库反编译难度大。

 

  • 2.3 NDK 文档

https://developer.android.google.cn/ndk/guides/

 

  • 2.4 demo

来一个小 demo 理解下 NDK 的流程。

Java 代码:

package ai.ctxc.core;

/**
 * Local function class.
 *
 * @author kuang on 2018/09/06.
 */
public final class NativeFunctions implements NoProGuard {

    static {
        System.loadLibrary("CtxcWallet_Native");
    }

    public static native String getDBKey();
}

主 model 下的 jni 目录中的 main.cpp 文件:

#include "utils.h"
#include "md5.h"
#include <iostream>
#include <string.h>
#include <malloc.h>

#define NELEM(x) ((int) (sizeof(x) / sizeof((x)[0])))

extern "C" {

    const char *APP_SIGNATURE = "xxxxxxxxxxx11111111111";

    JNIEXPORT jstring JNICALL Java_ai_ctxc_core_NativeFunctions_getDBKey(JNIEnv *env, jobject jobj) {
        int len = strlen(APP_SIGNATURE);
        int seeds[len];
        for (int i = 0; i < len; ++i) {
            seeds[i] = APP_SIGNATURE[i];
        }
        int seed = 0;
        for (int i = 0; i < NELEM(seeds); i++) {
            if (i % 2 == 0)
        	    seed &= seeds[i];
        	else
        	    seed ^= seeds[i];
        }
        std::string salt = "" + seed;
	    MD5 md5(salt);
	    std::string result = md5.md5();
	    const char *p = result.c_str();
	    return string2Jstring(env, p);
    }
}

和 JNI 的方式一样,extern "C" 是允许在 c++ 代码中调用 c 代码。Java_ai_ctxc_core_NativeFunctions_getDBKey 是本地函数与 Java 函数的一个映射。

Android.mk:

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := CtxcWallet_Native
LOCAL_CFLAGS = -DFIXED_POINT -DUSE_KISS_FFT -DEXPORT="" -UHAVE_CONFIG_H
LOCAL_C_INCLUDES := $(LOCAL_PATH)/
LOCAL_SRC_FILES := $(LOCAL_PATH)/md5.cpp \
                   $(LOCAL_PATH)/utils.cpp \
                   $(LOCAL_PATH)/main.cpp

LOCAL_LDLIBS:= -llog
LOCAL_CPPFLAGS := -std=c++11 -lpthread -Wno-deprecated -lm -Wwrite-strings
include $(BUILD_SHARED_LIBRARY)

Application.mk:

APP_STL := gnustl_static
APP_ABI := armeabi armeabi-v7a arm64-v8a x86 x86_64

执行 ndk-build 命令生成动态库:

最后执行调用 NativeFunctions.getDBKey() 就可以拿到 NDK 中方法的数据了。

 

 

3. Android 7种 cpu 架构

在上面的例子中 Application.mk 定义了这些内容:APP_ABI := armeabi armeabi-v7a arm64-v8a x86 x86_64,下面我们来了解下。

NDK 开发时会涉及到 cpu 架构的适配,不同的机器上可能会有不同的 cpu 架构,也就是说翻译到机器上使用的规则不一样,Android 上有7种 cpu 架构: 

  • 1. armeabi  
  • 2. armeabi-v7a  
  • 3. arm64-v8a  
  • 4. x86  
  • 5. x86_64  
  • 6. MIPS  
  • 7. MIPS64
  • mips/mips64:极少用于手机,出发点是高性能,主要用于路由器、猫。
  • x86/x86_64:x86 架构的手机的市场占有率很低,约为1%左右。而且 x86 架构都包含 arm 模拟层,兼容 arm 类型的 abi。注意,模拟器为 x86 架构
  • arm64-v8a:64位 arm 架构。可用32位模式运行 armeabi-v7a和armeabi。(所谓的 armv8 架构,就是在 MIPS64 架构上增加了armv7 架构中已经拥有的的 TrustZone 技术、虚拟化技术及 NEON advanced SIMD 技术等特性)。
  • armeabi-v7a:主流版本 armv7,2011年15月以后的生产的大部分 Android 设备都使用它。
  • armeabi:老版本 armv5,不支持硬件辅助浮点运算,支持所有的 arm* 设备。

从厂家上来分是有三种:arm,x86,MIPS。

arm 系列是绝大多数手机上使用的,x86 主要是运用在 pc 上,而 MIPS 基本上就没见过。x86 架构的设备支持 x86、armeabi-v7a 和 armeabi 等 ABI。但优先级从高到低依次为 x86、armeabi-v7a、armeabi。系统会根据此顺序寻找首个可用的最优的 so 库,找到则结束。

  • x86 设备包含 arm 模拟层,能够很好地运行 arm 类型的 so 库,但并不保证100%不发生 crash。
  • 64位设备 (arm64-v8a, x86_64, mips64) 能够运行32位的 so 库。但是以32位模式运行时,会丢失专为64位优化过的性能特征。

 

从类型来分,有32位和64位,名字中没有64的就是32位的。

正常来说只使用 armeabi-v7a 就可以适配基本所有手机了,因为现在手机基本上都支持这种 cpu 架构,但是对于同时也能支持 arm64-v8a 的手机来说,性能上就不如使用对应 cpu 架构的快了,毕竟是32位和64位的区别。值得一提的是,arm 系列本身是没有64位,而是 intel 的 x86_64 先出现的,之后 arm 收购了 MIPS64,基于 MIPS64 改良出 arm64-v8a,所以也能理解为什么 MIPS 几乎没有,而另外,arm64-v8a 的 cpu 架构上也能运行 armeabi-v7a,为什么呢?不是64位上运行32位,而是 arm64-v8a 上本身搭载了 armeabi-v7a,所以在 arm64-v8a 上运行 armeabi-v7a 是使用32位处理的。

如果应用中有不止一个 so,那就要注意了,如果这时你一个 so 同时支持了 armeabi-v7a 和arm64-v8a,而另一个 so 只支持了一种,那可能会运行有问题,这时要么另一个 so 也支持两种,要么把第一个 so 删掉对应目录,只支持相同的一种。

我以前移植挖矿程序的时候,将编译好的挖矿 so 放到一个 app 中测试,app 启动时加载的是自身应用的一个 so 库,只有 32 位,而挖矿 so 库当时只编译了 64 位的。这时整个 app 的运行环境是以32位来运行 so 的,当执行到挖矿逻辑时,系统崩溃了。

默认会以应用启动后加载的第一个 so 为准,比如你的手机是64位的,应用中同时存在32位和64的so,如果启动时加载的是32的 so,那么所有的 so 都会以32位的模式运行,如果某个 so 只有 64位的,那么运行到这个 so 相关的功能时,将会 crash。

 

 

 

4. Android.mk 使用说明

 

  • 4.1 简介

Android.mk 文件用来告知 NDK Build 系统关于 Source 的信息。 Android.mk 将是 GNU Makefile 的一部分,且将被 Build System 解析一次或多次。所以请尽量少的在 Android.mk 中声明变量,也不要假定任何东西不会在解析过程中定义。

Android.mk 文件语法允许我们将 Source 打包成一个 "modules",modules可以是:静态库或动态库。

只有动态库可以被 install/copy 到应用程序包 (APK),静态库则可以被链接入动态库 (静态库就是给动态库打辅助用的)。

可以在一个 Android.mk 中定义一个或多个 modules,也可以将同一份 source 加进多个 modules。

Build System 帮我们处理了很多细节而不需要我们再关心。例如:你不需要在 Android.mk 中列出头文件和外部依赖文件。NDK Build System 自动帮我们提供这些信息。这也意味着,当用户升级 NDK 后,你将可以受益于新的 toolchain/platform  而不必再去修改 Android.mk。

 

  • 4.2 Android.mk 语法

首先看一个最简单的 Android.mk 的例子:

LOCAL_PATH := $(call my-dir)
 
include $(CLEAR_VARS)
 
LOCAL_MODULE    := hello-jni
LOCAL_SRC_FILES := hello-jni.c
 
include $(BUILD_SHARED_LIBRARY)

 

  • LOCAL_PATH := $(call my-dir) 

每个 Android.mk 文件必须以定义 LOCAL_PATH 为开始,它用于在开发中查找源文件。

宏 my-dir 则由 Build System 提供,返回包含 Android.mk 的目录路径。

 

  • include $(CLEAR_VARS) 

CLEAR_VARS 变量由 Build System 提供。并指向一个指定的 GNU Makefile,由它负责清理很多 LOCAL_xxx。例如:LOCAL_MODULE, LOCAL_SRC_FILES, LOCAL_STATIC_LIBRARIES 等等,但不清理 LOCAL_PATH。

这个清理动作是必须的,因为所有的编译控制文件由同一个 GNU Make 解析和执行,其变量是全局的,所以清理后才能避免相互影响。

 

  • LOCAL_MODULE    := hello-jni 

LOCAL_MODULE 模块必须定义,以表示 Android.mk 中的每一个模块,名字必须唯一且不包含空格。

Build System 会自动添加适当的前缀和后缀。例如foo,要产生动态库,则生成 libfoo.so。但请注意,如果模块名被定为 libfoo,则生成 libfoo.so,不再加前缀。

 

  • LOCAL_SRC_FILES := hello-jni.c 

LOCAL_SRC_FILES 变量必须包含将要打包如模块的 c/c++ 源码。不必列出头文件,build System 会自动帮我们找出依赖文件。

缺省的 c++ 源码的扩展名为 .cpp,也可以通过 LOCAL_CPP_EXTENSION 来修改。

 

  • include $(BUILD_SHARED_LIBRARY) 

BUILD_SHARED_LIBRARY:是Build System 提供的一个变量,指向一个 GNU Makefile Script。它负责收集自从上次调用 include $(CLEAR_VARS)  后的所有 LOCAL_XXX 信息,并决定编译为什么。

BUILD_STATIC_LIBRARY:编译为静态库。

BUILD_SHARED_LIBRARY :编译为动态库 。

BUILD_EXECUTABLE:编译为 Native C 可执行程序。

PREBUILT_SHARED_LIBRARY:把共享库声明为一个独立的模块。 指向一个 build 脚本,用来指定一个预先编译好的动态库。与 BUILD_SHARED_LIBRARY 和 BUILD_STATIC_LIBRARY 不同,此时模块的 LOCAL_SRC_FILES 应该被指定为一个预先编译好的动态库,而非 source file,例如 .o 文件。

PREBUILT_STATIC_LIBRARY:  预先编译的静态库。

 

  • TARGET_ARCH

目标 CPU 架构名。如果为 arm 则声称 arm 兼容的指令,与 CPU 架构版本无关。

 

  • TARGET_PLATFORM

目标平台的名字。

 

  • TARGET_ARCH_ABI

Name of the target CPU+ABI  
armeabi For ARMv5TE  armeabi-v7a

 

  • LOCAL_C_INCLUDES 

一个可选的 path 列表,相对于 NDK ROOT 目录。编译时,将会把这些目录附上。  

LOCAL_C_INCLUDES := sources/foo  

LOCAL_C_INCLUDES := $(LOCAL_PATH)/../foo

 

  • LOCAL_CFLAGS

一个可选的设置,在编译 c/c++ source 时添加 Flags,用来附加编译选项。
也可以指定 include 目录通过:LOCAL_CFLAGS += -I<path>,这个方法比使用 LOCAL_C_INCLUDES 要好,因为这样也可以被ndk-debug 使用。    

 

  • LOCAL_CXXFLAGS

LOCAL_CPPFLAGS 的别名。

         

  • LOCAL_CPPFLAGS

c++ Source 编译时添加的 C Flags。这些 Flags 将出现在 LOCAL_CFLAGS flags 的后面。 

    

  • LOCAL_STATIC_LIBRARIES

要链接到本模块的静态库列表。
     

  • LOCAL_SHARED_LIBRARIES

要链接到本模块的动态库列表。      

 

  • LOCAL_LDLIBS

linker flags,可以用它来添加系统库。 如 -lz:   LOCAL_LDLIBS := -lz。

 

 

 

5. Application.mk 使用说明

APP_PLATFORM = android-11
APP_ABI := armeabi-v7a
APP_STL := stlport_static
APP_OPTIM := debug
  • APP_PLATFORM

使用的ndk库函数版本号,一般和 SDK 的版本相对应,各个版本在 NDK 目录下的 platforms 文件夹中。

  •  APP_ABI        

编译成什么类型 CPU 的 so。

  • APP_STL      

 如何链接 c/c++ 标准库。stlport_static (静态链接),stlport_shared (动态链接),system (系统默认)。

  • APP_OPTIM   

编译版本,如果是 DEBUG 版本就会带上调试信息。可以使用 gdb-server 进行动态断点低调试。debug (调试版本,so 中带调试信息),release  (发布版本,so 不带调试信息)。

 

 

 

6. 交叉编译

Android 中除了使用 so 库的方式来调用本地方法,还可以使用交叉编译的方式生成可执行文件,然后将可执行文件 push 到 Android 设备进行执行。

大概步骤是使用 NDK 提供的工具生成交叉工具链,然后用这个交叉工具链执行生成可执行文件。类似 c/c++ 中用 gcc/g++ 等编译工具编译链接生成可执行文件,只不过这边的编译工具换成了 Android 的交叉编译工具,比如 arm-linux-androideabi-gcc (arm 平台)。

详情步骤见:Android学习——NDK交叉编译

 

 

 

7. CMake 开发 JNI

一般 Android 中开发 app 用 android studio 就够了,从 android studio 2.3 以后,android studio 就已经支持使用 cmake 的方法进行 NDK 编译了。从底层来说,android studio 也是调用 cmake 的命令来进行编译的。

原理:

Android cmakelist.txt 和正常的 Linux 下程序没有什么区别,只是编译 Android 程序需要相应的编译器,连接器,而不是 Linux 的 g++ 等。调用 cmake 带上-DCMAKE_TOOLCHAIN 参数指定 android 交叉工具链的 .cmake 后,这个交叉工具链文件就会首先被执行,设置一系列的 cmake 的保留变量,如编译器,链接器等,还可以指定其他的参数指定平台,架构等信息。

感兴趣的同学可以看看这篇文章:Android笔记之使用CMake进行JNI开发(Android Studio)

 


 

 


  

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值