JNI

概述

几乎稍有经验的Android开发,都会在工作中用到JNI的开发。即使工作中没有涉及到JNI的开发,在我们使用第三方的库时,也经常需要引入.so文件。

最初我在学习JNI开发时,基本是懵的。因为大部分JNI开发的指南,其实是在教我们,如何生成.so文件和如何引入.so文件。

我们参照着博客的步骤,修改build.gradle,添加cmakelists,写JNI接口,写c++。但每一步,我们实际是在做啥,我们并没有清晰的认识。这也导致每次JNI的配置步骤,看一次忘一次。

本文旨在彻底写清楚,当我们在做JNI开发时,我们在做什么。

.so文件

so是shared object的缩写,见名思义就是共享的对象,机器可以直接运行的二进制代码。大到操作系统,小到一个专用软件,都离不开so。so主要存在于Unix和Linux系统中。

我们通过C/C++开发的软件,如果以动态链接库的形式输出,那么在Android中它的输出就是一个.so文件。

相比于.a,.so文件是在运行时,才会加载的。所以,当我们将.so文件放入工程时,一定有一段Java代码在运行时,load了这个native库,并通过JNI调用了它的方法。

所以,当我们使用JNI开发时,我们就是在开发一个.so文件。不论我们是开发一个工程,还是开发一个库,只要当我们使用C++开发,都会生成对应的.so文件。

所以JNI开发的核心是,我们生成so的过程,和使用so的过程。

生成.so文件

ndk build

第1步:配置Android NDK环境
在这里插入图片描述
在这里插入图片描述

第2步:创建Android项目,并与NDK进行关联

在local.properties文件中添加配置

在这里插入图片描述

第3步:在Android项目中声明所需要调用的Native方法

在这里插入图片描述

第4步:实现在Android中声明的Native方法

  • 重新Make Project一下工程,完成后会在工程目录app\build\intermediates\javac\debug\classes下看到编译后的classes文件NdkJniUtils.class文件

  • 使用javah工具生成头文件

    • 我们的NDK模块源代码由C/C++的头/源文件和make文件组成,这些文件必须放在jni目录下
    • 理论上,jni目录可以放在任何地方
    • 我们一般把jni目录放在项目根目录下(即src同级目录)
      javah -d .\app\jni -classpath .\app\build\intermediates\javac\debug\classes com.demo.sample.NdkJniUtils
      

在这里插入图片描述

  • 创建本地代码文件
    在jni目录下创建com_tzz_sample_NdkJniUtils.cpp文件,内容如下
    #include "com_tzz_sample_NdkJniUtils.h"
    NIEXPORT jstring JNICALL Java_com_tzz_sample_NdkJniUtils_getStringFromNative
    (JNIEnv *env, jclass){
        return env->NewStringUTF("这是ndk build方式编译实现的jni");
    }
    
  • 注意:
    • JNIEXPORT jstring JNICALL中的 JNIEXPORT和JNICALL不能省略,其中jstring表示该方法返回字符串
    • 方法名的格式:Java_包名_类名_Java需要调用的方法名,对于包名,包名中的 . 要改成 _ ,_ 要改成 _1

第5步:创建Android.mk文件和Application.mk文件

Android.mk文件是一个负责向NDK构建系统描述NDK项目的GNU Makefile片段,是每一个NDK项目的必备组件,根据GNU Make的命名规则,变量名要大写

# Android.mk必须以LOCAL_PATH开头,注释#除外
# 设置工作目录,而my-dir则会返回Android.mk文件所在的目录
LOCAL_PATH := $(call my-dir)

# 借助CLEAR_VARS变量清除除LOCAL_PATH外的所有LOCAL_<name>变量
include $(CLEAR_VARS)

# 设置模块的名称,即编译出来.so文件名
# 注,要和上述步骤中build.gradle中NDK节点设置的名字相同
LOCAL_MODULE := jniLib-ndkbuild

# 指定参与模块编译的C/C++源文件列表,多文件用"\"隔开
LOCAL_SRC_FILES := com_tzz_sample_NdkJniUtils.cpp


# 必须在文件结尾定义编译类型,指定生成的静态库或者共享库在运行时依赖的共享库模块列表。
# BUILD_SHARED_LIBRARY 共享库,供java或者其他共享库调用
# BUILD_STATIC_LIBRARY 静态库,供共享库调用,不能直接被java调用
include $(BUILD_SHARED_LIBRARY)

Application.mk文件配置编译平台相关的内容

# 最常用的APP_ABI字段:指定需要基于哪些CPU平台的.so文件
# 常见的平台有armeabi x86 mips,其中移动设备主要是armeabi平台
# 默认情况下,Android平台会生成所有平台的.so文件,即同APP_ABI := armeabi x86 mips
# 指定CPU平台类型后,就只会生成该平台的.so文件,即上述语句只会生成armeabi平台的.so文件
# APP_ABI := armeabi armeabi-v7a mips x86
APP_ABI := armeabi-v7a arm64-v8a
APP_PLATFORM := android-21

第6步:编译上述文件,生成.so库文件

cd ./app/jni
ndk-build

编译成功后,会在jni的同级目录中生成libs和obj两个文件夹,其中libs下存放的是.so库文件
在这里插入图片描述

第7步:在Android Studio项目中使用NDK实现JNI功能

  • 因为在build.gradle中已经配置了Android.mk,其中已经引入了libs中的第三方so文件,所以不用再配置jniLibs.srcDirs = [‘libs’],否则会报错: More than one file was found with OS independent path

  • 在申明Native方法的类中加载so文件
    在这里插入图片描述

    • 注意:libs中so文件自动加上了前缀lib,即libjniLib-ndkbuild.so,但是加载so文件时,在Android.mk文件里配置的so文件名是什么就用什么文件名
  • 使用

    public class MainActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            TextView tv = (TextView) findViewById(R.id.test);
            tv.setText(NdkJniUtils.getStringFromNative());
        }
    }
    

在这里插入图片描述

上述步骤全部做完,项目目录是这样的:
在这里插入图片描述

cmake

第1步:下载cmake
在这里插入图片描述

第2步:创建Android c/c++项目

当我们在新建工程过程中,选中support c++时,系统会为我们写好一些配置。

android studio 3.3以上新建项目时去掉了support c++选项,而是改为Native c++选项。

在这里插入图片描述

第3步:创建工程

  • 项目自动生成cpp文件夹,里面包含CMakeLists.txt和native-lib.cpp,MainActivity里自动申明native方法,并加载so使用。
    在这里插入图片描述
  • 同时build.gradle中自动添加配置:
    android {
    --------
    	defaultConfig {
     		--------
            externalNativeBuild {
                cmake {
                    cppFlags ""
                }
           }
    	}
    
        externalNativeBuild {
            cmake {
                path "src/main/cpp/CMakeLists.txt"
                version "3.10.2"
            }
        }
    }
    

这是module的build.gradle中的一段。其中,两个externalNativeBuild与ndk是与JNI相关的配置,不然会报错。

完成以上配置后,当你Make Project时,我们就可以在build目录下,看到我们的.so文件了。

在这里插入图片描述
不论是给自己项目用,还是提供给其他项目用。当我们执行我们的C++代码时,我们都使用的是这个.so文件了。

使用.so文件

在很多如何使用so文件的博客中,我们可以看到下面这一句话:

jniLibs.srcDirs = ['libs']

这句话是将我们的so文件的目录指定为libs文件夹。这样,我们只需要将so文件放入libs即可。

在这里插入图片描述

  • 这样,当我们构建APK包时,gradle就会帮我们,将jniLibs中的.so文件,打入我们的APK文件中的lib目录。
    然后我们安装APK时,系统会将APK包lib文件夹中的so文件拷贝到APP的私有目录下。具体来说就是:
    /data/user/0/[包名]/app_libs/

    当我们在代码中,执行到:

    System.loadLibrary("native2-lib");
    

    我们就会加载这个libnative2-lib库了。

  • 或者我们可以动态加载so文件:

    所以,我们可以将想要加载的so文件,在程序运行时,下载到APP的私有目录的对应位置中,然后使用
    System.load(…);
    加载我们需要的so文件。

    String soPath = Environment.getExternalStorageDirectory().toString() + "/libnative-lib.so";
    File file = new File(soPath);
    if (file.exists()){
       File dir = context.getDir("libs", Context.MODE_PRIVATE);
       String targetDir = dir.getAbsolutePath()+"/libnative-lib.so";
       FileUtils.copySdcardFile(file.toString(), targetDir);
       System.load(targetDir);
    }
    
  • 4
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值