一. 准备工作

ndk下载:https://developer.android.google.cn/ndk/downloads/

  1. 首先配置终端的ndk命令:
  • 启动终端Terminal
  • 输入cd~,进入当前用户的home目录
  • 如果没有.bash_profile文件,先输入touch .bash_profile进行创建
  • 输入open -e .bash_profile 编辑.bash_profile文件,加入下面语句
  • export PATH=${PATH}:“这里填你的ndk安装路径”
  • 保存并关闭.bash_profile文件
  • 输入source .bash_profile,更新刚刚配置的环境变量
  • 命令行输入ndk-build,验证是否配置成功,出现下图表示配置成功

iOS so库使用 so库ios版下载_android

  1. 另外,Android Studio中也不要忘记设置(一般会默认设置成Android Sdk中的ndk路径,但是自己如果下载了最新的ndk,可以替换),打开File --> Project Structure:

 

二. JNI文件编写

  1. 创建一个含有native方法的java文件:
package com.hyf.kaviewer.jni;

/**
 * Created by heyf on 2019/2/28
 */
public class JniTest {

    public static native void getSecret();
}
  1. 通过javah命令生成.h头文件
    首先需要进入到当前项目当前module的java路径下:
    cd /Users/admin/Demo-Project/KAViewer/app/src/main/java
    然后输入:
    javah -d ../jni com.hyf.kaviewer.jni.JniTest

生成的.h文件如下所示(jni路径也是自动生成)

iOS so库使用 so库ios版下载_so库_02

.h文件的内容为:

iOS so库使用 so库ios版下载_android_03

红框中的方法与java文件中所写的方法相对应,我们马上也会在.cpp文件中实现它。

  1. 创建.cpp文件
    复制一份.h文件并将后缀改为.cpp

接下来编辑.cpp文件的内容:

iOS so库使用 so库ios版下载_iOS so库使用_04

iOS so库使用 so库ios版下载_so库_05

 具体代码如下:

#include <android/log.h>
#include <string.h>
#include "com_hyf_kaviewer_jni_JniTest.h"

#define LOGI(...) ((void)__android_log_print(ANDROID_LOG_INFO, "security", __VA_ARGS__))
#define LOGE(...) ((void)__android_log_print(ANDROID_LOG_ERROR, "security", __VA_ARGS__))

static const char *SIGN = "ABCDEFGHIJKLMN";
static const char *SECRET = "1111111111111111111";
static int verifySign(JNIEnv *env);

jstring Java_com_hyf_kaviewer_jni_JniTest_getSecret(JNIEnv *env, jclass type){
      return env->NewStringUTF(SECRET);
}

jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *env = NULL;
    if (vm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) {
        return JNI_ERR;
   }
    if (verifySign(env) == JNI_OK) {
        return JNI_VERSION_1_4;
   }
    LOGE("签名不一致!");
    return JNI_ERR;
}

static jobject getApplication(JNIEnv *env) {
    jobject application = NULL;
    jclass activity_thread_clz = env->FindClass("android/app/ActivityThread");
    if (activity_thread_clz != NULL) {
        jmethodID currentApplication = env->GetStaticMethodID(
                activity_thread_clz, "currentApplication", "()Landroid/app/Application;");
        if (currentApplication != NULL) {
            application = env->CallStaticObjectMethod(activity_thread_clz, currentApplication);
       } else {
            LOGE("Cannot find method: currentApplication() in ActivityThread.");
       }
        env->DeleteLocalRef(activity_thread_clz);
   } else {
        LOGE("Cannot find class: android.app.ActivityThread");
   }

    return application;
}

static int verifySign(JNIEnv *env) {
    // Application object
    jobject application = getApplication(env);
    if (application == NULL) {
        return JNI_ERR;
   }
    // Context(ContextWrapper) class
    jclass context_clz = env->GetObjectClass(application);
    // getPackageManager()
    jmethodID getPackageManager = env->GetMethodID(context_clz, "getPackageManager",
                                                   "()Landroid/content/pm/PackageManager;");
    // android.content.pm.PackageManager object
    jobject package_manager = env->CallObjectMethod(application, getPackageManager);
    // PackageManager class
    jclass package_manager_clz = env->GetObjectClass(package_manager);
    // getPackageInfo()
    jmethodID getPackageInfo = env->GetMethodID(package_manager_clz, "getPackageInfo",
                                                "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");
    // context.getPackageName()
    jmethodID getPackageName = env->GetMethodID(context_clz, "getPackageName",
                                                "()Ljava/lang/String;");
    // call getPackageName() and cast from jobject to jstring
    jstring package_name = (jstring) (env->CallObjectMethod(application, getPackageName));
    // PackageInfo object
    jobject package_info = env->CallObjectMethod(package_manager, getPackageInfo, package_name, 64);
    // class PackageInfo
    jclass package_info_clz = env->GetObjectClass(package_info);
    // field signatures
    jfieldID signatures_field = env->GetFieldID(package_info_clz, "signatures",
                                                "[Landroid/content/pm/Signature;");
    jobject signatures = env->GetObjectField(package_info, signatures_field);
    jobjectArray signatures_array = (jobjectArray) signatures;
    jobject signature0 = env->GetObjectArrayElement(signatures_array, 0);
    jclass signature_clz = env->GetObjectClass(signature0);

    jmethodID toCharsString = env->GetMethodID(signature_clz, "toCharsString",
                                               "()Ljava/lang/String;");
    // call toCharsString()
    jstring signature_str = (jstring) (env->CallObjectMethod(signature0, toCharsString));

    // release
    env->DeleteLocalRef(application);
    env->DeleteLocalRef(context_clz);
    env->DeleteLocalRef(package_manager);
    env->DeleteLocalRef(package_manager_clz);
    env->DeleteLocalRef(package_name);
    env->DeleteLocalRef(package_info);
    env->DeleteLocalRef(package_info_clz);
    env->DeleteLocalRef(signatures);
    env->DeleteLocalRef(signature0);
    env->DeleteLocalRef(signature_clz);

    const char *sign = env->GetStringUTFChars(signature_str, NULL);
    if (sign == NULL) {
        LOGE("分配内存失败");
        return JNI_ERR;
   }

    LOGI("应用中读取到的签名为:%s", sign);
    LOGI("native中预置的签名为:%s", SIGN);
    int result = strcmp(sign, SIGN);
    // 使用之后要释放这段内存
    env->ReleaseStringUTFChars(signature_str, sign);
    env->DeleteLocalRef(signature_str);
    if (result == 0) { // 签名一致
        return JNI_OK;
   }

    return JNI_ERR;
}

为什么要在so文件中验证签名?是为了让我们的so库只能被我们想要的应用去调用,因此在so库中会验证预置的签名与当前调用so库的应用的签名是否一致,防止特定so库的滥用,进一步保证安全。

  1. 获取应用签名
/**
     * 展示了如何用Java代码获取签名
     */
    private String getSign() {
        try {
            // 下面几行代码展示如何任意获取Context对象,在jni中也可以使用这种方式
//           Class<?> activityThreadClz = Class.forName("android.app.ActivityThread");
//           Method currentApplication = activityThreadClz.getMethod("currentApplication");
//           Application application = (Application) currentApplication.invoke(null);
//           PackageManager pm = application.getPackageManager();
//           PackageInfo pi = pm.getPackageInfo(application.getPackageName(), PackageManager.GET_SIGNATURES);

            PackageManager pm = getPackageManager();
            PackageInfo pi = pm.getPackageInfo(getPackageName(), PackageManager.GET_SIGNATURES);
            Signature[] signatures = pi.signatures;
            Signature signature0 = signatures[0];
            String str = signature0.toCharsString();
            Log.i(TAG, "getSign: " + signature0.toCharsString());
            return signature0.toCharsString();
       } catch (Exception e) {
            e.printStackTrace();
            return "";
       }
   }

并在build.gradle中加入:

signingConfigs {
       debug {
           storeFile file("../xx.keystore")
           storePassword "xxxxxx"
           keyAlias "xx"
           keyPassword "xxxxxx"
       }
   }

可以用上述代码获取签名(可以将方法放入启动Activity中,运行后在log中获取签名)

  1. 在jni目录下创建Android.mk文件
    写入以下配置:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_LDLIBS    := -lm -llog      //这一句是为了支持.cpp文件中的Log

LOCAL_MODULE := security
LOCAL_SRC_FILES := com_hyf_kaviewer_jni_JniTest.cpp

include $(BUILD_SHARED_LIBRARY)

需要注意的是:

  • LOCAL_MODULE := security ,security是将要生成的so库的名字,可以按需要改成自己想要的
  • LOCAL_SRC_FILES := com_hyf_kaviewer_jni_JniTest.c ,此处需要替换成自己的c文件
  1. 在jni目录下创建Application.mk文件
    写入以下配置:
APP_ABI := all

该配置表示会生成所有主流ABI类型的so库

至此,所需的文件已创建完毕:

iOS so库使用 so库ios版下载_so库_06

  1. 在当前Module下的build.gradle下增加配置
    在 defaultConfig{} 下增加以下配置:
ndk {
   moduleName "security" 
}
  1. 最后生成so库
  • 在终端中,cd到jni目录下
  • 输入ndk-build

iOS so库使用 so库ios版下载_iOS so库使用_07

  • 成功后打印如下所示

iOS so库使用 so库ios版下载_so库_08

  • 生成so库的路径如下图

iOS so库使用 so库ios版下载_java_09

  1. PS:在使用so库的时候,方法调用处的路径和.h中的路径名一定要一致(即包名和文件名),否则会报错,因为编译器是根据这个路径去调用so库的代码