JSValue 在MRC下定义Block Property引起Block调用异常的一个坑

来源:互联网 发布:陕西师范远程网络教育 编辑:程序博客网 时间:2024/05/22 05:27

坑是什么

笔者在实现一个功能的过程中发现,对同一个JSContext在不同的函数块里面定义Block Property,有可能会产生Block Property映射关系错乱,结果通常会导致程序崩溃。
例如,在a函数里面对JSContext定义了add1,add2,add3三个Block(对于JS来说实际上是JS函数),在函数b里面对JSContext定义了delete1,delete2两个Block,结果可能导致调用delete1 Block的时候实际被调用的是add1 Block(也可能是add3),这里说可能是因为具体映射关系是根据内存地址进行映射的,只要定义Block的内存地址一样就会出现问题,所以存在不确定性。

注意:该问题只存在于MRC环境下,ARC环境下一切正常,后面会解释为什么MRC下有问题而ARC下正常。

测试代码:

+ (void)defineJSContext1:(JSContext *)context{    context[@"add1"] = ^(){ NSLog(@"%@",self);};    context[@"add2"] = ^(){ NSLog(@"%@",self);};    context[@"add3"] = ^(){ NSLog(@"%@",self);};}+ (void)defineJSContext2:(JSContext *)context{    context[@"delete1"] = ^(){ NSLog(@"%@",self);};    context[@"delete2"] = ^(){ NSLog(@"%@",self);};}+ (void)testSimpleJSContext{    JSContext *jsContext = [[JSContext alloc] init];    [self defineJSContext1:jsContext];    [self defineJSContext2:jsContext];    NSLog(@"%@", [jsContext.globalObject toDictionary]);}

同时我们Hook了JSValue的setValue:forProperty函数,方便我们观察传给JSValue的时候value的值是什么。
结果输出:

// 1! setValue:forProperty 输出value = <__NSStackBlock__: 0x7fff569555c0> property = add1value = <__NSStackBlock__: 0x7fff56955598> property = add2value = <__NSStackBlock__: 0x7fff56955570> property = add3value = <__NSStackBlock__: 0x7fff569555c0> property = delete1value = <__NSStackBlock__: 0x7fff56955598> property = delete2// 2! [jsContext.globalObject toDictionary] 输出add1 = "<__NSMallocBlock__: 0x600000058720>";add2 = "<__NSMallocBlock__: 0x60000005b810>";add3 = "<__NSMallocBlock__: 0x608000059140>";delete1 = "<__NSMallocBlock__: 0x600000058720>";delete2 = "<__NSMallocBlock__: 0x60000005b810>";

可以看到,delete1的Block地址与add1相同,delete2的Block地址与add2相同,如描述所述,调用delete1的时候实际上是调用了add1,delete2同理。

坑的由来

使用context[@”xx”] = yy;最终是操作了JSContext的globalObject(类型是JSValue),在globalObject里将xx这个变量赋值了yy,赋值调用的是JSValue的setValue:forProperty方法。

官方文档对JSValue的解释如下:

When converting between JavaScript values and Objective-C objects a copy is performed. Values of types listed below are copied to the corresponding types on conversion in each direction. For NSDictionaries, entries in the dictionary that are keyed by strings are copied onto a JavaScript object. For dictionaries and arrays, conversion is recursive, with the same object conversion being applied to all entries in the collection.

意思是说我们在JS和OC之间进行转换的时候实际上有一个copy操作产生,细心的同学估计已经发现,在上面的测试代码当中,我们传给JSContext的实际上是一个NSStackBlock,此时Block是在栈上,但最终在globalObject里面是一个NSMallocBlock,这里就是对JSValue设置property的时候copy起了作用,既然发生了copy,那么使用这个Block应该是安全的,这种写法应该没毛病。
既然写法没问题,那为什么会存在前面的问题?

我们先搞清楚下面这两个问题:

1:为什么栈上的delete1的Block地址会跟add1的Block地址一样?

2:为什么在globalObject里面的delete1的Block会跟add1的Block一样?

第一个问题好解释,因为内存地址被复用了,实际上在调用context[@”xx”] = yy;的时候,context[@”xx”]里面的值已经是[yy copy],所以这里地址在声明delete1的时候被复用不影响add1的Block。

既然JSValue会对Block进行copy,那么对栈上的同一个Block copy出来的堆上的Block应该不一样才对,又如何解释copy之后globalObject里面的delete1的Block会跟add1的Block一样?

答案是JSValue其内部存在一个key为value,值为对vaule进行封装的JSObject的cache map,对同一个value并不会产生多次copy,命中cache会直接将新的property映射到之前已经copy过的JSObject缓存对象。

看看JSValue的setValue:forProperty的实现就清楚了:

Step 1:

- (void)setValue:(id)value forProperty:(NSString *)propertyName{    JSValueRef exception = 0;    JSObjectRef object = JSValueToObject([_context JSGlobalContextRef], m_value, &exception);    if (exception) {        [_context notifyException:exception];        return;    }    JSStringRef name = JSStringCreateWithCFString((CFStringRef)propertyName);    JSObjectSetProperty([_context JSGlobalContextRef], object, name, objectToValue(_context, value), 0, &exception);    JSStringRelease(name);    if (exception) {        [_context notifyException:exception];        return;    }}

Step 2:

JSValueRef objectToValue(JSContext *context, id object){    JSGlobalContextRef contextRef = [context JSGlobalContextRef];    ObjcContainerConvertor::Task task = objectToValueWithoutCopy(context, object);    if (task.type == ContainerNone)        return task.js;    ObjcContainerConvertor convertor(context);    convertor.add(task);    ASSERT(!convertor.isWorkListEmpty());    do {        ObjcContainerConvertor::Task current = convertor.take();        ASSERT(JSValueIsObject(contextRef, current.js));        JSObjectRef js = JSValueToObject(contextRef, current.js, 0);        if (current.type == ContainerArray) {            ASSERT([current.objc isKindOfClass:[NSArray class]]);            NSArray *array = (NSArray *)current.objc;            NSUInteger count = [array count];            for (NSUInteger index = 0; index < count; ++index)                JSObjectSetPropertyAtIndex(contextRef, js, index, convertor.convert([array objectAtIndex:index]), 0);        } else {            ASSERT(current.type == ContainerDictionary);            ASSERT([current.objc isKindOfClass:[NSDictionary class]]);            NSDictionary *dictionary = (NSDictionary *)current.objc;            for (id key in [dictionary keyEnumerator]) {                if ([key isKindOfClass:[NSString class]]) {                    JSStringRef propertyName = JSStringCreateWithCFString((CFStringRef)key);                    JSObjectSetProperty(contextRef, js, propertyName, convertor.convert([dictionary objectForKey:key]), 0, 0);                    JSStringRelease(propertyName);                }            }        }    } while (!convertor.isWorkListEmpty());    return task.js;}

Step 3:

static ObjcContainerConvertor::Task objectToValueWithoutCopy(JSContext *context, id object){    JSGlobalContextRef contextRef = [context JSGlobalContextRef];    if (!object)        return (ObjcContainerConvertor::Task){ object, JSValueMakeUndefined(contextRef), ContainerNone };    if (!class_conformsToProtocol(object_getClass(object), getJSExportProtocol())) {        if ([object isKindOfClass:[NSArray class]])            return (ObjcContainerConvertor::Task){ object, JSObjectMakeArray(contextRef, 0, NULL, 0), ContainerArray };        if ([object isKindOfClass:[NSDictionary class]])            return (ObjcContainerConvertor::Task){ object, JSObjectMake(contextRef, 0, 0), ContainerDictionary };        if ([object isKindOfClass:[NSNull class]])            return (ObjcContainerConvertor::Task){ object, JSValueMakeNull(contextRef), ContainerNone };        if ([object isKindOfClass:[JSValue class]])            return (ObjcContainerConvertor::Task){ object, ((JSValue *)object)->m_value, ContainerNone };        if ([object isKindOfClass:[NSString class]]) {            JSStringRef string = JSStringCreateWithCFString((CFStringRef)object);            JSValueRef js = JSValueMakeString(contextRef, string);            JSStringRelease(string);            return (ObjcContainerConvertor::Task){ object, js, ContainerNone };        }        if ([object isKindOfClass:[NSNumber class]]) {            if (isNSBoolean(object))                return (ObjcContainerConvertor::Task){ object, JSValueMakeBoolean(contextRef, [object boolValue]), ContainerNone };            return (ObjcContainerConvertor::Task){ object, JSValueMakeNumber(contextRef, [object doubleValue]), ContainerNone };        }        if ([object isKindOfClass:[NSDate class]]) {            JSValueRef argument = JSValueMakeNumber(contextRef, [object timeIntervalSince1970]);            JSObjectRef result = JSObjectMakeDate(contextRef, 1, &argument, 0);            return (ObjcContainerConvertor::Task){ object, result, ContainerNone };        }        if ([object isKindOfClass:[JSManagedValue class]]) {            JSValue *value = [static_cast<JSManagedValue *>(object) value];            if (!value)                return (ObjcContainerConvertor::Task) { object, JSValueMakeUndefined(contextRef), ContainerNone };            return (ObjcContainerConvertor::Task){ object, value->m_value, ContainerNone };        }    }    return (ObjcContainerConvertor::Task){ object, valueInternalValue([context wrapperForObjCObject:object]), ContainerNone };}

Step 4:

- (JSValue *)wrapperForObjCObject:(id)object{    // Lock access to m_wrapperMap    JSC::JSLockHolder lock(toJS(m_context));    return [m_wrapperMap jsWrapperForObject:object];}

Step 5:

- (JSValue *)jsWrapperForObject:(id)object{    JSC::JSObject* jsWrapper = m_cachedJSWrappers.get(object);    if (jsWrapper)        return [JSValue valueWithJSValueRef:toRef(jsWrapper) inContext:m_context];    JSValue *wrapper;    if (class_isMetaClass(object_getClass(object)))        wrapper = [[self classInfoForClass:(Class)object] constructor];    else {        JSObjCClassInfo* classInfo = [self classInfoForClass:[object class]];        wrapper = [classInfo wrapperForObject:object];    }    // FIXME: https://bugs.webkit.org/show_bug.cgi?id=105891    // This general approach to wrapper caching is pretty effective, but there are a couple of problems:    // (1) For immortal objects JSValues will effectively leak and this results in error output being logged - we should avoid adding associated objects to immortal objects.    // (2) A long lived object may rack up many JSValues. When the contexts are released these will unprotect the associated JavaScript objects,    //     but still, would probably nicer if we made it so that only one associated object was required, broadcasting object dealloc.    JSC::ExecState* exec = toJS([m_context JSGlobalContextRef]);    jsWrapper = toJS(exec, valueInternalValue(wrapper)).toObject(exec);    m_cachedJSWrappers.set(object, jsWrapper);    return wrapper;}

其中m_cachedJSWrappers就是前面我们说的Cache,类型为:JSC::WeakGCMap

JSC::JSObject* jsWrapper = m_cachedJSWrappers.get(object);

所以针对测试代码,传给JSValue的delete1对应的内存地址和add1的一样,所以JSValue认为delete1命中了add1的JSObject缓存,直接将delete1的值映射到前面add1缓存的Block!这就解释了为什么调用delete1的Block会调用到add1的Block,而且这种映射关系的错乱对程序来说是致命的。

那么为什么ARC下面是正常的呢?因为在ARC下,传给JSValue的Block已经默认进行了一次copy操作,copy出来的Block一定是不一样的,就不会命中Cache导致映射关系错乱,所以在MRC下,前面我们写的测试代码还是有问题的,解决办法很简单,在传给JSValue之前对Block进行copy即可。

如何填坑

既然JSValue存在这样的Cache机制,那么不管在MRC或者ARC下,只要传给JSValue的value一样,不管是Block还是Object,就会命中Cache,如果这个逻辑并不是程序逻辑所期望的,那就要注意在赋值之前先对value进行copy操作,管理好value的内存生命周期

参考资料
JavaScriptCore源码:https://opensource.apple.com/source/JavaScriptCore/

原创粉丝点击