初级入门JNI练习步骤(整理总结)

概述

写这篇文章的目的就是让懵懵懂懂的新同学可以快速的体验和理解JNI的通信流程,从应用层面上理解底层(driver layer)、JNI、framework、applications之间的通信过程。 避免在整个过程中陷入泥坑耗费精力和时间。

什么是JNI

关于这个概念网上有很多解释,大白话就是语言转换,可以让java与c/c++之间进行相互通信的一种格式约定。这样的好处就是APP开发者不用关心顶层硬件如何实现的都可以通过java去控制硬件的响应。反之硬件反馈的数据也可以传递给上层app。

开发准备环境

  • Ubuntu16.04 - 用来编译Android源码及其模块,在这里我们主要用来编译jni so库和jar包。
  • Android Studio - 用来编译Application上层应用程序,进行验证jni的调用结果, 当前使用的版本为3.5.3
  • 目标设备 - 用来运行整个测试程序,当然也可以通过模拟器来测试。

编写JNI工程

可能有很多新同学刚接触JNI的时候就会疑惑它到底是个什么东西,其实就是一个so库(专指Linux下的动态库),只是这个so库是使用了一些jni语法进行编写然后编译形成的库。对于嵌入式的同学应该就会很容易理解了。 不理解的同学也没有关系,我们将通过实际操作来进行认识。

下面我们将进行第一个JNI练习。
目标为: 从jni层获得一个字符串,在屏幕上显示,当然是手机屏幕或者模拟器
我们要完成这一过程需要经历几个步骤:
首先:需要有jni层,当app调用jni给出的接口时, jni可以通过这个接口返回数据给到app。
其次,需要framework层, 当app调用jni接口时需要经过中间转换,否则不能直接调用到jni层。
最后,app就可以通过framework作用的jni接口,jni进而将会作用到硬件接口,如加载wifi,驱动马达,点亮一个led等操作。
再此我们只到jni这一步就结束了,如果有兴趣的话可以通过填充接口去作用相应的硬件。

在Android源码下创建JNI工程

首先我们将在AOSP源码树下创建工作目录,目录布局如下所示:

# 创建用于存放JNI源码的目录jni,并在该目录下创建Android.mk文件和jnidemo.cpp文件及onload.cpp
mkdir -p vendor/mediatek/proprietary/frameworks/base/core/jni
cd vendor/mediatek/proprietary/frameworks/base/core/jni
touch Android.mk
touch jnidemo.cpp
touch onload.cpp

然后编写jnidemo.cpp对外接口函数:

// jnidemo.cpp
#include <utils/Log.h>
#include "JNIHelp.h"
#include "jni.h"
#define LOG_TAG "Service-JNI"
 
namespace android {
	static jint jni_nativeOpen(JNIEnv* env,jobject obj){
        ALOGE("JNI test nativeOpen");
      	return 10;
	}

    static jstring jni_displayString(JNIEnv* env, jobject obj) {
        ALOGE("JNI test native string");
        return env->NewStringUTF("Hello Sven! I am from JNI!");
    }
 
 	/**
 	 * 在这里method_table定义了两个开放接口,一般要查看JNI的接口直接开个类型就可以快速了解
 	 * 提供了哪些接口, 其中JNINativeMethod的类型为:
 	 * typedef struct {
           const char* name;
           const char* signature;
           void* fnPtr;
       } JNINativeMethod;
     * @param name 就是供framework开放的接口。 
     * @param signature 是对该接口属性描述,比如返回值和参数的描述。具体关键字含义请自行查看。
     * @param fnPtr 是对framework接口被调用后真正在jni被执行的函数。这是一一对应的关系。
 	 */
    static JNINativeMethod method_table[] = {
        {"nativeOpen","()I",(void*)jni_nativeOpen },
        {"displayString","()Ljava/lang/String;",(void*)jni_displayString }
  	};
  	
    /**
     * 这个register_android_jnidemo_Service()方法,是用于注册JNI文件,在该方法中用到了两个
     * 关键的参数。一个是"com/example/test/Demo",对应着java代码的包名和类名,即调用JNI的
     * java代码所在的包是“com.example.test”,类名是Demo;另一个参数是method_table,即是
     * 上面初始化的JNINativeMethod结构体。
     */
    int register_android_jnidemo_Service(JNIEnv *env){
      	return jniRegisterNativeMethods(env,"com/example/test/Demo",
           method_table,NELEM(method_table));
    }
}

最后增加JNI导入装载函数入口:
在这里我们使用JNI_OnLoad()函数进行注册JNI。代码实现如下:

// onload.cpp
#include "JNIHelp.h"
#include "jni.h"
#include "utils/Log.h"
#include "utils/misc.h"
 
namespace android {
	int register_android_jnidemo_Service(JNIEnv* env);
};
 
using namespace android;

extern "C" jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
    JNIEnv* env = NULL;
    jint result = -1;
 
    if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
        ALOGE("GetEnv failed!");
        return result;
    }
    ALOG_ASSERT(env, "Could not retrieve the env!");
 
    register_android_jnidemo_Service(env);

    return JNI_VERSION_1_4;
}

以上就是JNI相关源码部分的实现了。
接下来我们需要实现编译控制,因此需要编写Android.mk将上述JNI源码编译成so库文件:

# Android.mk
# Sven JNI Test
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)

LOCAL_MODULE_TAGS := optional
# 加入指定JNI源码,在这我们直接指定具体文件, 也可以通过匹配方式加入
LOCAL_SRC_FILES:= \
    jnidemo.cpp \
    onload.cpp
 
LOCAL_SHARED_LIBRARIES := \
     libnativehelper \
     libcutils libutils \
     liblog
 
LOCAL_MODULE:= libjnidemo
include $(BUILD_SHARED_LIBRARY)

这样我们就可以进行编译了。
在jni当前目录执行mm 或者在Android源码顶层目录执行make这样将编译生成libjnidemo.so文件到/system/lib目录下。

注: 编译之前当然需要设置编译环境了 比如source build/envsetup.sh 然后再lunch 一下等等,才可以正常编译。

到这里我们的第一步jni库文件就产生了,我们可以将它push到我们的设备中,放到/system/lib目录下。

adb push ${AOSP}/out/target/product/sm3_bsp/system/lib/libjnidemo.so /system/lib

在IDE开发环境中创建JNI工程

常用的IDE开发环境主要是AS和Eclipse这两个开发环境,在本文中将不再表述如何创建jni工程,有兴趣的同学可以自行查看资料学习吧!

framework层java代码的实现

接下来我们需要实现java调用jni的接口层供app应用层调用。通常也有两种方式实现:
一种:将framework实现部分与app分离,以jar包的形式提供给app调用。
第二种:不以jar包的形式提供,将framework实现部分整合到app中进行实现

jar包方式提供

首先我们通过如果实现jar包的方式给app调用,这种方式也是比较常用的方法,特别是跨部门合作或团队合作基本都是这种方式。

在这里我们只通过android源码来进行实现,当然也可以通过IDE这样的开发环境进行实现产出jar包。
和前面一样首先准备源码目录:

# 在AOSP中创建java源码目录
mkdir -p vendor/mediatek/proprietary/frameworks/base/core/java
cd vendor/mediatek/proprietary/frameworks/base/core/java
# 创建包的目录,需要和注册jni时传入的包名一致 'com/example/test', 类名为Demo
mkdir -p com/example/test
cd com/example/test
# 创建framework源码文件
touch Demo.java
# 编写Demo.java
package com.example.test;

import android.util.Log;

public class Demo {
    String TAG="frameworkSven Demo";
    static {
        // 引入上述实现的libjnidemo.so
        System.loadLibrary("jnidemo");
    }
    //以native 申明JNI函数,该函数要和前面提到的method_table.name相一致
    public native int nativeOpen();	 
	public native String displayString();

    public Demo(){
        Log.d(TAG,"get from jni = "+nativeOpen());
    }
}
# 在当前目录中创建Android.mk文件, 制作出jar包好提供给app开发使用。
touch Android.mk
LOCAL_PATH:= $(call my-dir)
#make jar
include $(CLEAR_VARS)
LOCAL_JACK_ENABLED := disabled
LOCAL_SRC_FILES := $(call all-subdir-java-files)
LOCAL_MODULE := svendemo	# 编译出来的jar包就是svendemo.jar

include $(BUILD_STATIC_JAVA_LIBRARY)

最后开始编译产出svendemo.jar,如此将该jar包给到app开发就可以使用了。

不以jar包方式提供

这种方式是直接将前面jni部分实现的libjnidemo.so提供给app,让app开发者来实现java接口调用的接口导入。实际上它是将前面实现jar的部分直接放在了app中进行实现,相比前面那种方法省去了jar包。 这种模式集成度高通常内部分工没有那么细化或不用提供给第三方很多使用的情况下采用的方法。

首先通过Android studio3.5创建一个工程 - package name:com.runoob.myjniload。

第二步, 创建一个Demo.java类,该类就是实现framework调用jni的导入接口,其目的和前面的jar包一样。
在main.java.com.example.test目录下创建Demo.java并编辑如下:

// 这个包名一定要和jni所指定的包一致,否则加载so时将会出现不能找到包的问题
package com.example.test;
import android.util.Log;

public class Demo {
    String TAG="AndroidSven Demo";
    static {
        System.loadLibrary("jnidemo");
   }

    public native int nativeOpen();	//以native 申明JNI函数
    public native String displayString();
    public Demo(){
        Log.d(TAG,"get from jni = ");
        Log.d(TAG,"get from jni = "+nativeOpen()); // 调用jni native api
    }
}

是不是和前面的方式一样呢。

注意:
包名不一致会提示的错误:
jni_internal.cc:593] JNI FatalError called: Native registration unable to find class ‘com/example/test/Demo’; aborting…

第三步:需要将前面实现的libjnidemo.so导入到工程中
需要在src目录中创建一个jniLibs目录来存放这个so。我的工程目录结构如下:
在这里插入图片描述

以上就是不单独提供jar包的方式。 接下来就是在该工程继续实现app调用逻辑即可。

第四步, app调用framework层实现jni调用
编写MainActivity.java

package com.runoob.myjniload;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.widget.TextView;
import android.util.Log;
import com.example.test.Demo;  // 导入Demo.java的包

public class MainActivity extends AppCompatActivity {
    private TextView tv;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.d("AndroidSven :", "onCreate() event");
        System.out.println(System.getProperty("java.library.path"));
        Demo instance = new Demo(); // 创建Demo事例,通过构造函数调用jni native api
        tv = (TextView)findViewById(R.id.tv);
        // 从jni接口获取的string显示到ui文本框中
        tv.setText("myJniLoad test native displayString() : " + instance.displayString());
        Log.d("AndroidSven :", "after onCreate() event");
    }
}

编译产出myDemo.apk及libjnidemo.so,将它们push到设备,然后运行apk在logcat中将会输出jni调用信息:

2019-06-07 07:20:21.400 19357-19357/com.runoob.myjniload D/AndroidSven :: onCreate() event
2019-06-07 07:20:21.401 19357-19357/com.runoob.myjniload I/System.out: /system/lib:/vendor/lib
2019-06-07 07:20:21.403 19357-19357/com.runoob.myjniload D/AndroidSven Demo: get from jni = 
2019-06-07 07:20:21.403 19357-19357/com.runoob.myjniload E/Service-JNI: JNI test nativeOpen
2019-06-07 07:20:21.404 19357-19357/com.runoob.myjniload D/AndroidSven Demo: get from jni = 10
2019-06-07 07:20:21.404 19357-19357/com.runoob.myjniload E/Service-JNI: JNI test native string
2019-06-07 07:20:21.404 19357-19357/com.runoob.myjniload D/AndroidSven :: after onCreate() event

UI试图显示:
在这里插入图片描述

APP上层调用framework

前面已经讲到如何生成jar包给到app使用。
现在将开始如何导入这个jar包供app开发调用。
首先,通过Android studio3.5创建一个工程
第二步, 导入上面产出的svendemo.jar包放入到工程src\libs目录下
在这里插入图片描述
第三步, 编写app逻辑MainActivity.java

package com.runoob.myjniload;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;

import com.example.test.Demo;

public class MainActivity extends AppCompatActivity {
    private TextView tv;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.d("AndroidSven :", "onCreate() event");
        System.out.println(System.getProperty("java.library.path"));
        Demo instance = new Demo();
        tv = (TextView)findViewById(R.id.tv);
        tv.setText("Demo test native displayString() : " + instance.displayString());
        Log.d("AndroidSven :", "after onCreate() event");
    }
}

编译后产出MyDemoJni.apk,放到设备运行
在这里插入图片描述
logcat日志信息:

2019-06-07 07:36:02.247 19427-19427/com.example.mydemojni D/AndroidSven :: onCreate() event
2019-06-07 07:36:02.249 19427-19427/com.example.mydemojni E/Service-JNI: JNI test nativeOpen
2019-06-07 07:36:02.249 19427-19427/com.example.mydemojni D/frameworkSven Demo: get from jni = 10
2019-06-07 07:36:02.250 19427-19427/com.example.mydemojni E/Service-JNI: JNI test native string
2019-06-07 07:36:02.305 19427-19427/com.example.mydemojni D/WindowClient: Add to mViews: DecorView@64

总结

使用jni的流程大体有两种方式:
一种: 生成jar包给第三方app开发使用,app不用关系jni的接口声明,只需指定jar提供了哪些接口,这种是应用和接口分离的方法也是主流的方式
二种:不生成jar包,只提供native api文件(so文件)给app开发使用,这样app开发人员还需要做nativ接口声明,需要指定jni提供了哪些接口,这种相对于第一种稍微复杂一些

在这里插入图片描述
以下是链接工程代码:
打包jar
不打包jar

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值