本文已参与「新人创作礼」活动, 一起开启掘金创作之路。
在做 VR 产品过程中,由于需要对 Android 原有的 渲染、刷屏 等机制进行修改,重新封装。VR SDK 是极其重要的一个环节,Oculus 最初开源过一套 VR SDK。后来高通也对客户提供一些 VR SDK 源码参考,其架构与 Oculus 的极其相似。 我们要说的,是在做 SDK 的过程中,SDK 上线后给开发者使用,难免会存在一些未检测出的 BUG。当我们有设备、而且对设备有 root 权限,但是没有开发者的应用工程源码时,使用 so 可替换技术,可以实现在不替换开发者的apk的情况下,极大的方便我们在 SDK 中添加相应的 LOG 以便快速定位问题。
JNI_OnLoad(JavaVM * vm, void * reserved)
Java JNI 有两种方法,一种是通过 javah 获取一组带签名函数,然后实现这些函数。这是一种很常用,也是官方推荐的方法(参考)。另一种就是 JNI_OnLoad 方法。
当 Android 的 VM(Virtual Machine) 执行到 C 组件(即 *.so)里的 System.loadLibrary() 函数时,(一般来说,位于 Java 主类的 静态初始化块 中),首先会去执行 C 组件里的 JNI_OnLoad() 函数。它有两个用途
- 一是告诉 VM 此组件使用哪个 JNI 版本。
- 二是让 C 组件开发者可以利用该函数进行 C 组件内的相关变量初始值的设定(Initialize)。
其实Android中的so文件就像是Windows下的DLL一样,JNI_OnLoad和JNI_OnUnLoad函数
就像是DLL中的PROCESS ATTATCH和DEATTATCH的过程一样,可以同样做一些初始化和反初始化的动作。
一般来说,JNI_OnLoad 函数主要内容如下:
jint JNI_OnLoad(JavaVM* vm, void* reserved)
{
LogDebug("JNI_OnLoad() called!");
JNIEnv* javaEnv;
if (vm->GetEnv(reinterpret_cast<void**>(&javaEnv), JNI_VERSION_1_2) != JNI_OK)
{
return -1;
}
// Get jclass with env->FindClass.
// Register methods with env->RegisterNatives.
plugin.javaVm = vm;
return JNI_VERSION_1_2;
}
So 可替换
因原APP中集成的 旧 so 在被加载时首先执行 JNI_OnLoad 函数,因此,可以在 JNI_OnLoad 中加载指定位置下用来替换的新 so,并利用函数指针指向新 so 中的函数地址,实现接口函数调用过程的重定向,最终实现所谓的 So 可替换。
因此,为了保证替换的完整性,我们把 JNI_OnLoad 函数内容保留不变,重命名为 JNI_OnLoad_Origin 函数,并创建一个指向该函数的指针。
jint JNI_OnLoad_Origin(JavaVM * vm, void * reserved);
jint (*JNI_OnLoad_Origin_interface)(JavaVM *, void *) = JNI_OnLoad_Origin;
jint JNI_OnLoad_Origin(JavaVM * vm, void * reserved)
{
LogDebug("JNI_OnLoad() called!");
// ... 省略若干行
return JNI_VERSION_1_2;
}
然后重新定义 JNI_OnLoad 函数如下:
jint JNI_OnLoad(JavaVM * vm, void * reserved)
{
const char * so_path = "xxx/libSxrApi.so";
static void * handle;
handle = dlopen(so_path, RTLD_LAZY|RTLD_GLOBAL);
if (0 == handle) {
LogDebug("Load %s failed.", so_path);
} else {
LogDebug("Load %s succeed.", so_path);
loadSym(handle, JNI_OnLoad_Origin_interface, "JNI_OnLoad_Origin");
dlclose(handle);
}
return JNI_OnLoad_Origin_interface(vm, reserved);
}
其中,loadSym 定义如下:
#define loadSym(handle, f, target) do {void** p = (void**)&(f); *p = (void*)dlsym((handle), (target));} while(0)
这里的 do while(0) 写法是完全必要的,请参考文章《do {...} while (0) 在宏定义中的作用》。
为了实现 so 可替换,需要在 JNI_OnLoad 时就已经定义好替换用的 so 存放地址(调试时重新打包一个so之后的push位置)。因此可以实现 so 可替换的 JNI_OnLoad 函数这么写:
// EnochApi.h
extern "C" {
extern void (*unity_interface)(int);
extern uint8_t * (*Enoch_GetCameraData_Ext_interface)();
extern float (*Enoch_GetFloorHeight_interface)();
extern void (*Enoch_SetRatio_interface)(float x, float y);
extern int (*Enoch_GetSeeThroughState_interface)(int &state);
bool enoch_OnLoad(JavaVM * JavaVm_);
extern JavaVM * VrLibJavaVM;
void enoch_Init();
} // extern "C"
// EnochApi.cpp
JavaVM * VrLibJavaVM;
static pid_t OnLoadTid;
static jclass VrLibClass = NULL;
extern "C"
{
JNIEXPORT bool couldBeLoaded(char* srcVer) {
#if 0
return true;
#else
int myX, myY, myZ, myW;
int x, y, z, w;
decodeVersion(srcVer, x, y, z, w);
decodeVersion(Enoch_GetSDKVersion_interface(),
myX, myY, myZ, myW);
LOG("APP version, %d, %d, %d, %d", x, y, z, w);
LOG("lib version, %d, %d, %d, %d", myX, myY, myZ, myW);
// 这里可能会有 2.10.4.4 因此,不能直接用 x*1000+y*100+z*10+w 这种方法比结果的大小
// 这里,需要确保系统中的 so 版本要晚于应用中的 so 版本,以防止因较早版本的 so 缺失
// 必要的接口函数而崩溃
if (myX < x) {
return false;
} else if (myX > x) {
return true;
} if (myY < y) {
return false;
} else if (myY > y) {
return true;
} if (myZ < z) {
return false;
} else if (myZ > z) {
return true;
} if (myW < w) {
return false;
} else if (myW > w) {
return true;
}
return false;
#endif
}
jint JNI_OnLoad_( JavaVM * vm, void * reserved ); // JNI_OnLoad 也被写成可替换的函数
jint (*JNI_OnLoad_intf)(JavaVM *, void *) = JNI_OnLoad_;
JNIEXPORT jint JNI_OnLoad_( JavaVM * vm, void * reserved ) {
if(enoch_OnLoad( vm )){
enoch_Init();
}
return JNI_VERSION_1_6;
}
JNIEXPORT jint JNI_OnLoad( JavaVM * vm, void * reserved )
{
static void *handle;
const char* so_path = "/system/lib/libPvr_UnitySDKExt5.so";
handle = dlopen(so_path, RTLD_LAZY|RTLD_GLOBAL);
if (handle == 0) {
LOG( "open replaceable failed %s",so_path );
} else {
LOG( "open replaceable Scuccessed %s",so_path );
bool(*couldBeLoaded_)(char*);
loadSym(handle, couldBeLoaded_, "couldBeLoaded");
LOG( "open replaceable Scuccessed 2" );
if ( NULL != couldBeLoaded_ && couldBeLoaded_(Enoch_GetSDKVersion_interface())) {
LOG( "open replaceable started" );
// 在这里,读取系统中的 so 并对相应函数指针做好赋值
loadSym(handle, unity_interface, "UnityRenderEvent_");
loadSym(handle, JNI_OnLoad_intf, "JNI_OnLoad_");
loadSym(handle, Enoch_GetCameraData_Ext_interface, "Enoch_GetCameraData_Ext_");
loadSym(handle, Enoch_GetFloorHeight_interface, "Enoch_GetFloorHeight_");
loadSym(handle, Enoch_SetRatio_interface, "Enoch_SetRatio_");
loadSym(handle, Enoch_GetSeeThroughState_interface, "Enoch_GetSeeThroughState_");
} else {
LOG( "open replaceable aborted" );
}
dlclose(handle);
}
return JNI_OnLoad_intf(vm, reserved);
}
}// extern C
// This must be called by a function called directly from a java thread,
// preferably at JNI_OnLoad(). It will fail if called from a pthread created
// in native code, or from a NativeActivity due to the class-lookup issue:
//
// http://developer.android.com/training/articles/perf-jni.html#faq_FindClass
//
// This should not start any threads or consume any significant amount of
// resources, so hybrid apps aren't penalizing their normal mode of operation
// by supporting VR.
bool enoch_OnLoad( JavaVM * JavaVm_ )
{
if ( JavaVm_ == NULL ) {
FAIL( "JavaVm == NULL" );
}
if ( VrLibJavaVM != NULL ) {
return false;
}
VrLibJavaVM = JavaVm_;
OnLoadTid = gettid();
JNIEnv * jni;
bool privateEnv = false;
if ( JNI_OK != VrLibJavaVM->GetEnv( reinterpret_cast<void**>(&jni), JNI_VERSION_1_6 ) ) {
LOG( "Creating temporary JNIEnv" );
// We will detach after we are done
privateEnv = true;
const jint rtn = VrLibJavaVM->AttachCurrentThread( &jni, 0 );
if ( rtn != JNI_OK ) {
FAIL( "AttachCurrentThread returned %i", rtn );
}
} else {
LOG( "Using caller's JNIEnv" );
}
VrLibClass = enoch_GetGlobalClassReference( jni, "com/psmart/vrlib/VrLib" );
jclass versionClass = jni->FindClass( "android/os/Build$VERSION" );
if ( versionClass != 0 ) {
jfieldID sdkIntFieldID = jni->GetStaticFieldID( versionClass, "SDK_INT", "I" );
if ( sdkIntFieldID != 0 ) {
BuildVersionSDK = jni->GetStaticIntField( versionClass, sdkIntFieldID );
LOG( "BuildVersionSDK %d", BuildVersionSDK );
}
jni->DeleteLocalRef( versionClass );
}
// Detach if the caller wasn't already attached
if ( privateEnv ) {
LOG( "Freeing temporary JNIEnv" );
VrLibJavaVM->DetachCurrentThread();
}
return true;
}
// A dedicated VR app will usually call this immediately after pvr_OnLoad(),
// but a hybrid app may want to defer calling it until the first headset
// plugin event to avoid starting the device manager.
void enoch_Init()
{
// initialize enoch code
enoch_InitializeInternal();
JNIEnv * jni;
const jint rtn = VrLibJavaVM->AttachCurrentThread(&jni, 0);
if (rtn != JNI_OK) {
FAIL( "AttachCurrentThread returned %i", rtn);
}
// After enoch_Initialize(), because it uses String
enoch_InitBuildStrings(jni);
}
jclass enoch_GetGlobalClassReference( JNIEnv * jni, const char * className )
{
jclass lc = jni->FindClass(className);
if ( lc == 0 ) {
FAIL( "FindClass( %s ) failed", className );
}
// Turn it into a global ref, so we can safely use it in other threads
jclass gc = (jclass)jni->NewGlobalRef( lc );
jni->DeleteLocalRef( lc );
return gc;
}
// UnityPlugin.h
extern "C" {
uint8_t * Enoch_GetCameraData_Ext();
float Enoch_GetFloorHeight();
void Enoch_SetRatio(float x, float y);
int Enoch_GetSeeThroughState(int &state);
} // extern "C"
// UnityPlugin.cpp
#include "UnityPlugin.h"
#include "EnochApi.h"
extern "C" {
void UnityRenderEvent_(int eventID);
void (*unity_interface)(int) = UnityRenderEvent_;
void UnityRenderEvent(int eventID) {unity_interface(eventID);}
void UnityRenderEvent_(int eventID) {
// Implement...
}
uint8_t * Enoch_GetCameraData_Ext_() {
// Implement...
}
float Enoch_GetFloorHeight_() {
float res = xxxx();
return res;
}
void Enoch_SetRatio_(float x, float y) {
xxxxxxx( x, y );
}
int Enoch_GetSeeThroughState_(int &state) {
state = getSeeThroughState();
return 0;
}
} // extern "C"