AndroidStudio下NDK开发流程

前言

使用Android Studio进行NDK开发时,可在创建Android Studio项目时选择创建C项目,创建好之后,默认会在src/main/下创建一个cpp的文件夹,C/C相关的文件就存放在这个文件夹中;在app下面的build.gradle中有NDK的相关配置

一、NDK开发进行文件加密解密

1、实现Java层native方法

  • 创建Android项目
  • 写一个含有加密和解密按钮的view
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="加密"
        android:onClick="mCrypt"/>

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="解密"
        android:onClick="mDecrypt"/>

</LinearLayout>
  • 编写一个实现对文件加密解密的java类: Crpytor.java
package com.example.ndk.filecrypt;


public class Cryptor {

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

    /**
     * 对文件进行加密
     * @param path 需要加密的文件路径
     * @return 加密后的文件路径
     */
    public native static void crypt(String path,String cryptPath);

    /**
     * 对文件进行解密
     * @param cryptPath 加密文件的路径
     * @return 解密后的文件路径
     */
    public native static void decrypt(String cryptPath,String decryptPath);
}
  • 在View对应的java类中实现相应的点击事件
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    /**
     * 加密的点击事件
     *
     * @param view
     */
    public void mCrypt(View view) {
        File sdDir = Environment.getExternalStorageDirectory();
        String sdpath = sdDir.getAbsolutePath();

        String path = sdpath + "/ndk.jpg";
        String cryptPath = sdpath + "/ndk_crypt.jpg";
        Cryptor.crypt(path, cryptPath);
        Toast.makeText(this, "加密完成", Toast.LENGTH_SHORT).show();
    }

    /**
     * 解密的点击事件
     *
     * @param view
     */
    public void mDecrypt(View view) {
        File sdDir = Environment.getExternalStorageDirectory();
        String sdpath = sdDir.getAbsolutePath();

        String cryptPath = sdpath + "/ndk_crypt.jpg";
        String decryptPath = sdpath + "/ndk_decrypt.jpg";
        Cryptor.decrypt(cryptPath, decryptPath);
        Toast.makeText(this, "解密完成", Toast.LENGTH_SHORT).show();
    }
}

2、使用 javah 命令生成头文件

  • 执行javah命令,因为AndroidStudio使用的是UTF-8的编码,所以在执行javah命令时需要指定编码为UTF-8(默认为GBK);
javah -encoding UTF-8 com.example.ndk.filecrypt.Cryptor
  • 生成 com_example_ndk_filecrypt_Cryptor.h 文件

3、创建JNI/CPP目录,添加NDK本地支持

  • 在src/main目录下创建jni/cpp目录,将刚刚生成的.h文件复制到该目录下
  • 在项目配置中设置 Android NDK location 目录,需要提前下载 NDK 相关支持 (在Android SDK 下载中选择 SDK Tools 中的 LLDB 、CMake 、NDK 三项进行下载)
  • 在jni 目录下创建 CMakeLists.txt 文件
# cmake 版本
cmake_minimum_required(VERSION 3.4.1)

# 添加支持
add_library( # 为library设置名称
             cryptor

             # 设置该library为共享的
             SHARED

             # 提供C/C++相关文件的相对路径
             cryptor.c )


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 )

target_link_libraries( # Specifies the target library.与上面add_library的名称相同
                       cryptor

                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib} )
  • 在 app目录下的build.gradle 文件中配置jni 目录,以及NDK支持
android {
    
    defaultConfig {
        // NDK的配置
        ndk{
             moduleName "cryptor"
            abiFilters  "armeabi-v7a", "x86"
        }
    }

    sourceSets {
        //配置jni目录
        main {
            jni.srcDirs = []
        }
    }
    
    externalNativeBuild {
        //配置cmake文件路径
        cmake {
            path file('src/main/jni/CMakeLists.txt')
        }
    }
   
}

4、实现头文件中定义的函数

#include "com_example_ndk_filecrypt_Cryptor.h"
#include <string.h>

char password[] = "qazwsxedc";
//加密
char *crypt(char normal_path[], char crypt_path[]) {
    //打开文件
    FILE *normal_fp = fopen(normal_path, "rb");
    FILE *crypt_fp = fopen(crypt_path, "wb");
    //一次读取一个字符
    int ch;
    int i = 0;
    int pwd_len = strlen(password);
    while ((ch = fgetc(normal_fp)) != EOF) {//End of FILE
        //加密
        fputc(ch ^ password[i % pwd_len], crypt_fp);
        i++;
    }
    fclose(normal_fp);
    fclose(crypt_fp);
}

//解密
char *decrypt(char crypt_path[], char decrypt_path[]) {
    //打开文件
    FILE *crypt_fp = fopen(crypt_path, "rb");
    FILE *decrypt_fp = fopen(decrypt_path, "wb");
    //一次读取一个字符
    int ch;
    int i = 0;
    int pwd_len = strlen(password);
    while ((ch = fgetc(crypt_fp)) != EOF) {//End of FILE
        //加密
        fputc(ch ^ password[i % pwd_len], decrypt_fp);
        i++;
    }
    fclose(crypt_fp);
    fclose(decrypt_fp);
}



/**
 * 加密
 * @param env
 * @param cls
 * @param jstr
 * @return
 */
JNIEXPORT void JNICALL Java_com_example_ndk_filecrypt_Cryptor_crypt
        (JNIEnv *env, jclass cls, jstring normal_path_str, jstring crypt_path_str) {
    char *path = (*env)->GetStringUTFChars(env, normal_path_str, NULL);
    char *crypt_path = (*env)->GetStringUTFChars(env, crypt_path_str, NULL);
    crypt(path, crypt_path);
    //对变量的内存进行释放
    (*env)->ReleaseStringChars(env, normal_path_str, path);
    (*env)->ReleaseStringChars(env, crypt_path_str, crypt_path);
}


/**
 * 解密
 * @param env
 * @param cls
 * @param jstr
 * @return
 */
JNIEXPORT void JNICALL Java_com_example_ndk_filecrypt_Cryptor_decrypt
        (JNIEnv *env, jclass cls, jstring crypt_path_str, jstring decrypt_path_str) {
    char *crypt_path = (*env)->GetStringUTFChars(env, crypt_path_str, NULL);
    char *decrypt_path = (*env)->GetStringUTFChars(env, decrypt_path_str, NULL);
    decrypt(crypt_path, decrypt_path);
    //对变量的内存进行释放
    (*env)->ReleaseStringChars(env, crypt_path_str, crypt_path);
    (*env)->ReleaseStringChars(env, decrypt_path_str, decrypt_path);
}


5、编译生成.so动态库

  • 点击Build -> Make Module app
  • 可以在app -> build -> intermediates -> cmake -> debug -> obj 下看到对应的armeabi-v7a 和 x86 的so库

6、加载.so动态库,运行程序

  • 在src/main 目录下创建 jniLibs 文件夹
  • 将上面生成的 armeabi-v7a 和 x86 的so库 复制到该 jniLibs 文件夹内
  • 运行程序

二、NDK开发进行文件拆分与合并

1、实现Java层native方法

  • 创建Android项目
  • 写一个含有加密和解密按钮的view
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="拆分"
        android:onClick="mDiff"/>

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="合并"
        android:onClick="mPatch"/>

</LinearLayout>
  • 编写一个实现对文件拆分合并的java类: FilePatchUtils.java
package com.example.ndk.filepatch
public class FilePatchUtils {
    static {
        System.loadLibrary("filepatch-lib");
    }

    /**
     * 对文件进行拆分
     * @param path  文件路径
     * @param count 拆分成多少个
     */
    public native static void diff(String path,String pathPattern,int count);

    /**
     * 对文件进行合并
     * @param pathPattern 需合并文件路径(%d)
     * @param count 将多少个文件合并
     * @param patchPath 合并后文件路径
     */
    public native static void patch(String pathPattern,int count,String patchPath);
}
  • 在View对应的java类中实现相应的点击事件
public class FilePatchActivity extends AppCompatActivity {

    private String sdpath;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_file_patch);
        File sdDir = Environment.getExternalStorageDirectory();
        sdpath = sdDir.getAbsolutePath();
    }

     /**
     * 文件拆分
     *
     * @param view
     */
    public void mDiff(View view) {
        String path = sdpath + File.separatorChar + "ndk.jpg";
        String pathPattern = sdpath + File.separatorChar + "ndk_%d.jpg";
        FilePatchUtils.diff(path, pathPattern, 3);
        Toast.makeText(this, "拆分完成", Toast.LENGTH_SHORT).show();
    }

    /**
     * 文件合并
     *
     * @param view
     */
    public void mPatch(View view) {
        String patchPath = sdpath + File.separatorChar + "ndk_patch.jpg";
        String pathPattern = sdpath + File.separatorChar + "ndk_%d.jpg";
        FilePatchUtils.patch(pathPattern, 3, patchPath);
        Toast.makeText(this, "合并完成", Toast.LENGTH_SHORT).show();
    }
}

2、使用 javah 命令生成头文件

  • 执行javah命令,因为AndroidStudio使用的是UTF-8的编码,所以在执行javah命令时需要指定编码为UTF-8(默认为GBK);
javah -encoding UTF-8 com.example.ndk.filepatch.FilePatchUtils
  • 生成 com_example_ndk_filepatch_FilePatchUtils.h 文件

3、创建JNI目录,添加NDK本地支持

  • 在src/main目录下创建jni目录,将刚刚生成的.h文件复制到该目录下
  • 在项目配置中设置 Android NDK location 目录,需要提前下载 NDK 相关支持 (在Android SDK 下载中选择 SDK Tools 中的 LLDB 、CMake 、NDK 三项进行下载)
  • 在jni 目录下创建 CMakeLists.txt 文件;同上;
  • 在 app目录下的build.gradle 文件中配置jni 目录,以及NDK支持;同上;

4、实现头文件中定义的函数

#include "com_example_ndk_filepatch_FilePatchUtils.h"
#include <android/log.h>

#define LOGI(FORMAT, ...) __android_log_print(ANDROID_LOG_INFO,"FILE_PATCH",FORMAT,__VA_ARGS__);
#define LOGE(FORMAT, ...) __android_log_print(ANDROID_LOG_ERROR,"FILE_PATCH",##FORMAT,__VA_ARGS__);

/**
 * 获取文件大小
 * @param path 文件路径
 * @return 文件大小
 */
long get_file_size(char *path) {
    FILE *fp = fopen(path, "r");
    if (fp == NULL) {
        return 0;
    }
    fseek(fp, 0, SEEK_END);
    return ftell(fp);
}

/*
 * Class:     com_example_ndk_filepatch_FilePatchUtils
 * Method:    diff
 * Signature: (Ljava/lang/String;Ljava/lang/String;I)V
 */
JNIEXPORT void JNICALL Java_com_example_ndk_filepatch_FilePatchUtils_diff
        (JNIEnv *env, jclass cls, jstring file_path_str, jstring pattern_str, jint file_count) {
    const char *path = (*env)->GetStringUTFChars(env, file_path_str, NULL);
    const char *pattern = (*env)->GetStringUTFChars(env, pattern_str, NULL);

    //得到分割之后的文件的路径列表
    char **filePaths = malloc(sizeof(char *) * file_count);
    //读取path对应路径,循环写入子文件
    int i = 0;
    for (; i < file_count; i++) {
        //给元素开辟空间
        filePaths[i] = malloc(sizeof(char) * 100);
        //给元素赋值
        sprintf(filePaths[i], pattern, (i + 1));
        LOGI("patch path:%s", filePaths[i]);
    }
    //分割文件
    FILE *fp = fopen(path, "rb");
    int fileSize = get_file_size(path);
    if (fileSize % file_count == 0) {
        //能整除
        int part = fileSize / file_count;
        //逐一写入分割子文件中
        int i = 0;
        for (; i < file_count; i++) {
            FILE *fwp = fopen(filePaths[i], "wb");
            int j = 0;
            for (; j < part; j++) {
                fputc(fgetc(fp), fwp);
            }
            fclose(fwp);
        }
        fclose(fp);
    } else {
        //不能整除
        int part = fileSize / (file_count - 1);
        //逐一写入分割子文件中
        int i = 0;
        for (; i < file_count - 1; i++) {
            FILE *fwp = fopen(filePaths[i], "wb");
            int j = 0;
            for (; j < part; j++) {
                fputc(fgetc(fp), fwp);
            }
            fclose(fwp);
        }
        part = fileSize % (file_count - 1);
        if (part > 0) {
            FILE *fwp = fopen(filePaths[file_count - 1], "wb");
            int j = 0;
            for (; j < part; j++) {
                fputc(fgetc(fp), fwp);
            }
            fclose(fwp);
        }
        fclose(fp);
    }
    //释放
    i = 0;
    for (; i < file_count; i++) {
        free(filePaths[i]);
    }
    free(filePaths);
    //对变量的内存进行释放
    (*env)->ReleaseStringChars(env, file_path_str, path);
    (*env)->ReleaseStringChars(env, pattern_str, pattern);
}

/*
 * Class:     com_example_ndk_filepatch_FilePatchUtils
 * Method:    patch
 * Signature: (Ljava/lang/String;Ljava/lang/String;I)V
 */
JNIEXPORT void JNICALL Java_com_example_ndk_filepatch_FilePatchUtils_patch
        (JNIEnv *env, jclass cls, jstring pattern_str, jint count, jstring patch_path_str) {
    char *pattern = (*env)->GetStringUTFChars(env, pattern_str, NULL);
    char *patch_path = (*env)->GetStringUTFChars(env, patch_path_str, NULL);
    FILE *fwp = fopen(patch_path, "wb");
    int i = 0;
    for (; i < count; i++) {
        //单个文件逐一写入fwp
        char *path = malloc(sizeof(char) * 100);
        sprintf(path, pattern, (i + 1));
        LOGI("patch path:%s", path);
        FILE *frp = fopen(path, "rb");
        //判断文件是否为NULL
        if (frp != NULL) {
            //获取单个文件大小
            int file_size = get_file_size(path);
            int j = 0;
            for (; j < file_size; ++j) {
                fputc(fgetc(frp), fwp);
            }
            fclose(frp);
        }
        //释放分配的内存空间
        free(path);
    }
    fclose(fwp);
    //对变量的内存进行释放
    (*env)->ReleaseStringChars(env, pattern_str, pattern);
    (*env)->ReleaseStringChars(env, patch_path_str, patch_path);
}

5、编译生成.so动态库

  • 点击Build -> Make Module app
  • 可以在app -> build -> intermediates -> cmake -> debug -> obj 下看到对应的armeabi-v7a 和 x86 的so库

6、加载.so动态库,运行程序

  • 在src/main 目录下创建 jniLibs 文件夹
  • 将上面生成的 armeabi-v7a 和 x86 的so库 复制到该 jniLibs 文件夹内
  • 运行程序