Android Studio 1.3及以上 NDK环境配置

自从Android Studio1.3以后,在Android 环境开发JNI程序搭建开发环境变得相对简单。这里就来介绍一下急于Android Studio如何进行jni开发。

  首先准备基本工具,Android Studio (>=1.3.x), NDK(ndk-r10-e)。打开Android Studio 建立一个空工程,关联上NDK,操作步骤方式如下图:(注意是空工程,要不后面会很麻烦)

 

 

  设置好NDK之后,开始设置gradle,设置gradle主要需要设置三个地方,设置好之后就可以直接编写和编译JNI代码了,不需要像以前一样编写Makefile,相当方便。但是设置gradle也是需要比较小心的,由于当前NDK还处于Experimental 阶段,更新不断,经常会爆出各种奇怪的错误,因此也要特别留心。好了废话不多说,下面来介绍设置gradle的三个主要步骤。

  首先设置TopLevel gradle,也就是Project gradle,这里比较简单,在dependencies中设置:classpath 'com.android.tools.build:gradle-experimental:0.2.0' ,注意这里要把之前的类似classpath 'com.android.tools.build:gradle:1.3.0' 注释掉。还要多提一句的是,这里设置的是gradle-experimental:0.2.0,后面对应设置gradle wrapper的时候要对应gradle2.5-all 版本,这里先说到这里。

  接着设置 Module gradle,这一步是比较麻烦的。由于我们在创建工程的时候自动生成的这个gradle文件内容比较多,而且如果要使用NDK的话这个gradle变化比较大,这里直接贴出需要使用NDK的gradle,然后来进行说明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<span style= "font-size: 16px;" >  <br>apply plugin:  'com.android.model.application'
 
model {
     android {
         compileSdkVersion = 23
         buildToolsVersion =  "23.0.1"//改成你电脑的对应版本,这是我的版本
 
         defaultConfig.with {
             applicationId =  "com.zyp.ndktest"
             minSdkVersion.apiLevel  = 19   // Unable to load class com.android.build.gradle.managed.ProductFlavor_Impl
             targetSdkVersion.apiLevel = 23
             versionCode = 1
             versionName =  "1.0"
         }
 
     }
     android.buildTypes {
         release {
             minifyEnabled =  false
             proguardFiles += file( 'proguard-rules.pro' )
         }
     }
 
 
     compileOptions.with {
         sourceCompatibility = JavaVersion.VERSION_1_7
         targetCompatibility = JavaVersion.VERSION_1_7
     }
     android.ndk {
         moduleName =  "NdkSample"
         cppFlags +=  "-std=c++11"
         cppFlags +=  "-fexceptions"
         cppFlags +=  "-I${file(" src/main/jni //include")}".toString()
         ldLibs += [ "android" "log" ]
         stl =  "gnustl_shared"
     }
 
     android.productFlavors {
         create( "arm7" ) {
             ndk.abiFilters.add( "armeabi-v7a" )
         }
         create( "arm8" ) {
             ndk.abiFilters.add( "arm64-v8a" )
         }
 
     }
}
 
 
dependencies {
     compile fileTree(dir:  'libs' , include: [ '*.jar' ])
     compile  'com.android.support:appcompat-v7:23.0.1'//改成你电脑的对应版本,这是我的版本
</span>

  和自动生成的gradle相比,首先是 apply plugin: 'com.android.application' 变成了 apply plugin: 'com.android.model.application'。下面的配置也需要包装在model{}中。

  这个gradle的配置有几点需要注意的:

  1. 所有值的设置都要写成 xxx = yyyy的形式,比如: applicationId = "com.zyp.ndktest" (自动生成的gradle 则可能是: applicationId = "com.zyp.ndktest"  ),否则会爆这种错误:Error:Cause: org.gradle.api.internal.ExtensibleDynamicObject, 当出现此类错误,检查是否都用了 “=”的方式。

  2. buildTypes 需要从android{} 中取出来,写成android.buildTypes{}的形式,否则会出现这种错误:Error:Unable to load class 'org.gradle.nativeplatform.internal.DefaultBuildType_Decorated'.    

   此外,自动生成的buildTypes的形式和上面的也不一样为以下的形式:

1
2
3
4
5
<span style= "font-family: 'Microsoft YaHei'; font-size: 16px;" >          <br>release {
             minifyEnabled  false
             proguardFiles getDefaultProguardFile( 'proguard-android.txt' ),  'proguard-rules.pro'
         }
</span>

  需要改成上面文件中的格式,否则会报这种错误:Error:No signature of method: org.gradle.model.ModelMap.minifyEnabled() is applicable for argument types: (java.lang.Boolean) values: [false]     

  3. defaultConfig{} 需要写成defaultConfig.with{} 的形式,否则会报这种错误:Error:Cause: com.android.build.gradle.managed.AndroidConfig_Impl

  4. 在defaultConfig.with{} 中 需要写成 

    minSdkVersion.apiLevel  = 19  
    targetSdkVersion.apiLevel = 23

    也就是比自动生成的多 .apiLevel ,否则会报这种错误:Unable to load class com.android.build.gradle.managed.ProductFlavor_Impl

  5. 增加compileOptions.with{} 需要选择JavaVersion.VERSION_1_7,否则会报这种错误:Bad class file magic or version

  6. 最后一点,在gradleWrapper中使用的是2.5,则android.ndk {} 中类似cppFlags 的添加使用 += 的方式,否则需要使用 .add的方式

  以上可能遇到的问题我这里帮大家罗列出来,具体的请参考Google的文档,只不过这个文档需要FQ。

  最后设置gradle wrapper就好了,将左边的工程视图调整到Project,在gradle->wrapper->grale-wrapper.properties文件的最后设置:distributionUrl=https\://services.gradle.org/distributions/gradle-2.5-all.zip,注意这里如果在Project gradle中设置的是gradle-experimental:0.2.0,则这里选择gradle-2.5-all,如果是gradle-experimental:0.4.0,需要设置gradle-2.8-all。

我是去网站下载的services.gradle.org/distributions/gradle-2.5-all.zip这个包,解压到所在位置

  gradle设置完成之后就可以创建jni文件夹,然后编写Native代码了,创建好jni后一个工程的基本结构见下图:


搭建好基于Android Studio的环境之后,编写native代码相对来说也比较简单了。在Android上编写Native代码和在Linux编写C/C++代码还是有区别,Native代码一般需要与JVM交互数据,需要遵循一定的规范,本文来介绍一下基本的JNI代码写法。

  我们还是从实例出发,配置好Android Studio工程之后,我们需要创建jni目录和在jni目下创建c/c++文件和相应的头文件,创建方式见下图。

  在实例工程中我们创建了NdkSample.cpp 和 NdkSample.h,源码见下面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "NdkSample.h"
 
JNIEXPORT jstring JNICALL Java_com_zyp_ndktest_MainActivity_sayHello
         (JNIEnv *env, jclass cls, jstring j_str)
{
     const  char  *c_str = nullptr;
     char  buff[128] = {0};
     jboolean isCopy;   
     c_str = env->GetStringUTFChars(j_str, &isCopy);
     printf( "isCopy:%d\n" ,isCopy);
     if (c_str == NULL)
     {
         return  NULL;
     }
     printf( "C_str: %s \n" , c_str);
     sprintf(buff,  "hello %s" , c_str);
     env->ReleaseStringUTFChars(j_str, c_str);
     return  env->NewStringUTF(buff);
}  
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#ifndef NDKTEST_NDKSAMPLE_H
#define NDKTEST_NDKSAMPLE_H
 
#include "jni.h"
#include <stdio.h>
#include <string.h>
 
extern  "C"  {
JNIEXPORT jstring JNICALL
         Java_com_zyp_ndktest_MainActivity_sayHello(JNIEnv *env, jclass type,
                                                        jstring filename);
}
 
#endif //NDKTEST_NDKSAMPLE_H

  现在来简单介绍一下,首先是NdkSample.h文件,刚刚创建的时候只有相应的预处理命令,我们在头文件预处理命令之间加上 jni.h ,stdio.h ,string.h 后两个非必要。将我们要在java层调用的接口声明出来,放在extern "c"{} 中(告诉编译器按照C标准进行编译)。第一次接触jni的同学看到那么复杂的函数命名和奇怪的JNIEXPORT ,JNICALL,JNIEnv之类的估计有点不习惯,本文就不详细介绍它们的意思,其实你跟踪源码它们就是几个宏(JNIEnv是一个结构体保存当前环境的上下文),其它的jstring,jclass之类的很好理解就是在Native环境中对JVM中java对应结构的一种表示方式。

  函数命令方式是包名加activity名加函数名,表如我们在java层中的包名是java.com.zyp.ndktest,在MainActivity中调用sayHello函数,则jni层函数命名就要写成上面的方式。

  接下来看NdkSample.cpp文件中函数的定义。ni层的函数还需要多两个参数,一个是JNIEnv * ,一个是jclass。我们在java层调用的时候就只用传递前两个参数之外的参数。例子中我们想从java层传递一个String类型的参数到jni层,jni层从JVM中取数据的时候取到的却是jstring类型,在jni层我们不能直接使用需要转换。这里我们通过env->GetStringUTFChars(j_str, &isCopy)函数来完成,将j_str所在的地址转换并赋值给const char *类型的指针,之后我们就可以通过该指针访问那块内存了。注意这里是const char * 表示该指针指向的内存区域的内容是不可以改变的,java中的String 也是自带final属性的。GetStringUTFChars()实际上是将JVM内部的Unicode转化成为了C/C++认识的UTF-8的格式的字符串,注意这个函数内部发生了内存分配,相当于是拷贝了一份Unicode然后进行转化,所以后面需要ReleaseStringUTFChars()来释放内存。

  最后该函数返回一个新的构建好的jstring类型给java层,为了将C/C++层的UTF-8字符串转换为JVM中的Unicode字符串,需要调用另外一个函数NewStringUTF()来完成转换。

  我们注意到JVM中的内容jni中不能直接操作需要进行转换,jni中的内同也要进行转换,因此也有大量的相关jni接口存在,后面文章中会挑选一些来讲解。

  此外,函数中JNIEnv *env这个参数需要说一下,在C和C++中使用方式是不一样的,不要搞混了。在C中,看到JNIEnv 我们实质是取得了JNINativeInterface* (JNIEnv指针的指针),我们得使用**env获取结构体,从而才能使用结构体里面的方法。在C++中,看到JNIEnv我们实质是取得了JNIEnv*(JNIEnv结构体的指针),我们可以直接使用env->使用结构体里面的方法。注意我们调用GetStringUTFChars()的方式,但是注意和Java_com_zyp_ndktest_MainActivity_sayHello()进行区别。

  现在来看看我们在java层中如何调用jni层的接口,看下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.zyp.ndktest;
 
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
 
public  class  MainActivity extends AppCompatActivity {
 
     @Override
     protected  void  onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_main);
         String ret = sayHello( "zhuzhu" );
         Log.i( "JNI_INFO" , ret);
     }
 
     static  {
         System.loadLibrary( "NdkSample" );
     }
 
     public  native  static  String sayHello(String str);
}

  我们首先要通过System.loadLibrary()加载jni代码编译后生成的.so,但是这个库的名字怎么来的呢,注意回过头去看上一篇中gradle ndk{}中的内容,我们是在那里进行的命名的;然后还要声明native static 类型的该函数。然后直接调用就好了。运行结果见下图。

配置了一整天,基本配置好了。

猜你喜欢

转载自blog.csdn.net/jjdbear/article/details/50933522