【Android】一种便于 SDK 开发的 so 可替换技术

356 阅读5分钟

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

在做 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_OnLoadJNI_OnUnLoad函数 就像是DLL中的PROCESS ATTATCHDEATTATCH的过程一样,可以同样做一些初始化和反初始化的动作。

一般来说,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"