1. cmake配置,快速入门
JNI(一) 认识JNI
从事Android开发的小伙伴时不时的就会涉及到JNI,一提到JNI大部分小白可能有点迷茫,不知从何下手,感觉很高深,在这我给大家说一下,不要怕。在工作中,不管有什么很难需求,首先沉住气,不要退缩,第一反应应该是,我试试,倒要看看他有什么难。
首先我们的先下载好需要的工具:
lldb的作用是用来调试咱们的native代码的,很好用
- 创建项目时勾选c++支持 如图
- 在最后一步勾选c++标准,c++11
下面还有两个选项,一个是c++异常处理的支持,还有一个rtti 是通过运行时类型信息程序能够使用基类的指针或引用来检查这些指针或引用所指的对象的实际派生类型(来源于百度)。 - 点击finish生成新项目
上图我标记了三个地方,咱们先看CMakeLists这个文件:
# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
# "#"开头的都是注释
# Sets the minimum version of CMake required to build the native library.
# 设置构建本机库的cmake的最低版本
cmake_minimum_required(VERSION 3.4.1)
# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
# 大家光看注释也知道是怎么么回事,你想用哪几个文件(在这里就是src/main/cpp/native-lib.cpp)
# 生成一个叫什么(在这里是native-lib)的一个哪种类型的库(是静态的'STATIC',还是动态的'SHARED')
add_library( # Sets the name of the library.
native-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
src/main/cpp/native-lib.cpp)
add_library( # Sets the name of the library.
native-lib1
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
src/main/cpp111/native1-lib.cpp)
# 例子里只提供了这种比较单一的情况,假如我要用好几个文件来生成一个库,或者我要生成多个库,怎么写
#
# 一,先看多个文件生成一个库,并且多个文件不在同一个目录下
# add_library( # Sets the name of the library.
# native-lib
#
# # Sets the library as a shared library.
# SHARED
#
# # Provides a relative path to your source file(s).
# src/main/cpp/native-lib.cpp
# src/main/cpp111/native-lib111.cpp
# src/main/cpp222/native-lib222.cpp)#多个文件之间不用写"," 只要空格隔开就行
#
# 二,还是生成一个库,但是这些文件都在同一个文件夹下,咱们就可以这要简写了
#
# file(GLOB native_srcs "src/main/cpp/*.cpp") #选择父目录里所有的cpp文件
# add_library(# Sets the name of the library.
# native-lib
#
# SHARED
#
# ${native_srcs})
#
# 三,我要生成多个库,这个和上面的写法一样,无非就是多谢几遍的事
# 下面我没来生成两个库 native-lib 和 naive-lib1
# add_library( # Sets the name of the library.
# native-lib
#
# # Sets the library as a shared library.
# SHARED
#
# # Provides a relative path to your source file(s).
# src/main/cpp/native-lib.cpp)
# add_library( # Sets the name of the library.
# native-lib1
#
# # Sets the library as a shared library.
# SHARED
#
# # Provides a relative path to your source file(s).
# src/main/cpp111/native1-lib.cpp)
#
# 这个文件编写很简单,只是写法上大家注意格式就行
# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.
# 这个作用注释里很明确,你把你将要用到的ndk里的库给告诉cmake,可以有多个
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log )
# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.
target_link_libraries( # Specifies the target library.
native-lib #这就是你上面生成的so库的名字
# Links the target library to the log library
# included in the NDK.
# 注释很明确,只要注意写法就好,把上面find_library的库和
# 你要生成的库链接起来,如果有多个,空格隔开
${log-lib} )
相信大家看完后都对CMakeLists.txt的编写都有一定的掌握了,也对上图标红三处都非常明白了吧,CMakeLists.txt的配置就先学点基本的,然后咱们来看gradle的配置
apply plugin: 'com.android.application'
android {
compileSdkVersion 26
buildToolsVersion "26.0.1"
defaultConfig {
applicationId "com.example.goobird_imac.myapplication"
minSdkVersion 21
targetSdkVersion 26
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
externalNativeBuild {
cmake {//如果在创建项目时勾选了 frtti,fexceptions,就会被添加在这里
cppFlags "-std=c++11 -frtti -fexceptions"
}
}
ndk {
// Specifies the ABI configurations of your native
// libraries Gradle should build and package with your APK.
// 这个可以不配置,如果不配置,就会生成所有的版本,如下图(一)
// 如过配置了就会生成我们配置的版本,如下图(二)
abiFilters 'armeabi-v7a'
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:26.+'
compile 'com.android.support.constraint:constraint-layout:1.0.2'
testCompile 'junit:junit:4.12'
}
图一 图二
在代码里引用库想必大家对这个都不陌生,首先我们的在用到我们编写的库的地方先加载库,再声明native方法,咱们就android studio自动生成的这个例子来看一下
第二处标红的地方那些native方法也可以在别的类里面,只要能保证在库加载之后就行
好哩,配置咱们就说到这里,接下俩就可以高高兴兴的写咱们的jni了
废话不多说,从一个小例子来认识一下JNI
我定义了一个类,这就不多说了,再简单不过的一个实体类
public class SchoolObj {
public String schoolName;
public String schoolAddress;
public int studentCount;
public Person headMaster;
public static class Person {
public int age;
public String name;
}
public String toString(){
return "学校名:"+schoolName+"\r\n"+"学校址:"+schoolAddress+"\r\n"+"学生人数:"+studentCount+"\r\n"+"校长名:"+headMaster.name+"\r\n"+"校长年龄:"+headMaster.age;
}
我把native 方法写在了JniUtils里
public class JniUtils {
public static native String stringFromJNI();//这个是系统自动生成的
//用下面这个方法来生成一个SchoolObj对象
public static native SchoolObj createSchool(String schoolName, String schoolAddress, int studentsCount, int headMasterAge, String headMasterName);
}
我想创建一个SchoolObj对象,so easy! SchoolObj obj = new SchoolObj();但是我不,现在我要走一下弯路,我用JNI代码来创建这个对象。
public class MainActivity extends AppCompatActivity {
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// Example of a call to a native method
TextView tv = (TextView) findViewById(R.id.text_system);
tv.setText(JniUtils.stringFromJNI());
TextView tv_lijing = (TextView) findViewById(R.id.text_lijing);
SchoolObj school = JniUtils.createSchool("北京大学", "海淀区", 19999, 24, "river lee");
tv_lijing.setText(school.toString());
}
}
但是我在C代码里怎么创建java对象啊?C里面创建的对象在java里怎么转换啊?。。。还是那句话,我试试。
现在我给大家普及一下JNI里面的数据类型和java里的数据类型的对应关系:
大家就这样理解就好了:
java里的boolean类型在jni里就是jboolean;
java里的int类型在jni里就是jint;
…
java里的Object在jni里就是jobject;
这是我写的一个生成Java里咱们定义的SchoolObj对象的一个方法。
咱们一行一行来分析。
#include <jni.h>
#include <string>
//extern "C" 作用是支持C和C++混合编译
extern "C" {
jstring
Java_com_example_hasee_myapplication_JniUtils_stringFromJNI(
JNIEnv *env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
//JNIEXPORT ,JNICALL在jni.h中定义的关键字,就是一个标记
/**
* *env 上篇说过,JNIENV里面封装了很多jni的方法
* instance 就是调用这个函数的那个对象,有两种情况,1,一个实体类调用时,instance就是这个实体类。2,这个函数在Java里是通过静态函数(static)声明的,instance就是这个Java类。
* 上面这两个参数是必须的,下面的的参数是咱们定义的,相传上面就传什么。
* Java类型和jni类型的映射关系不明白的请看我上一篇
*/
JNIEXPORT jobject JNICALL Java_com_example_hasee_myapplication_JniUtils_createSchool(JNIEnv *env,
jobject instance,
jstring schoolAddress_,
jint studentsCount,
jstring headMasterName_) {
//把咱们传进来的字符串转成c或c++可以识别的char类型指针;
// 这种写法是c++的写法,在创建项目的时候需要添加c++支持
//c的写法为 const char *schoolName = (*env).GetStringUTFChars(schoolName_, 0);
//记得用完之后要释放内存
const char *schoolName = env->GetStringUTFChars(schoolName_, 0);
const char *schoolAddress = env->GetStringUTFChars(schoolAddress_, 0);
const char *headMasterName = env->GetStringUTFChars(headMasterName_, 0);
//下面这几步就像Java里的反射一样
//1,咱们要创建一个对象,首先得知道类名吧(是全类名呦)
jclass schoolObjClass = env->FindClass("com/example/hasee/myapplication/SchoolObj");
//2,我总要知道你想用哪个构造函数吧,"<init>" 就是空参构造,"()V"是这个构造函数的签名,
// 大家可能还不会查看函数的签名:(1),首先编译Java文件生成class文件,找到这个class文件,复制该class文件的绝对路径
//(2),打开命令行,输入“javap -p -s (加刚才复制的内容)”请看图:
jmethodID methodID_SchoolObj = env->GetMethodID(schoolObjClass, "<init>", "()V");//找到java里面SchoolObj的无参构造函数,也可以是别的构造函数
//调用这个函数生成对象,记住生成的对象基类都是jobject
jobject SchoolObj = env->NewObject(schoolObjClass, methodID_SchoolObj);
//..........................到此这个对象就生成了,你完全可以把他给当返回值返回,就像这样 return SchoolObj;然后就可以在Java里用着个对象了。
//但是,对象生成了,接下来你要干嘛,我肯定要给属性赋值嘛,(除非你要的就是一个不需要给属性赋值的对象)
//注意了,反射又来了,思路很简单
// 要赋值,两个明确,
// 第一,明确给哪个属性辅助,那个属性什么类型的,
// 第二,明确我到底要给哪个对象赋值
//这是实现第一个明确,请看参数
// jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
//{ return functions->GetFieldID(this, clazz, name, sig); }
// 第一个参数clazz,就是上面env->FindClass()所得到的schoolObjClass;
// 第二个参数name,你所要给赋值的那个变量的变量名
// 第三个参数sig,这个变量的类型的签名这个签名也是上面提到的javap命令可以得到,下面是各类型的签名
// jboolean Z;
// jbyte B;
// jchar C;
// jshort S;
// jint I;
// jlong J;
// jfloat F;
// jdouble D;
// jobject L;(这个和基础类型有点区别,先是开头大写L来说明这是个object类型,后面紧接着是这个类的绝对路径,最后必须加";"号
// 如:String类型 "Ljava/lang/String;" ,
// 还有一种情况就是内部类用"$"隔开,比如A类有一个内部类B,要写成"LA$B;"
// jarray (这个也比较有特点,要在object的基础上加数组的标记"[" 如String[] : "[Ljava/lang/String;"
// 注意:这些东西不要去记,没必要,还容易记错,我们只要会通过命令获取就行了
jfieldID schoolNameId = env->GetFieldID(schoolObjClass, "schoolName", "Ljava/lang/String;");
jfieldID schoolAddressId = env->GetFieldID(schoolObjClass, "schoolAddress", "Ljava/lang/String;");
jfieldID studentCountId = env->GetFieldID(schoolObjClass, "studentCount", "I");
jfieldID headMasterId = env->GetFieldID(schoolObjClass, "headMaster", "Lcom/example/hasee/myapplication/SchoolObj$Person;");
//这是第二个明确的实现,给某个对象的某个变量赋值
env->SetObjectField(SchoolObj, schoolNameId, (jobject) schoolName_);
//我们看到schoolAddress_是jstring类型,怎么能用SetObjectField()这个方法,jni里,所有的对象都是jobject,jstring 也是 jobject,
//这个可以再jni.h里看出 class _jstring : public _jobject {};,所以可以进行强转。
env->SetObjectField(SchoolObj, schoolAddressId, (jobject) schoolAddress_);
env->SetIntField(SchoolObj, studentCountId, studentsCount);
//上面是c++的写法,当然也可以用c的写法
//(*env).SetObjectField(SchoolObj, schoolAddressId, (jobject) schoolAddress_);
//下面是给SchoolObj的headMaster赋值,因为headMaster是个实体类,所以不能直接想基本数据类型那样赋值,
// 我们也得先把这个对象给new出来,在把这个对象给headMaster变量赋值,还是上面的流程再走一遍
jclass PersonObjClass = env->FindClass("com/example/hasee/myapplication/SchoolObj$Person");
jmethodID methodID_Person = env->GetMethodID(PersonObjClass, "<init>", "()V");//找到java里面SchoolObj的无参构造函数,也可以是别的构造函数
jobject PersonObj = env->NewObject(PersonObjClass, methodID_Person);
jfieldID headMasterAgeId = env->GetFieldID(PersonObjClass, "age", "I");
jfieldID headMasterNameId = env->GetFieldID(PersonObjClass, "name", "Ljava/lang/String;");
env->SetIntField(PersonObj, headMasterAgeId, headMasterAge);
env->SetObjectField(PersonObj, headMasterNameId, (jobject) headMasterName_);
//一定看清是给哪个对象的哪个变量赋值
env->SetObjectField(SchoolObj,headMasterId,PersonObj);
//最后记得要释放内存
env->ReleaseStringUTFChars(schoolName_, schoolName);
env->ReleaseStringUTFChars(schoolAddress_, schoolAddress);
env->ReleaseStringUTFChars(headMasterName_, headMasterName);
//这里要记住, Android studio,若果你自己写的Java方法返回值不是void,那么编辑器会提醒你要返回值,
// 但是在写jni时却不会提示,所以要记得 return返回值,不然你得到的结果会很奇怪,有兴趣的可以打断点看看
return SchoolObj;
}
}
哈哈,是不是感觉jni和反射简直就是一个写法啊,也不难吧,下一篇咱们介绍一下jni稍微高级一点的用法