
-------本文业务场景,在引擎组件渲染的游戏页面中异步调用native获取用户头像和昵称
先上一段代码,这是第一版。
void bkPlatformMQQAccountGetInfo(JSContextRef ctx,const char * openID,enum bkMQQAccountInfoType type)
{
if (openID == NULL) {
bkPlatformMQQAccountRetrunInfo(ctx, openID, type, NULL);
return ;
}
if (type == bkMQQAccountNick) {
const char * openIdCopy = strdup(openID);
asyncGetNickName([NSString stringWithUTF8String:openIdCopy], ^(NSString *nick) {
if (nick) {
bkPlatformMQQAccountRetrunInfo(ctx, openIdCopy, type, (void *)[nick UTF8String]);
} else {
bkPlatformMQQAccountRetrunInfo(ctx, openIdCopy, type, NULL);
}
free((void *)openIdCopy);
});
} else {
bkPlatformMQQAccountRetrunInfo(ctx, openID, type, NULL);
}
}
这段代码截取自组件和native中间层的一部分。该函数传入JSContextRef,为游戏渲染界面当前上下文;openID用于兑换uin请求昵称;type则用来标识是获取昵称还是其他信息。引擎在必要时调用此C函数,native拿到昵称用bkPlatformMQQAccountRetrunInfo函数回调给引擎。
这样的场景在平日的业务开发中很常见,发起请求,异步获取,通过代理模式或闭包回调结果;在可能野指针或循环引用的地方,ARC使用__weak做弱引用,MRC使用__block声明生命周期跟随block,很容易解决。
那么在看这篇文章的各位可以思考,上方的中间层函数会不会出现野指针crash,如果存在这种可能,该怎么解决呢?
查看JSContextRef的定义,是
/*! @typedef JSContextRef A JavaScript execution context. Holds the global object and other execution state. */
typedef const struct OpaqueJSContext* JSContextRef;
很明显,JSContextRef是一个结构体指针。根据我对C语言的理解,直接赋值ctx变量是浅拷贝,只保存了ctx的地址,如果ctx的内存被释放,从浅拷贝的指针值中是无从得知的。而如何深拷贝,成为此问题的切入点。
众所周知,C语言中值拷贝是深拷贝,那我能否新建一个JSContextRef,取出ctx指针对应的结构体的值,赋值后保存呢?修改后第二版的代码如下:
void bkPlatformMQQAccountGetInfo(JSContextRef ctx,const char * openID,enum bkMQQAccountInfoType type)
{
if (openID == NULL) {
bkPlatformMQQAccountRetrunInfo(ctx, openID, type, NULL);
return ;
}
if (type == bkMQQAccountNick) {
//用NSData对象把指针对应的内存空间内容保存起来
NSData *stateData = [[NSData dataWithBytes:&ctx length:sizeof(ctx)] retain];
const char * openIdCopy = strdup(openID);
asyncGetNickName([NSString stringWithUTF8String:openIdCopy], ^(NSString *nick) {
JSContextRef ctxRe;
[stateData getBytes:&ctxRe length:stateData.length];
[stateData release];
if (nick) {
bkPlatformMQQAccountRetrunInfo(ctxRe, openID, type, (void *)[nick UTF8String]);
} else {
bkPlatformMQQAccountRetrunInfo(ctxRe, openID, type, NULL);
}
free((void *)openIdCopy);
});
} else {
bkPlatformMQQAccountRetrunInfo(ctx, openID, type, NULL);
}
}
大家觉得,第二版代码可以达到深拷贝的效果吗?
还是出现了crash。仔细review代码,发现在获取ctx值拷贝时,写成了&ctx。ctx是const struct OpaqueJSContext*
类型,本就是结构体指针的typedef形式,&ctx就成了指针的指针,深拷贝了一个整型指针值,起不到任何作用。修改后的第三版代码如下:

起初以为是代码哪里写错了,自己又写了个Demo验证,Demo代码如下。
typedef struct {
int number; //学号
int age; //年龄
char *name; //名字
} Student;
typedef Student * StudentRef;
void testStructPointerDeepCopy() {
//构造原始结构体变量
StudentRef stu = calloc(1, sizeof(Student));
stu->number = 2017;
stu->age = 27;
stu->name = "a goodman";
//对stu变量的数据深拷贝,保存在NSData对象中
NSData * data = [NSData dataWithBytes:stu length:sizeof(*stu)];
StudentRef stuCopy = calloc(1, data.length);
[data getBytes:stuCopy length:data.length];
//打印深拷贝结果
printf("copied result, number:%d, age:%d, name:%s\n\n", stuCopy->number, stuCopy->age, stuCopy->name);
free(stu);
stu = NULL;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//修改深拷贝的结果,跟原结构体变量一起打印做对比
stuCopy->number = 2018;
stuCopy->age = 28;
stuCopy->name = "still a goodman";
printf("copied result after modifying {number:%d, age:%d, name:%s}\n", stuCopy->number, stuCopy->age, stuCopy->name);
//释放内存
free(stuCopy);
});
}
打印结果为:
copied result, number:2017, age:27, name:a goodman
copied result after modifying {number:2018, age:28, name:still a goodman}
Demo的打印结果表明,深拷贝内存区域的二进制数据达到对上下文的异步持有,这个方案一般是可行的。但是对于JSContextRef
这个结构体指针的typedef类型来说,由于结构体定义细节未暴露出来,无法应用sizeof()
函数计算一个结构体对象占用内存空间大小,进而也无法通过深拷贝内存二进制数据的方法持有上下文。
经过和其他同事的讨论,最终采用了异步请求逻辑和页面“构造-销毁”周期同步的方式来管理上下文的生命周期。方法如下:
- 因为游戏页面通过单例管理器创建,可以在全局任意处获取单例中的页面数据,所以在游戏页面构造时生成一个UUID绑定该页面,页面销毁时UUID清空,用UUID标识一个游戏页面。
- 在发起异步获取头像时,在逻辑对象中持有一份当前页面的UUID,并在异步请求回来后,拿持有的UUID和游戏管理器单例的当前页面的UUID比较,如果管理器页面的UUID为空或比较结果不相等,则说明发起异步请求的页面已释放,不执行后续的回调逻辑。
网友评论