Android Jni(Java Native Interface)笔记

首先记录一个问题,关于如何用javah生成头文件。

为什么要生成头文件?在含有

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

这样代码的类下面定义方法,比如

    static public native String PrintHello();

那么再用javah生成头文件,h文件就会更新,含有这个方法的定义。

如何生成h文件?

windows下:javah -classpath bin/classes -d jni com.ggndktest1.JniGg

ubuntu下:这么写:

javah -classpath bin/classes -bootclasspath /home/larry/adt-bundle-linux-x86-20140702/sdk/platforms/android-20/android.jar -d jni com.example.hellojni.MainActivity

-classpath后面跟着的bin/classes指定的就是class的path,这里我是创建了一个HelloJni的项目,包名是com.example.hellojni,里面有一个Activity是MainActivity,我是在Hellojni目录下执行上面的命令的,所以要跟bin/classes指定类目录;

-bootclasspath我猜想是引导后面的android.jar这一参数用的。之前搜了很多答案都没有提到这个。大多数是直接用这个android.jar的路径后面上一个冒号,然后就跟了类的地址。

-d jni代表把生成的h文件放到根目录的jni文件夹里面。

另外一种方法,按照12楼的说法,我还试了把android.jar添加到classpath里去,不行,还是提示无法访问android.app.Activity,可能是我加classpath的方式不对?不得而知,不去整了。

图在这里:

Android Jni(Java Native Interface)笔记

下面重新开始学习JNI。。

----------------------------------------------------------------------------------

找到一篇这个人的笔记,觉得写得很好,就按照他的来试试吧。

代码敲好了,ndk-build的时候,出现错误:

/home/larry/android-ndk-r9d/toolchains/arm-linux-androideabi-4.6/prebuilt/linux-x86/bin/arm-linux-androideabi-g++:命令未找到

循着路径找过去发现linux-x86这个文件夹,原来大神下载的这个ndk试windows版本的。还要去下载么。。。android developers网站都被屏蔽了(可怜的天朝开发者。。)要去windows*下载。

发现一个问题,打*的中文,会被cnblogs转换成FQ。。可怜啊可怜,*下的这些企业也只能多一事不如少一事,忍气吞声,理解理解。

---------------------------------------------------------------------------------

9月在看雪上看到一篇文章也不错:http://www.cnblogs.com/hibraincol/archive/2011/05/30/2063847.html

概括一下编写ndk程序的流程:

1.在src中写Java代码,其中要loadLibrary。

2.编译Java代码(可以用Javac或Eclipes,用Eclipse导入代码的话会直接生成bin文件夹以及class。)

3.编写相应的C/C++代码。通常先用javah这个工具生成相应的.h文件,然后根据这个.h文件编写相应的C/C++代码。 然后可以删掉.h文件。(h文件是不是无关紧要)

4.编写Android.mk

-------------------------------------Sept.6th--------------------------------------------

举个例子:android.content.ContextWrapper里的getPackageName()方法,怎么转换成jni代码?
转换成cpp代码:

//获取包名
jmethodID methodID_packagename =env->GetMethodID(native_clazz,"getPackageName", "()Ljava/lang/String;");
jstring name_str = static_cast<jstring>(env->CallObjectMethod(thiz, methodID_packagename));

这段代码如果转换成c,要这样写:

//获取包名
jmethodID methodID_packagename =(*env)->GetMethodID(env,native_clazz,"getPackageName", "()Ljava/lang/String;");
jstring name_str = (*env)->CallObjectMethod(env,thiz, methodID_packagename);
return name_str;

也就是,要把C++中的env改成(*env),并且所有方法都要加一个参数env。
如果不改成(*env),会报错:request for member 'GetMethodID' is something not a struct or union.
如果改成了(*env)但没加一个参数env,会报错too few arguments to function '(*env)->GetObjectClass.

static_cast<jstring>是静态类型转换关键字,是 C++ 编译器的新特性,C 编译器不支持这个关键字。

-------------------------------------Oct.9th--------------------------------------------

 JNIEXPORT 和 JNICALL

这两个关键字其实是两个宏,在%JAVA_HOME%/include/win32/下找到jni_md.h,打开后里面有这样的东西:

#ifndef _JAVASOFT_JNI_MD_H_
#define _JAVASOFT_JNI_MD_H_ #define JNIEXPORT __declspec(dllexport)
#define JNIIMPORT __declspec(dllimport)
#define JNICALL __stdcall typedef long jint;
typedef __int64 jlong;
typedef signed char jbyte; #endif /* !_JAVASOFT_JNI_MD_H_ */

摘自msdn:在 32 位编译器版本中,可以使用 __declspec(dllexport) 关键字从 DLL 导出数据、函数、类或类成员函数。__declspec(dllexport) 将导出指令添加到对象文件。(.DLL对应.so?)

__stdcall调用约定用于调用Win32 API函数。

这两个关键字,有没有似乎都是无关紧要的?有待验证。

-------------------------------------Oct.15th--------------------------------------------

上面有个链接说JNIEXPORT 和 JNICALL两个关键字是jni的宏,Android里没用,可有可无。之前还讨论javah生成头文件的步骤是不是可有可无,目前的猜测是,在生成了.h并且include进来的时候,要在函数的返回值类型两侧加上这两个关键字,也就是跟.h文件生成的函数名保持一致;而若不include这个头文件则直接用返回类型 Java_包名_类名_方法名的次序命名。 

「方法的签名」

Java中允许方法的多态,仅仅是通过方法名并没有办法定位到一个具体的方法,因此需要一个字符串来唯一表示一个方法。但是怎么利用一个字 符串来表示方法的具体定义呢?JDK中已经准备好一个反编译工具javap,通过这个工具就可以得到类中每个属性、方法的签名。在CMD下运行javap -s -p -classpath c:\ test.Demo即可看到属性和方法的签名。如下图红色矩形框起来的字符串为方法String append(String str, int i)的签名。

Android Jni(Java Native Interface)笔记

后来发现,javap命令很坑,只能看部分的签名啊,比如一个类里面有一个函数,那这个函数的签名可以看到,但函数里面的各种东西它都不管了。。

还是直接看smali比较方便。比如:

DESKeySpec localDESKeySpec = new DESKeySpec(paramString.getBytes());

这个的初始化部分的smali:

invoke-direct {v1, v6}, Ljavax/crypto/spec/DESKeySpec;-><init>([B)V

反射调用:

clazz = (*env)->FindClass("javax.crypto.spec.DESKeySpec");
methodID = GetMethodID(env , clazz , "<INIT>","([B)V");//get到DESKeySpec的带参数构造函数,参数是Byte数组,返回类型Void;为什么不是DESKeySpec类型呢 是不是初始化都这样?答:看上面smali里面怎么写就怎么写。
localDESKeySpec = (*env)->NewObject(env , clazz , methodID);

GetMethodID的第三第四个参数跟smali中的很对应,有木有。

-------------------------------------Oct.16th--------------------------------------------

下面是360无线测试第一题的把Java的DES加密改写成JNI代码的例子,照着网上大神的例子自己写了一遍累成马,总算对如何把Java转换成JNI代码有了点理解。网上几乎搜不到这方面的内容,在一个Android JNI开发群提问他们好像也没人懂,的确,原本一行的Java代码要拆成好几段来写,而且逻辑性强,太费脑子了;而且写完了你调试更麻烦。为了安全或者是其他原因把Java全部改成C成本太大。Geohot去年的towelroot应用所有的代码全部放到了so里,可见他真的是神。

改写的过程中最好有Java和Smali作为对照,这样方便很多,参数什么的可以直接复制,比如

methodID = (*env) -> GetStaticMethodID(env , clazz , "getInstance" , "(Ljava/lang/String;)Ljavax/crypto/SecretKeyFactory;");

它的后两个参数直接可以从smali里面复制。

DES algorithm in Java:

  private byte[] decrypt(byte[] paramArrayOfByte, String paramString)
throws Exception
{
SecureRandom localSecureRandom = new SecureRandom();
DESKeySpec localDESKeySpec = new DESKeySpec(paramString.getBytes());
SecretKey localSecretKey = SecretKeyFactory.getInstance("DES").generateSecret(localDESKeySpec);
Cipher localCipher = Cipher.getInstance("DES");
localCipher.init(2, localSecretKey, localSecureRandom);
return localCipher.doFinal(paramArrayOfByte);
}

  

Transform it into test.c:

// #include "com.qihoo.crack.MainActivity.h"
#include <jni.h>
#include <stdio.h>
#include <string.h>
//Q:为什么此处即便只是调用方法,也写上返回类型
//A:自定义方法如果放在「主函数」的下面,需要进行函数说明。如果放在上面则无需。下面这行这起到声明作用。
jobject myNewObject(JNIEnv * env , char classname[]);//为什么是char不是jcharArray jbyteArray Java_com_qihoo_test_first_MainActivity_decrypt(JNIEnv * env , jobject obj , jbyteArray paramArrayOfByte,jstring key)//JNIEnv*,jobject是所有jni函数必有的两个参数,分别表示jni环境和对应的java类(或对象)本身
{
jclass clazz = NULL ;
jobject localSecureRandom = NULL ;
jobject localDESKeySpec = NULL ;
jbyteArray jb = NULL ;
jmethodID methodID = NULL;
jobject localSecretKeyTemp = NULL ;
jobject localSecretKey = NULL ;
jstring algorithm = NULL;
jobject localCipher = NULL ;
jint two = 2 ;
//SecureRandom localSecureRandom = new SecureRandom();无参构造初始化,对应下面一行即可
localSecureRandom = myNewObject( env , "java/security/SecureRandom" ); //DESKeySpec localDESKeySpec = new DESKeySpec(paramString.getBytes()); //有参数,不能用myNewObject()函数了;这句话分两部分来写,先写括号里的String2Byte的功能
clazz = (*env) -> FindClass(env,"java/lang/String");
methodID = (*env)->GetMethodID(env , clazz , "getBytes" , "()B");
jb = (jbyteArray)(*env)->CallObjectMethod(env , key , methodID);
//注意此处强制(jbyteArray) 把jb变成byteArray
//此处展示了String的实例key如何调用.getBytes()方法(CallObjectMethod)
//再写括号外的初始化对象
clazz = (*env) -> FindClass(env, "javax/crypto/spec/DESKeySpec");
methodID = (*env) -> GetMethodID(env , clazz , "<INIT>","([B)V");//get到DESKeySpec的带参数构造函数,参数是Byte数组,返回类型Void
localDESKeySpec = (*env)->NewObject(env , clazz , methodID , jb); //SecretKey localSecretKey = SecretKeyFactory.getInstance("DES").generateSecret(localDESKeySpec); algorithm = (*env) -> NewStringUTF(env, "DES");
clazz = (*env)->FindClass(env,"javax/crypto/SecretKeyFactory");
methodID = (*env) -> GetStaticMethodID(env , clazz , "getInstance" , "(Ljava/lang/String;)Ljavax/crypto/SecretKeyFactory;");//两个分号?
localSecretKeyTemp = (*env)->CallStaticObjectMethod(env , clazz , methodID , algorithm);//static Method //此处无需再写一个clazz,因为CallObjectMethod直接调用了上面的localSecretKeyTemp生成的类,exquisite!
methodID = (*env) -> GetMethodID(env , clazz , "generateSecret" , "(Ljava/security/spec/KeySpec;)Ljavax/crypto/SecretKey;");
localSecretKey = (*env)->CallObjectMethod(env , localSecretKeyTemp , methodID, localDESKeySpec ); //Cipher localCipher = Cipher.getInstance("DES");
clazz = (*env)->FindClass(env,"javax/crypto/Cipher");
methodID = (*env) -> GetStaticMethodID(env , clazz , "getInstance" , "(Ljava/lang/String;)Ljavax/crypto/Cipher;");
localCipher = (*env)->CallStaticObjectMethod(env , clazz , methodID , algorithm);//static Method //localCipher.init(2, localSecretKey, localSecureRandom);
methodID = (*env) -> GetMethodID(env , clazz , "init" , "(ILjava/security/Key;Ljava/security/SecureRandom;)V");
(*env)->CallObjectMethod(env , localCipher , methodID, two , localSecretKey , localSecureRandom );//参数依次排列 //return localCipher.doFinal(paramArrayOfByte);
methodID = (*env) -> GetMethodID(env , clazz , "doFinal" , "([B)[B");//class始终是Cipher
return (jbyteArray)(*env)->CallObjectMethod(env , localCipher , methodID, paramArrayOfByte );
} jobject myNewObject(JNIEnv * env , char classname[])
{
jclass clazz = NULL ;
clazz = (*env)->FindClass(env , classname);
if(clazz == NULL)return NULL ; jmethodID methodID = NULL ;
//GetMethodID()第三个参数<INIT>代表clazz的无参构造函数;第四个参数是signature,格式是"(L参数类型)返回类型"
methodID = (*env)->GetMethodID(env , clazz , "<INIT>" , "()V");
if(methodID == NULL )return NULL ;
//jobject对应任何Java对象,或者没有对应Java类型的对象
jobject obj = NULL ;
obj = (*env)->NewObject(env , clazz , methodID);
if(obj == NULL ) return NULL ;
else return obj ;
}

  

  

---------------------lib和libs-----------------------------

打包上面提到的第一题的时候提示findlibrary returned null,发现so没有打包进apk。将libs文件夹改名为lib解决。网上找不到相关的知识,这里试着总结一下:

1.ndk-build生成的so是在libs中的,要把libs中的so搬运到lib中去才能被Apktool识别并打包进apk。

2.apktool decode出来的so在lib文件夹里。这时候直接打包成apk也是可以的,说明apktool b 是可以识别lib中的so的。注意Android.mk只是在生成so时用到。

3.用Eclipse(ADT)来生成apk的话,要把so放到libs里。

--------------------jni_onLoad & init_array-----------------------------

当在系统中调用System.loadLibrary函数时,该函数会找到对应的动态库,然后首先试图找到"JNI_OnLoad"函数,如果该函数存在,则调用它。

另外,记得以前看过,Application是最先执行的,优先于launcher Activity。static{loadLibrary()};是优先执行的。现在有的apk直接把static{loadLibrary()};放到自定义的Application中去。如此一来,最先执行的就是"JNI_OnLoad"(如果有的话)。

JNI_OnLoad函数并不是最开始执行的。在JNI_OnLoad函数执行之前,还会执行init段和init_array中的一系列函数

-----------------------动态注册JNI:----------------------------------

jni方法直接在本地操作系统上执行,
java方法由dvm解释(interpret)之后执行。

除了用通过Java_<包名>_<类名>_<方法名>这种静态注册方法,还可以动态注册。

动态注册先绑定要调用的方法,然后指定注册的类(那个方法所在的类),然后registerNativeMethods。

-----------------2016 .01.05----------------------------

在Android Studio中要想在打包的时候把so打包进apk,需要在build.gradle的android{}标签下加入:

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

------------------可供参考的文章-------------------------

动态注册JNI

JNI数据类型

2015年1月27日读到鬼哥的一篇入门,如果当年自己读到这样一篇会少走不少弯路啊:http://blog.csdn.net/guiguzi1110/article/details/41980035

上一篇:JNI(java Native Interface)


下一篇:C#Random()函数详解