Android 生成SO - 基础工程创建

最近需要给小伙伴扫盲一下如何使用Android Studio 生成一个SO文件,网上找了很多都没有合适的样例,那只能自己来写一个了。

原先生成SO是一个很麻烦的事情,现在Android Studio帮忙做了很多的事情,基本只要管好自己的C代码即可。

创建工程

  • C++ Standard :使用下拉列表选择你希望使用哪种 C++ 标准。选择 Toolchain Default 会使用默认的 CMake 设置。

 创建后报错的问题

 这个是由于我默认使用的 java 1.8 ,需要至少升级到 java11

创建完成后的工程样式

工程解析 

native-lib.cpp

这个工程我是这样理解的,native-lib.cpp 是实际编写C++代码的部分,这里来定义方法

#include <jni.h>
#include <string>
#include <android/log.h>

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_myapplication_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    // 使用 android log输出日志
    __android_log_print(ANDROID_LOG_INFO, "log", "Hello log from JNI function");
    return env->NewStringUTF(hello.c_str());
}


// System.loadLibrary 时发起
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) {
    JNIEnv *env = NULL;
    jint result = -1;

    __android_log_print(ANDROID_LOG_INFO, "log", "Hello log from JNI_OnLoad");

    if (jvm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
        return -1;
    }

    // 从C 层去调用 java的方法
    jclass clazz = env->FindClass("com/example/myapplication/MainActivity");
    if (clazz == NULL) {
        __android_log_print(ANDROID_LOG_ERROR, "log", "Cannot find SampleClass");
        return -1;
    }
    jmethodID methodId = env->GetStaticMethodID(clazz, "getJavaString", "()Ljava/lang/String;");
    if (methodId == NULL) {
        __android_log_print(ANDROID_LOG_ERROR, "log", "Cannot find sampleMethod");
        return -1;
    }
    /**
     *  这行代码是在 JNI 中调用 Java 类的静态方法,并将返回值转换为 jstring 类型的对象。

        env->CallStaticObjectMethod(clazz, methodId) 是 JNI 中用于调用 Java 类的静态方法的函数。它接受三个参数:类引用 clazz、方法 ID methodId 和任何必要的参数列表。
        在这种情况下,我们调用了一个静态方法,并且没有传递其他参数,因此只传递了类引用 clazz 和方法 ID methodId。
        静态方法执行后会返回一个 Java 对象,因此我们使用 (jstring) 将其强制转换为 jstring 类型的对象,因为我们知道该方法返回的是字符串对象。
        最终,返回的 jstring 对象被赋值给名为 resultString 的变量,以便后续在 JNI 函数中进行处理或操作。
     */
    jstring resultString = (jstring)env->CallStaticObjectMethod(clazz, methodId);
    // 从 Java 字符串对象中获取一个指向 UTF-8 编码的 C 字符串的指针,并将其存储在名为 str 的 const char* 类型指针变量中。
    const char* str = env->GetStringUTFChars(resultString, NULL);
    __android_log_print(ANDROID_LOG_INFO, "log", "Output from Java: %s", str);
    // 释放由 GetStringUTFChars 函数获取的 UTF 字符串的内存
    env->ReleaseStringUTFChars(resultString, str);


    result = JNI_VERSION_1_6;
    return result;
}

extern "C" JNIEXPORT jstring JNICALL

在 JNI(Java Native Interface)中,extern "C" 用于指定 C++ 函数按照 C 语言的命名和调用约定来处理。JNIEXPORTJNICALL 是 JNI 提供的宏,通常用于声明 JNI 函数,这两个宏通常会展开为适合当前环境的修饰符。

  • extern "C" 告诉编译器按照 C 语言的规则处理函数 stringFromJNI
  • JNIEXPORT 表示该函数将被导出供 JNI 调用。
  • JNICALL 是一个宏,用于设置正确的调用约定。

include

上面 include 就是C当中引入相关库的地方。

这里我加了  #include <android/log.h> 用于使用Android log 方法:__android_log_print

这里不可以直接使用C原生的 printf("Hello log from JNI function\n")

因为在 Android 开发中,printf 输出的内容通常不会直接显示在 Logcat 中。Android 应用默认会将 stdoutstderr 重定向到 /dev/null,因此 printf 的输出不会在 Logcat 中出现

Java_com_example_myapplication_MainActivity_stringFromJNI

  • Java: 这个部分表示这是一个 JNI 函数的标识符,表明该函数是与 Java 代码进行交互的本机方法。

  • com_example_myapplication_MainActivity: 这部分指定了 Java 类的完整路径,即 com.example.myapplication.MainActivity。这个路径应该与包名和类名一致,使用下划线 _ 替代点号 .

  • stringFromJNI: 这部分是 Java 类中方法的名称,这个名称应该与 Java 类中定义的native方法名称一致。也就是 :public native String stringFromJNI();

CMakeLists.txt 

# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.

cmake_minimum_required(VERSION 3.22.1)

# Declares and names the project.

project("myapplication")

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.

add_library( # Sets the name of the library.
        myapplication

        # Sets the library as a shared library.
        SHARED

        # Provides a relative path to your source file(s).
        native-lib.cpp)

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

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)

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.
        myapplication

        # Links the target library to the log library
        # included in the NDK.
        ${log-lib})

这里逐条解析

  • cmake_minimum_required(VERSION 3.22.1)

这个指定了构建该项目所需的最低 CMake 版本为 3.22.1、

  • project("myapplication")

声明并命名项目为 "myapplication"

  • add_library

创建并命名一个库,设置为 SHARED 类型,并提供源代码文件的相对路径。在此示例中,创建了名为 myapplication 的共享库,并提供了 native-lib.cpp 源文件的路径。

  • find_library

在系统路径中搜索指定的预构建库,并将其路径存储为变量。在这里,查找名为 log 的 NDK 库,并将路径存储在 log-lib 变量中。

  • target_link_libraries

指定要链接到目标库的库。在这里,将 myapplication 目标库链接到 log NDK 提供的 log 库。


所以当你需要改变生成的so的名称时,需要改动 add_library中的myapplication以及关联target_link_libraries中的值,不然报错。

同时,可以看到 find_library 作用是引入log库,如果不使用log,那么这个就并不是必须的,如果我在native-lib.cpp 中不使用那句  __android_log_print ,那么就可以精简为:

cmake_minimum_required(VERSION 3.22.1)

project("myapplication")

add_library(myapplication SHARED native-lib.cpp)

MainActivity

package com.example.myapplication

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;
import android.widget.TextView;

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.example.myapplication.databinding.ActivityMainBinding

public class MainActivity extends AppCompatActivity {

    // Used to load the 'myapplication' library on application startup.
    static {
        System.loadLibrary("myapplication");
    }

    private ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        // Example of a call to a native method
        TextView tv = binding.sampleText;
        tv.setText(stringFromJNI());
    }

    public static String getJavaString(){
        return "javaString 123";
    }

    /**
     * A native method that is implemented by the 'myapplication' native library,
     * which is packaged with this application.
     */
    public native String stringFromJNI();

}

这个其实没什么可以多说的,就是标准的调用,这里可以测试调用需要的代码。同时,因为so对 class的包名以及方法名固定的特性,使用so的地方也需要这里的代码。

SO编译

如果只是测试,可以直接run apk 。对应的文件会生成在如下位置。

此时,apk中只会包含和你测试设备相符合的so架构

但是当你需要多个架构时,需要在 build.gradle 中进行指定

然后直接编正式APK即可。

编完之后再apk 中可以获取到你需要的架构so

SO安全

这里额外聊一下这个事情,很多人觉得代码放在SO里面,别人不好反编译,更加的安全。

但是有一个盲点,就是别人在看完你的 Android 代码之后,读完你 native定义,可以直接使用你的so来进行操作。

例如你把关键的加密函数做成SO,明文 -> so -> 密文,或者 密文 -> so -> 明文,那别人直接调用你的so就解密了。特别是使用加固的项目,so很多都不加固,只加固java,这样反而不安全。

所以so至少要进行签名校验。当然so的校验也借助 java 的packagemanage,也就是容易被上层hook,这个我们有额外的对抗方案,这里不细说。

放一个so获取签名的代码在这里。具体的so代码后面有机会再开新坑

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_myapplication_MainActivity_getAppSignature(JNIEnv *env,jobject obj) {
    jclass context_class = env->FindClass("android/content/Context");
    jmethodID method_getPackageManager = env->GetMethodID(context_class, "getPackageManager",
                                                          "()Landroid/content/pm/PackageManager;");
    jmethodID method_getPackageName = env->GetMethodID(context_class, "getPackageName",
                                                       "()Ljava/lang/String;");

    jobject context = obj;
    jobject package_manager = env->CallObjectMethod(context, method_getPackageManager);
    jstring package_name = static_cast<jstring>(env->CallObjectMethod(context,
                                                                      method_getPackageName));

    jclass package_manager_class = env->GetObjectClass(package_manager);
    jmethodID method_getPackageInfo = env->GetMethodID(package_manager_class, "getPackageInfo",
                                                       "(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;");

    // 获取签名信息
    jobject package_info = env->CallObjectMethod(package_manager, method_getPackageInfo, package_name, 64);
    jclass package_info_class = env->GetObjectClass(package_info);
    jfieldID field_signatures = env->GetFieldID(package_info_class, "signatures",
                                                "[Landroid/content/pm/Signature;");
    jobjectArray signatures = (jobjectArray) env->GetObjectField(package_info, field_signatures);
    jobject signature = env->GetObjectArrayElement(signatures, 0);

    jclass signature_class = env->GetObjectClass(signature);
    jmethodID method_toCharsString = env->GetMethodID(signature_class, "toCharsString",
                                                      "()Ljava/lang/String;");
    jstring signature_str = (jstring) env->CallObjectMethod(signature, method_toCharsString);

    return signature_str;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值