1. so文件
so文件是unix系统的动态连接库,是二进制文件,作用相当于windows下的.dll文件。so是一种ELF格式的可执行文件,一种c\c++可执行文件(这里区别于插件so)。Android使用的so是通过对c\c++交叉编译生成的。因为是native代码,所以对so的执行依赖于系统类型,因此,Android so运行时需要对应的cpu指令的版本。但是一般都向下兼容, 比如咱们常见的arm版本有 'armeabi-v7a', 'arm64-v8a' 这些,还有一些x86的版本。需要的时候咱们编译出对应的版本,放进apk对应的版本目录下即可。
so文件细节可以看 blog.csdn.net/zplxl99/art… www.jianshu.com/p/b8d25a2d9…
2. Android.mk
早期ndk的库都才用这种方式编译,编译的时候,只需下载ndk编译包 developer.android.google.cn/ndk/downloa… 通命令: ndkbuild [path] path目录下包含Android.mk 文件即可,对于有些同学需要研究一些老版本的代码,可能需要用到这方面的知识,或者是将对应的文件改成下面一节咱们用到的CMakeLists。 同时跟Android.mk,一遍都会带一个## Application.mk文件,这个文件里标识相关编译参数和需要输出的arm版本
编译C/C++静态库 的 Android.mk文件内容
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
# 目标静态库的名称
LOCAL_MODULE := libgif-ex
# C/C++源文件
LOCAL_SRC_FILES := \
dgif_lib.c \
egif_lib.c \
gifalloc.c \
gif_err.c \
gif_hash.c \
openbsd-reallocarray.c \
quantize.c
LOCAL_CFLAGS += -Wno-format -Wno-sign-compare -Wno-unused-parameter -DHAVE_CONFIG_H
LOCAL_SDK_VERSION := 8
LOCAL_NDK_STL_VARIANT := c++_static
# 构建C/C++静态库
include $(BUILD_STATIC_LIBRARY)
编译C/C++共享库Android.mk文件内容
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
# C/C++头文件
LOCAL_C_INCLUDES := \
$(LOCAL_PATH)/include \
$(TOPDIR)system/core/include
# C FLAG
LOCAL_CFLAGS := -O3 -DNDEBUG
# LDFLAG
LOCAL_LDFLAGS := -llog
# 依赖的共享库
LOCAL_SHARED_LIBRARIES := \
libutils \
myutils
# 依赖的静态库
LOCAL_STATIC_LIBRARIES := libjpeg_static_ndk
# C/C++源文件
LOCAL_SRC_FILES := \
main.cpp \
utils.cpp
# 目标共享库的名称
LOCAL_MODULE := mylib
LOCAL_MODULE_TAGS := optional
# 构建共享库
include $(BUILD_SHARED_LIBRARY)
Application.mk 例子
APP_ABI := armeabi-v7a x86
APP_PLATFORM := android-19
具体细节可看:www.jianshu.com/p/8aabfbbb1…
这里有动态库和静态库的区分,简单来说,静态库可以独立使用,so文件较大,因为打包时吧相关的依赖全部打包到目标so文件了,好处是稳定,不依赖于外部环境。动态库需要跟依赖的so库一起使用,so文件较小,坏处是当依赖的so无法从系统或者app的so目录找到时将会引起功能异常
3. CMakeLists
新版本的nkd编译都采用这个方式编译,简单明了。 现在AndroidStudio默认创建的nativie库会自动添加一个cpp文件和CMakeLists文件,同时在build.gradl文件中会自动添加
android{
...
externalNativeBuild {
cmake {
path "src/main/jni/CMakeLists.txt"
}
}
}
在项目工程配置好ndk路径即可完成编译(新建工程会自动提示现在安装ndk),接下来点运行即可完成代码编译和运行
具体可看blog.csdn.net/u013896064/…
4. native函数
1. 代码
NDK与java的交互主要通过JNI来完成,而这些写能力其实是通过so文件的函数导出表来实现的。默认AS创建的native库会默认创建一个native函数来完成函数的对接。
extern "C" JNIEXPORT jstring JNICALL
Java_com_land_nativelib_NativeLib_stringFromJNI(
JNIEnv* env,
jobject /* this */) {
std::string hello = "Hello from C++";
return env->NewStringUTF(hello.c_str());
}
public class NativeLib {
// Used to load the 'nativelib' library on application startup.
static {
System.loadLibrary("nativelib");
}
/**
* A native method that is implemented by the 'nativelib' native library,
* which is packaged with this application.
*/
public native String stringFromJNI();
}
这里做一些解释 extern “C” 表示在cpp文件下,已c函数的编译方式导出函数表(主要是保证不对函数名做相关的修改,c语言的函数名和C++的导出函数名不太一样),保证系统能够正确找到c函数。 前面的Java是一个固定传,后面的com_land_nativelib,对应到java的包名。同理依次NatvieLib为类名,stringFromJNI是函数名。 参数第一个JNIEnv是java虚拟机线程相关的环境变量,可以理解为上下文,对于c和c++通过env来完成Java层的相关调用,这个上次有提到。 jobject表示当前的类实例对象。 当你的navtive方法为static方法时,这里参数不再是jobject 而是jclass,道理大家应该理解。
上面的例子不涉及传参,如果需要手动传参可以添加对应的参数即可。这里有一份对应的参数对照表。一一对应就能完成传递 blog.csdn.net/zq5599966_x…
| V | void | void | N/A |
|---|---|---|---|
| Z | jboolean | boolean | 8 unsigned |
| I | jint | int | 32 |
| J | jlong | long | 64 |
第二列为c语言用到的类型, 第三列对应到java中的类型。 至于第一列 在函数注册的时候有用,具体可以参考下面的例子。
static JNINativeMethod libenc_methods[] = {
{ "setEncoderResolution", "(II)V", (void *)libenc_setEncoderResolution },
{ "setEncoderFps", "(I)V", (void *)libenc_setEncoderFps },
{ "setEncoderGop", "(I)V", (void *)libenc_setEncoderGop },
{ "setEncoderBitrate", "(I)V", (void *)libenc_setEncoderBitrate },
{ "setEncoderPreset", "(Ljava/lang/String;)V", (void *)libenc_setEncoderPreset },
{ "RGBAToI420", "([BIIZI)[B", (void *)libenc_RGBAToI420 },
{ "RGBAToNV12", "([BIIZI)[B", (void *)libenc_RGBAToNV12 },
{ "openSoftEncoder", "()Z", (void *)libenc_openSoftEncoder },
{ "closeSoftEncoder", "()V", (void *)libenc_closeSoftEncoder },
{ "RGBASoftEncode", "([BIIZIJ)I", (void *)libenc_RGBASoftEncode },
};
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
jvm = vm;
if (jvm->GetEnv((void **) &jenv, JNI_VERSION_1_6) != JNI_OK) {
LIBENC_LOGE("Env not got");
return JNI_ERR;
}
jclass clz = jenv->FindClass("net/ossrs/yasea/SrsEncoder");
if (clz == NULL) {
LIBENC_LOGE("Class "net/ossrs/yasea/SrsEncoder" not found");
return JNI_ERR;
}
if (jenv->RegisterNatives(clz, libenc_methods, LIBENC_ARRAY_ELEMS(libenc_methods))) {
LIBENC_LOGE("methods not registered");
return JNI_ERR;
}
return JNI_VERSION_1_6;
}
2. 工具
第一步中提到c函数导出,咱们怎么确认这个是否有这个功能,需要用到一个工具,linux或者macOS都有的工具,在命令行输入nm -D *.so 即可列出so文件所有导出的函数。
5. 加载流程
简单来说,安装包安装so文件到手机时,PMS对安装包进行相关的解析并将对应的so安装到对应的指令目录下。调用so过程中,通过搜索so文件中的函数导出表,找到与java文件中对应的native函数,具体的细节可参考 blog.csdn.net/liujian8654…
6. 更安全的做法
通过上面的分析,咱们发现所有so函数导出对于外部来说都是透明的,通过nm工具就能看出所有的导出函数,这些函数还能直接调用,因此存在很大的安全隐患。 对于这个问题,简单的处理方案是,对于java和c的jni函数做混淆处理,整个so只保留一个导出函数,具体内部调用通过自定义一套通用的处理逻辑来做相关的业务分发。 第二种方案是,也只保留一个导出函数。但是这个导出函数不提供业务相关方面的能力。在so层启动一种服务能力来完成so和java层的数据交互,(比如网络,管道,binder等等)。核心思想是通过内核层来进行数据交换,而不是直接通过函数调用来完成数据的处理。