最近需要给小伙伴扫盲一下如何使用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.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());
}
/**
* 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;
}