最近需要给小伙伴扫盲一下如何使用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());
}
extern "C" JNIEXPORT jstring JNICALL
在 JNI(Java Native Interface)中,extern "C"
用于指定 C++ 函数按照 C 语言的命名和调用约定来处理。JNIEXPORT
和 JNICALL
是 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 应用默认会将 stdout
和 stderr
重定向到 /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.myapplicationimport 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.ActivityMainBindingpublic class MainActivity extends AppCompatActivity {// Used to load the 'myapplication' library on application startup.static {System.loadLibrary("myapplication");}private ActivityMainBinding binding;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);binding = ActivityMainBinding.inflate(getLayoutInflater());setContentView(binding.getRoot());// Example of a call to a native methodTextView tv = binding.sampleText;tv.setText(stringFromJNI());}/*** 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;
}