佛涅盘之后,正法一千年,像法一千年,末法一万年。现在是末法时期…..。

开始我们的JNI开发之旅。我们知道,Java容易被反编译,C不容易被反编译(C更安全);android 底层最终还是Linux,在运行代码的性能上,C的性能远远高于Java的性能。这些因素都促使我们使用JNI。

NDK 配置

1: 先点击File > settings.. > android SDK 点击SDK Tools , 然后勾选 CMake, LLDB , NDK 三项, 点击OK,如下图所示:

下载完成之后 ,可以点击 :File > Project Structure 可以看到 NDK的下载路径,如下图所示:

Android hook修改方法 android jni hook_Java

2: 配置NDK环境变量; 打开计算机高级环境变量,然后新建系统变量,变量名 :NDK_ROOT ,变量值:填写上图中的NDK路径; 我的NDK路径是:C:\Users\John\AppData\Local\Android\Sdk\ndk-bundle,如下图所示:

Android hook修改方法 android jni hook_Java_02

然后打开Path系统变量,在末尾添加 ;%NDK_ROOT% , 确定保存。我们打开cmd,输入:ndk-build,出现下图所示,即表示配置成功。

Android hook修改方法 android jni hook_Android hook修改方法_03


至此 ,配置完成。

第一个JNI实例

1:创建NDKDemoProject工程,然后创建JNIFirst.java,代码如下:

package com.app.test.ndkdemoproject;

/**
 * Created by ${liumengqiang} on 2017/9/22.
 */

public class JNIFirst {
    static{
        //声明要加载的库,库名:MyJNIFirst  自己定义库名
        System.loadLibrary("MyJNIFirst");
    }
    //声明方法
    public native String getString();
}

2: 在app的build.gradle中的android > defaultConfig节点下添加如下代码:

ndk{
//            定义声明的库名一致
            moduleName "MyJNIFirst"
        }

3: 在gradle.properties中添加如下代码:

android.useDeprecatedNdk=true

rebuild 工程

4: 生成头文件;点击Build > Make Project 此时在工程的app > build > intermediates 目录下会出现classes文件夹;此时我们再点击View >Tool Windows > Terminal (是提供让我们输入命令行的),然后使用命令行进入到 app > build > intermediates > classes >debug 目录下,再执行命令:

javah -classpath . -jni com.app.test.ndkdemoproject.JNIFirst

此时会在debug目录下生成.h 头文件,如下图所示:

Android hook修改方法 android jni hook_ndk开发_04

注意: 使用完cd命令行之后,要退出到原始路径下,比如:现在是在app > build > intermediates > classes >debug目录下,应该一直执行命令行:cd.. 否则 你会看见一个很头疼得bug!这个下文会专门提到。

5:创建JNI文件夹,如下图所示:

Android hook修改方法 android jni hook_android_05

将上面生成的.h头文件复制粘贴到JNI文件夹下;然后再创建一个C文件,C文件名称要和头文件名称保持一致!!如下图所示:

C文件代码示下:

//
// Created by John on 2017/9/22.
//
#include <jni.h>
JNIEXPORT jstring JNICALL Java_com_app_test_ndkdemoproject_JNIFirst_getString
  (JNIEnv * env, jobject obj){
    return (*env)->NewStringUTF(env, "hello world C !");
  }

这里面是C代码(如果你是用的是C++代码,那就应该创建C++文件),看一下方法的命名:Java_JNIFirst文件的包名_JNIFirst ,至于里面的代码别问我是啥,我也不懂C。。。

此时rebuild工程

5:代码完毕了,此时就该在UI层愉快的玩耍JNI了,我在UI层弹了个Toast,

Toast.makeText(this, "" + (new JNIFirst()).getString(), Toast.LENGTH_SHORT).show();

效果如下:

Android hook修改方法 android jni hook_Java_06

编译生成SO文件

在实际开发中,基本上都是给你一个SO文件,然后调用里面的方法,接下来说一下怎么生成这些SO文件:

1: 在你创建的JNI目录下,创建Android.mk文件,代码示下:

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := JNIFirst

LOCAL_SRC_FILES := com_app_test_ndkjniproject_JNIFirst.c

include $(BUILD_SHARED_LIBRARY)

LOCAL_MODULE := MyJNIFirst 就是你上文中定义的库
LOCAL_SRC_FILES := com_app_test_ndkjniproject_JNIFirst.c 就是你的C文件(或者C++文件)

2: 同样在JNI目录下创建Application.mk文件,代码示下:

APP_STL := gnustl_static

APP_CPPFLAGS := -frtti -fexceptions -std=c++0x

APP_ABI := armeabi-v7a

APP_PLATFORM := android-25

APP_ABI := armeabi-v7a 也就是生成的你要生成哪些SO文件,如果生成所有的SO文件,应该改为 all

APP_PLATFORM := android-25 也就是根据你自己的AS填写了。

如下图所示:

Android hook修改方法 android jni hook_android_07

3: 使用Terminal中的命令行,进入到 你创建的JNI目录下,然后执行命令:

ndk-build

此时如果不出意外的话,将会在main目录下,看到多了libs和obj文件夹,libs文件夹下的文件也就是要生成的so文件!

注意:同理,使用cd.. 退出到原始目录下。

关于这些SO文件的使用,我们稍后再讲。

C调用Java方法

知道了Java调用C,接下来说一下C调用Java方法,一般来说分为两种:静态的方法和非静态的方法;我们基于上面的代码说一下这两种方法的调用区别。

1:首先在JNIFirst.java类中添加两个C方法:一个静态一个非静态;两个Java方法:一个静态一个非静态,代码示下:

package com.app.test.ndkdemoproject;

import android.util.Log;

/**
 * Created by ${liumengqiang} on 2017/9/22.
 */

public class JNIFirst {
    static{
        //声明要加载的库,库名:MyJNIFirst  自己定义库名
        System.loadLibrary("MyJNIFirst");
    }
    //java 调用C方法
    public native String getString();
    public native static String getStaticString();

    //用于C调用Java方法
    public native void cUseJava();
    public native static void cUseJavaStatic();

    //java方法
    public void show(){
        Log.e("------","C 调用了我,我是非静态方法!");
    }
    public void showStatic(){
        Log.e("++++++","C 调用了我,我是静态方法!");
    }
}

2: 重新生成头文件(与上面的步骤一致);上面的步骤在走一遍,不再赘述。

3: 生成头文件之后,还是复制粘贴到JNI文件夹下,这时打开头文件,会发现多了一个方法;此时再打开C文件,粘贴下述代码:

//
// Created by John on 2017/9/22.
//
#include <jni.h>
JNIEXPORT jstring JNICALL Java_com_app_test_ndkdemoproject_JNIFirst_getString
  (JNIEnv * env, jobject obj){
    return (*env)->NewStringUTF(env, "hello world C, I am not static!");
  }

JNIEXPORT jstring JNICALL Java_com_app_test_ndkdemoproject_JNIFirst_getStaticString
  (JNIEnv * env, jobject obj)){
    return (*env)->NewStringUTF(env, "hello world C , I am static !");
  }

4:此时Java调用C的代码已经完成了,那么C调用Java的代码呢? 首先需要科普一下,C调用Java代码需要用到反射,回想一下,在Java代码中的反射是:先findClass,然后getMethodID,最后使用,在这里都是一样的,只不过在获取methodID时,需要先获取到Java方法签名。
Java签名获取方法:使用Terminal命令行进入到 app>build>intermediates>classes>debug>目录下, 然后执行命令行:javap -s com/app/test/ndkdemoproject/JNIFirst,此时看到如下图即表示成功:

Android hook修改方法 android jni hook_ndk开发_08

descriptor:后面的就是方法签名,可以看到 show() 和showStatic()方法签名都是 “()V”;

最终我们的C文件代码为:

//
// Created by John on 2017/9/22.
//
#include <jni.h>
JNIEXPORT jstring JNICALL Java_com_app_test_ndkdemoproject_JNIFirst_getString
  (JNIEnv * env, jobject obj){
    return (*env)->NewStringUTF(env, "hello world C, I am not static!");
  }

JNIEXPORT jstring JNICALL Java_com_app_test_ndkdemoproject_JNIFirst_getStaticString
  (JNIEnv * env, jobject obj){
    return (*env)->NewStringUTF(env, "hello world C , I am static !");
  }

  /*
   * Class:     com_app_test_ndkdemoproject_JNIFirst
   * Method:    cUseJava
   * Signature: ()V
   */
  JNIEXPORT void JNICALL Java_com_app_test_ndkdemoproject_JNIFirst_cUseJava
    (JNIEnv * env, jobject obj){
    //获取Java类, 第二个参数:包名+ Java类名
    jclass clazz= (*env)->FindClass(env, "com/app/test/ndkdemoproject/JNIFirst");
    //获取类中的方法ID, 第三个参数是:Java方法名:
    //第四个参数是:方法签名
    jmethodID methodid = (*env)->GetMethodID(env, clazz, "show","()V");
    (*env)->CallVoidMethod(env,obj,methodid);
  };

  /*
   * Class:     com_app_test_ndkdemoproject_JNIFirst
   * Method:    cUseJavaStatic
   * Signature: ()V
   */
  JNIEXPORT void JNICALL Java_com_app_test_ndkdemoproject_JNIFirst_cUseJavaStatic
    (JNIEnv * env, jobject obj){
    //获取Java类
    jclass clazz= (*env)->FindClass(env, "com/app/test/ndkdemoproject/JNIFirst");
    //获取类中的方法ID
    jmethodID methodid = (*env)->GetStaticMethodID(env, clazz, "showStatic","()V");
    (*env)->CallStaticVoidMethod(env,obj,methodid);
  };

CallVoidMethod方法在这里我们没使用传递参数的,其实是可以传递参数的。CallVoidMethod内部的参数含义示下:

env-->JNIEnv 
obj-->通过本地方法穿过来的jobject 
mid-->要调用的MethodID(即第二步获得的MethodID) 
depth-->方法需要的参数(对应方法的需求,添加相应的参数)

可以看到GetStaticMethodID和GetMethodID是一对,也就是静态和非静态。

以下是非静态和静态的对数:

CallVoidMethod                   CallStaticVoidMethod
CallIntMethod                     CallStaticVoidMethod
CallBooleanMethod              CallStaticVoidMethod
CallByteMethod                   CallStaticVoidMethod

注意:静态C方法调用静态Java方法,非静态C方法调用非静态Java方法;

5:至此就可以在UI层玩耍了,UI层代码示下:

package com.app.test.ndkdemoproject;

import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity implements View.OnClickListener{

    private Button button1;
    private Button button2;
    private Button button3;
    private Button button4;
    private JNIFirst jniFirst;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button1 = (Button) findViewById(R.id.button1);
        button2 = (Button) findViewById(R.id.button2);
        button3 = (Button) findViewById(R.id.button3);
        button4 = (Button) findViewById(R.id.button4);
        button1.setOnClickListener(this);
        button2.setOnClickListener(this);
        button3.setOnClickListener(this);
        button4.setOnClickListener(this);
        jniFirst = new JNIFirst();
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.button1:{//java调用C非静态方法
                jniFirst.getString();
                break;
            }
            case R.id.button2:{//Java调用C静态方法
                jniFirst.getStaticString();
                break;
            }
            case R.id.button3:{//C调用Java非静态方法
                jniFirst.show();
                break;
            }
            case R.id.button4:{//C调用Java静态方法
                jniFirst.showStatic();
                break;
            }
        }
    }
}

运行结果示下:

Android hook修改方法 android jni hook_jni开发_09

Android hook修改方法 android jni hook_android_10

Android hook修改方法 android jni hook_ndk开发_11

Android hook修改方法 android jni hook_android_12

项目使用SO文件

1: 新建工程,将上文的SO文件copy到app > libs文件夹下;
2: 在app的build.gradle中添加如下代码:

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

3:新建与上述JNIFirst相同的包,复制JNIFirst到该包下中,注意:包名一致!!!下图所示:

Android hook修改方法 android jni hook_Android hook修改方法_13

4:集成完毕,在UI层可以尽情玩耍了。

JNI常见错误

本来有好多错误,但是手残,文档内容被写成别的东西了,还保存了。系统还原也不行。。。没办法了,记住多少写多少吧

错误1: Error:Execution failed for task ‘:app:clean’.

Unable to delete directory: D:\ElmBusiness\NDLTestSOProject\app\build\intermediates\classes\debug

这是因为你只进入该目录下执行命令行了,执行完了之后,没退出!!!这个问题网上的解决办法:删除build文件,是坑爹中坑爹办法。。

注意:如果你创建的是C文件,那就写C代码;如果你创建的是C++文件,那就写C++代码!