1.介绍
业务需求需要提供较多native接口(Android、IOS)给Unity、Unreal来进行使用。为了方便开发,提供类似于阉割版的JSBridge的框架来实现通信。
native需要给engine提供大量接口,即主要讨论Engine如果调用Native接口为主。
2.原理
Unity调用Android、IOS接口。Unity提供AndroidJavaClass工具类来进行通信,IOS则是直接通过C#调用C++方法。
//Android
var javaClass = new AndroidJavaClass("className");
javaClass.CallStatic("methodName", "params");
//IOS
[DllImport("__Internal")]
private static extern void ObjCMethod();
CSharpObj.ObjCMethod();
Unreal4调用Android、IOS接口。Unreal也类似,提供FAndroidApplication来进行JNI调用,IOS则是可以直接通过Objc和C++相互调用进行实现。
//Android
JNIEnv *env = FAndroidApplication::GetJavaEnv();
auto jUnrealClass = FAndroidApplication::FindJavaClass(ClassName);
env->CallStaticObjectMethod();
//IOS 省略
3.调用流程
除了业务需求外,需要保证以下几点的实现,Unity、Unreal、Android、IOS的兼容,接入,方法调用以及回调。
3.1 方法调用
框架的核心点就在于Engine如何调用到native的方法。要保证各个平台的兼容性,就得自定义一套通信协议。这里使用Json来当做消息通信的载体。
{
"service":"className",
"method":"methodName",
"callback":true,
"callbackId":"callbackId",
"args":"{
"args1":"value1",
"args:2":"value2"
}"
}
Engine通过发送以上消息体(Command)给native层。
service:类名称
method:方法名称
callback:是否需要回调
callbackId:回调UUID,用于区分回调
args:参数
3.2 方法回调
我们可以通过Command来进行发送消息,要支持回调,也就需要订一个Result来接受Native的信息。
{
"code":0,
"message":"Success",
"callbackId":"callbackId",
"content":"{
"result1":"value1",
"result2":"value2"
}"
}
Engine通过以上消息题来接受Native的消息。
code:0 是否调用成功
message:调用消息
callbackId:同Command的CallbackId相同,用于Engine方法区分回调是由哪一个命令来执行的。
content:回调内容
native在接收到Command消息体之后执行异步操作,在执行完之后,构造Result消息体回调给Engine。这样就形成了Engine调用Native的一个闭环。
3.3 native层接入方式
3.3.1 Android接口定义以及注册、实现
@BridgeService("CommandService")
public interface CommandService{
@BridgeMethod("execute")
void execute(Activity activity, @BridgeParam("param") string param, @BridgeParam("param2") string param2, BridgeCallback callback);
}
public class CommandServiceImpl implements CommandService{
@Override
public void execute(Activity activity,string param,string param2,BridgeCallback callback){
//TODO Engine真正调用到的方法
}
}
Android端通过三个注解@BridgeService,@BridgeMethod,@BridgeParam来对Command中的service,method,args三个字段进行绑定。下文也是通过改规则来反射获取到指定的方法进行执行。
tips:Activity 以及 BridgeCallback 不需要通过BridgeParam来标注。Activity通过Bridge框架初始化后缓存,而BridgeCallback则可以通过Unity的AndroidJavaProxy来进行构建(Unreal通过jni方法进行回调)。
3.3.2 IOS接口定义、实现
+ (void) param:(NSString *) param param2:(NSString*) param2 bridgeCallback:(void (^)(NSString *result))callback
因为IOS方法名为 args1:args2:args3: ,所以IOS的匹配规则和Android不同。当参数不止一个,IOS则去匹配Command.args而不是Command.method。所以定义JSON时,需要考虑到IOS的方法名特点。
3.4 Engine层接入
3.4.1 Unity 使用
EngineBridge.GetInstance().Register(serviceClz,serviceImpl);
EngineBridge.GetInstance().Reigster(command,(result)=>{
//TODO 回调
});
3.4.2 Unreal 使用
GetBridge()->CallHandler(commandJson);
4.如何实现
以Android端为主,IOS仅列举Command解析流程。
4.1 native层实现
4.1.1 调用入口
Engine所有的Command消息体都用该方法入口
public void registerHandler(String json,BridgeCallback callback){
Command command = new Command(json);
//缓存BridgeCallback
mBridgeCallbackMaps.put(command.callbackId,callback);
//创建执行Handler
createEngineHandler();
//切换到主线程执行方法
getActivity().runOnUIThread(()=>{
new CommandTaskImpl().execute(command,result=>{
//通过Handler切换到 EngineThread 进行回调
sendMessage(result);
});
});
}
4.1.2 Command 执行方法
native接收到Json字符串后,通过反序列化转换成Command类。这里通过CommandTask来对该命令进行处理。会经过一下几步:
获取Command.service
获取执行方法 Command.method
解析Command.args参数,填入method方法中
执行method方法
以上每步操作都需要TryCatch,及时抛出异常给Engine。
//执行方法
public void execute(Command command,BridgeCallback callback){
//获取Service、方法
Method method = BridgeReflect.getServiceMethodFromCommand(BridgeHolder.INSTANCE.getBridgeService(command.service), command);
//获取执行方法的Object
IBridgeService serviceImpl = BridgeHolder.INSTANCE.getBridgeService(command.service).getValue();
//执行method
Object result = this.execute(serviceImpl, method, command, callback);
//判断方法是否有返回值和Callback是否为false
if (result != null && command.callback) {
callback.onResult(BridgeJsonHelper.object2JsonString(result));
}
}
4.1.3 Engine回调给native处理
如果Command中Callback字段为true且方法中定义中包含了BridgeCallback,那么则会构建一个BridgeCallback进行传递下去。
tips:BridgeCallback接口就是用于回调给Engine
以下代码举例:
public void createEngineHandler()
{
//回调给engine时需要切换到引擎的主线程(eg:UnityMain),所以需要构建一个Engine Thread的Handler
Looper.prepare();
mEngineHandler = new Handler(Looper.myLooper(),new Handler.Callback(){
@Override
public boolean handleMessage(Message message){
...
//balabal处理之后,回调给游戏
callback.onResult(message.obj);
...
//如果是Unreal的情况下,通过JNI方法回调
nativeOnResult(message.obj);
}
});
Looper.looper();
}
这里需要注意的点是,Engine的执行线程通常不是Android的主线程。通常Engine调用原生方法,不能保证不进行UI操作,所以我们需要确认的是调用到Android方法时的线程是在Android的主线程,回调的时是在 Engine的执行线程。
4.1.4 反射具体执行方法
根据 4.1.2中介绍,native层接收到Json之后会进行反序列化成Command命令,需要注意的关键点在在于匹配Command.args和@BridgeParam注解。
public static Object[] constructorCommandArgs(Method method,Command command,BridgeCallback callback){
Class>[] parameterTypes = method.getParameterTypes();
Object[] args = new Object[parameterTypes.length];
try {
//讲Command.args 转换为JsonObject
JSONObject jsonObject;
if (TextUtils.isEmpty(command.args)) {
jsonObject = new JSONObject();
} else {
jsonObject = new JSONObject(command.args);
}
for (int i = 0; i < parameterTypes.length; i++) {
Class> paramsClz = parameterTypes[i];
if (paramsClz == Activity.class) {
//如果参数为Activity,设置当前Bridge缓存的Activity
args[i] = Bridge.getInstance().getActivity();
} else if (paramsClz == BridgeCallback.class) {
args[i] = callback;
} else {
//匹配BridgeParam和Command.args参数
args[i] = BridgeReflect.findParams(jsonObject, method);
}
}
} catch (IllegalArgumentException | JSONException e) {
e.printStackTrace();
throw new EngineBridgeException(EngineBridgeExceptionStatus.COMMAND_ARGS_ERROR.getExpandMessage(e.getMessage()));
}
return args;
}
IOS的解析过程略有不同。
根据Command.service获取当前metaClass(元类,用于获取Method)以及targetClass(执行方法的Class类)
Class targetClz = objc_getClass([command.service UTF8String]);
Class metaClz = objc_getMetaClass([command.service UTF8String]);
获取当前类的所有方法,然后遍历所有方法以及属性,匹配Command.args。
关键代码:
Method *methodArr = class_copyMethodList(metaClz,&methodIndex);
for (NSInteger i = 0; i < methodIndex; i++) {
Method method = methodArr[i];
//获取方法指针SEL
SEL sel = method_getName(method);
//根据SEL获取方法名
NSString* methodName = NSStringFromSelector(sel);
//因为IOS的方法名为 args:args2:arge3凭借而成,所以通过字符串“:”来分割。
NSArray *methodNameSpreate = [methodName componentsSeparatedByString:@":"];
//根据Command.args、方法名构造出方法参数
NSArray *methodArgs = [self class:metaClass
currentMethod:command.method
methodSeparated:methodNameSpreate
commandArgs:commandArgs
bridgeCallback:result];
//根据元类以及方法SEL获取方法签名信息
NSMethodSignature *signature = [metaClass instanceMethodSignatureForSelector:sel];
if (signature == nil) {
continue;
}
//IOS 的签名信息默认是2个参数,以此累加
//匹配规则:当方法的参数仅为1,且方法名匹配Command.service、构造BridgeCallback 作为参数
// example: -(void) registerCallback:(void (^)(NSString *result)) callback;
if (signature.numberOfArguments == 3
&& [command.method isEqualToString:methodNameSpreate[0]]
&& (methodArgs == nil || [methodArgs count] == 0)) {
NSLog(@"method:%@ add BridgeCallback",methodName);
methodArgs = [NSArray arrayWithObject:result];
}
if (methodArgs == nil) {
NSLog(@"method:%@ args is NULL",methodName);
continue;
}
if ([methodArgs count] != signature.numberOfArguments - 2) {
NSLog(@"method:%@ args 个数与签名个数不同",methodName);
continue;
}
//执行方法
id methodResult = [self invoke:metaClass signature:signature targetClass:targetClass method:sel args:methodArgs];
if (command.callback && methodResult != nil) {
result([TDSBridgeTool jsonStringWithObject:methodResult]);
}
5.成果
目前该框架已经稳定运行3个月以上。