关于Android开发中JNI/NDK使用的一点总结

咳咳,作为一名android爱好者(其实是为了钱钱),之前一直在使用Android Sdk进行开发,同时也一直知道有个ndk的开发方式,知道全名是native development kit,基原生开发工具集,模糊的知道应该是和c/c++开发有关系的,然后就没有深入一点的了解了。

目前阶段想系统性的收一下自己的android技能,整理成一个比较系统的知识体系,于是乎ndk就成了一个绕不过去的技术,这篇文章就简单的说说android开发中jni/ndk技术的使用。

1、关于JNI技术的说明

JNI全称Java Native Interface,其实也不是啥新鲜的东西,简单的说也就是java中调用c/c++类库的技术,也可以理解为”java + c”的一门技术,至于为什么要使用”java + c”的技术,难道c能实现的功能还有java做不到的么,其实不也全不是这样,在实际项目中使用JNI的主要目的有

a、增强安全性,防止被反编译后被不法分子分析应用的逻辑(增强安全性是相对的);

b、java不能直接与硬件交互的时候,需要c/c++上场干活;

c、有些功能可以通过c/c++打包后,供多种平台和语言调用,也就提高了程序的可移植性;

而JNI的开发流程大致分为六步

第一步: 编写声明了 native 方法的 Java 类
第二步: 将 Java 源代码编译成 class 字节码文件
第三步: 用 javah -jni 命令生成.h头文件(javah 是 jdk 自带的一个命令,-jni 参数表示将 class 中用native 声明的函数生成 JNI 规则的函数)
第四步: 用本地代码实现.h头文件中的函数
第五步: 将本地代码编译成动态库(Windows:\*.dll,linux/unix:\*.so,mac os x:\*.jnilib)
第六步: 拷贝动态库至 java.library.path 本地库搜索目录下,并运行 Java 程序

关于这六步的具体实现,JNI 开发流程这篇文章中有很好的讲解,这里就不多说了。

2、关于NDK技术的说明

第一小节讲了JNI,那么这个NDK又是什么东西呢,其实这个NDK不是什么新鲜技术,它只是一个Native开发的工具集,让开发者更方便的进行JNI开发而已。

JNI的不方便之处可以用下面的话来表述

Android SDK 文档里,找不到任何 JNI 方面的帮助。即使第三方应用开发者使用 JNI 完成了自己的 C 动态链接库(so)开发,但是 so 如何和应用程序一起打包成 apk 并发布?这里面也存在技术障碍。比如程序更加复杂,兼容性难以保障,无法访问Framework API,Debug 难度更大等。

而NDK的出现就是为了解决上面描述的一些问题的,NDK作为工具集,有以下功能

NDK 集成了交叉编译器,并提供了相应的 mk 文件隔离 CPU、平台、ABI 等差异,开发人员只需要简单修改 mk 文件(指出“哪些文件需要编译”、“编译特性要求”等),就可以创建出 so。

NDK 可以自动地将 so 和 Java 应用一起打包,极大地减轻了开发人员的打包工作。

NDK 提供了一份稳定、功能有限的 API 头文件声明

根据第一小节我们知道,JNI开发有六个步骤,而当前的NDK版本提供便利表现在第五和第六个步骤,第五步的便利表现在ndk提供了ndk-build命令可以直接将c/c++代码编译为so包,而第六步则体现在可通过gradle中的ndk配置来直接加载对应的so包(这里不懂别着急,后面会讲ndk的使用,到时便可以体会)。

3、关于ABI的说明

可能有童鞋到这里不明白了,好好的讲JNI/NDK,怎么又讲到ABI了,这是个什么玩意。这里就来说说为什么要说这个ABI,ABI的全称是Application Binary Interface,知道全称其实也不知道是干嘛的吧。

ABI(应用程序二进制接口)定义了二进制文件(尤其是.so文件)如何运行在相应的系统平台上,从使用的指令集,内存对齐到可用的系统函数库。而Andriod作为一种操作系统,它得去适应不同的CPU架构,早期的Android系统几乎只支持ARMv5的CPU架构,现在Android已经支持7中CPU架构了,分别是ARMv5,ARMv7 (从2010年起),x86 (从2011年起),MIPS (从2012年起),ARMv8,MIPS64和x86_64 (从2014年起),每一种CPU架构都关联着一个相应的ABI。学会汇编的同学都知道CPU不一样,对应的指令集就不一样,这里的ABI就可以理解为指令集。那么不同的CPU架构自然就需要不同的ABI,所以与7种CPU架构相对应的就有其中ABI,分别是armeabi,armeabi-v7a,x86,mips,arm64- v8a,mips64,x86_64。

还是有同学说了,你说这么多还是没有说清楚为啥讲JNI/SDK要讲这个ABI啊。童鞋们,我们编写的so包最终是要打包到android手机上的也就是会执行在android cpu上,那么必然需要不同的指令集,所以我们写的c/c++代码就得根据不同的ABI进行编译,最后编译出针对不同cpu的so包来,也就是我们写的一份c/c++代码,最多可以编译出7个so包,然后一起打包到apk中,apk会根据当前的cpu架构去调用对应的so包。

这里写图片描述

我想到这里算是说清楚了吧。

4、NDK的使用

我为了尝鲜kotlin已经把开发工具升级到android studio 3.0版本了,但是感觉这个ndk开发和ide版本没有太大关系。

实际上使用NDK开发JNI还是蛮简单的,但是我还是走了不少弯路,刚开始便是依据NDK-JNI实战教程(一) 在Android Studio运行第一个NDK程序这篇文章进行开发,但是怎么都不成功。

后面换了个思路,改依据Android studio运行JNI程序以及生成.so文件(Windows下)这篇文章后成功。

这里还是来说说吧。

4.1 基本配置

首先得配置好ndk,这里的配置包括两方面的,

一是在android stuido中配置好

project structure中的配置

这里写图片描述

local.properties文件中的配置

这里写图片描述

二是系统环境变量中的配置

配置NDK_HOME

在系统环境变量中添加NDK_HOME 然后值为你的ndk路径

配置Path

path环境变量中添加%NDK_HOME%\

配置好后,cmd中运行ndk-build,出来正常提示就表示配置成功了。

4.2 生成.h头文件

在android 项目中添加NdkUtil.java文件

package com.yjing.ndk.utils;

public class NdkUtil {

    public native String helloFromJni();

    public native void callBackJavafromC();
}

然后编译项目,到build\intermediates\classes\debug这个目录下查看,是否成功编译出NdkUitl.class文件,有的话表示编译成功,然后继续

cmd中cd到build\intermediates\classes\debug这个目录下,然后执行

javah -jni com.yjing.ndk.utils.NdkUtil

或者

javah -classpath . -jni com.yjing.ndk.utils.NdkUtil

总之目的就是在debug目录下生成一个针对NdkUtil.class文件的.h文件就是了。

4.3 jni文件夹中代码完善

在main文件目录下创建jni目录

这里写图片描述

将4.2中的头文件,拷贝到jni目录中,同时在jni目录中创建hello.c文件,对该文件做实现

#include "com_yjing_ndk_utils_NdkUtil.h"

/*
 * Class:     com_yjing_ndk_utils_NdkUtil
 * Method:    helloFromJni
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_yjing_ndk_utils_NdkUtil_helloFromJni
  (JNIEnv *env, jobject obj){

    char* c ="hello from c";
    return (*env)->NewStringUTF(env, c);

  }


/*
 * Class:     com_yjing_ndk_utils_NdkUtil
 * Method:    callBackJavafromC
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_yjing_ndk_utils_NdkUtil_callBackJavafromC
  (JNIEnv *env, jobject obj){

    // 1.通过反射找到类
    jclass clazz = (*env) -> FindClass(env, "com/wangjin/hellondkdemo/MainActivity");
    // 2. 找到方法ID
    jmethodID methodId = (*env) -> GetMethodID(env, clazz, "logout", "()V");
    // 3.调用方法,obj就是调用的类实例,所以不用再次创建了
    (*env)->CallVoidMethod(env, obj, methodId);

  }

然后在jni目录中创建Android.mk以及Application.mk两个文件,内容分别为

Android.mk

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)

LOCAL_MODULE := hello
LOCAL_SRC_FILES := hello.c

include $(BUILD_SHARED_LIBRARY)

Application.mk

APP_ABI := all

这里的Android.mk类似linux中的makefile文件用来指定c/c++文件之间的依赖关系,而Application.mk文件用来指定ABI类型,这里指定我们的so包要适配所有的CPU架构。

Android.mk中的LOCAL_MODULE对应的是build.gradle文件中的ndk配置

这里写图片描述

到这里jni目录下的东西就算是讲完了

4.4 build.gradle文件中的配置

实际上4.3小节都提到了build.gradle文件的配置,但是不全面,这里单独用一小节来说明

build.gradle中首先要如4.3小节中的那样配置ndk模块,其次还需要指定so包编译后的存放路径

sourceSets{
    main{
        jni.srcDirs = ['libs']
    }
}

这里指定so报的路径为main目录下的libs目录

4.5 使用ndk-build命令

我们配置好后,便到了生成so包的时候了,生成so包很简单,只需要借助ndk-build工具便可。

cmd 中cd到项目的main目录路径,然后执行ndk-build便能在main/libs目录下生成各种CPU架构的so包了。

这里写图片描述

4.6 项目中使用so包

编辑NdkUtil.java文件,添加加载so包的代码后,NdkUtil.java内容如下

package com.yjing.ndk.utils;

public class NdkUtil {

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

    public native String helloFromJni();

    public native void callBackJavafromC();
}

然后在代码中条用helloFromJni以及callBackJavafromC方法便能够成功调用到native代码中去。

这里给出测试的MainActivity代码吧

package com.yjing.ndk;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import com.yjing.ndk.utils.NdkUtil;

public class MainActivity extends AppCompatActivity {
    private Button btnNdk;
    private TextView tvNdk;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        this.initViews();
        this.initEvents();
    }

    private void initViews(){
        this.btnNdk = (Button)findViewById(R.id.btnNdk);
        this.tvNdk = (TextView)findViewById(R.id.tvNdk);
    }

    private void initEvents(){
        this.btnNdk.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                NdkUtil ndkUtil = new NdkUtil();
                ndkUtil.callBackJavafromC();
            }
        });
    }

    public void logout() {
        System.out.println("hahahahahahahahahah===");
    }
}

运行后提示错误

couldn't find "libhello.so

只需要修改build.gradle文件配置如下,便能解决问题

sourceSets{
    main{
        jni.srcDirs = ['libs']
        jniLibs.srcDirs 'src/main/libs'
    }
}

从代码中可以看出java能够调用native方法,而native也能够调用java方法。

到这里关于Android开发中的JNI/NDK基本总结就算完成了。

5、参考文献

1、JNI/NDK 开发指南

2、Android studio运行JNI程序以及生成.so文件(Windows下)

3、Android 中arm64-v8a、armeabi-v7a、armeabi、x86简介~

4、Android Studio2.3NDK的简单配置及快速开发

5、Getting Started with the NDK

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章