应用场景:
·参数校验:网络请求数据点参数校验,返回数据格式校验
·无痕埋点:统一处理埋点,降低代码耦合
·页面统计:帮助统计页面访问量
·事务处理:拦截指定事件
·异常处理:发生异常时使用面向切面编程处理
·热修复:可以让某方法执行前后或直接替换为另一段代码
我们从头文件开始看
typedef NS_OPTIONS(NSUInteger, AspectOptions) { AspectPositionAfter = 0, /// 在原函数执行完后调用 (default) AspectPositionInstead = 1, /// 替换hook的类/对象方法. AspectPositionBefore = 2, /// 在原函数执行之前. };
头文件定义了一个枚举类型,分别设置了三种不同的策略。
现Aspect定义,使用property来声明
@interface AspectObject : NSObject /// The instance that is currently hooked. 返回当前被hook的实例 @property (nonatomic, unsafe_unretained, readonly) id instance; /// The original invocation of the hooked method. 返回被hooked方法的原始invocation @property (nonatomic, strong, readonly) NSInvocation *originalInvocation; /// All method arguments, boxed. This is lazily evaluated.返回所有方法的参数,懒加载实现 @property (nonatomic, strong, readonly) NSArray *arguments; @end
这里和以前的Aspect有所不同,以前版本是定义一个protocol协议来遵循
前Aspect定义,使用protocol
@protocol AspectInfo <NSObject> - (id)instance; - (NSInvocation *)originalInvocation; - (NSArray *)arguments; @end
instance:返回当前被hook的实例
originalInvocation:返回被hooked方法的原始invocation
argumenets:返回所有方法的参数
源文件中还有一段注释需要注意Note1: Disallow hook a method and super method at the same time 不允许同一时间hook 类和父类方法
Note2:(e.g ViewController:UIViewController) 例如ViewController 子类:UIViewController 父类
Note3:Don`t hook ViewController.viewDidload and UIViewtroller.viewDidload at the same time
前aspect 声明的类及对象方法
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector withOptions:(AspectOptions)options usingBlock:(id)block error:(NSError **)error; /// Adds a block of code before/instead/after the current `selector` for a specific instance. - (id<AspectToken>)aspect_hookSelector:(SEL)selector withOptions:(AspectOptions)options usingBlock:(id)block error:(NSError **)error;
现aspect 声明的类及对象方法
@interface NSObject (Aspect) + (BOOL)hookSelector:(SEL)selector position:(AspectPosition)position usingBlock:(id)block; - (BOOL)hookSelector:(SEL)selector position:(AspectPosition)position usingBlock:(id)block; + (BOOL)unhookSelector:(SEL)selector; - (BOOL)unhookSelector:(SEL)selector; @end
多了两个类/对象方法unhookSelector,error移动到block里
现用法
[ViewController hookSelector:@selector(viewWillAppear:) position:0 usingBlock:^(){ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ NSLog() }); }];
存疑:这里block里面使用GCD来执行网络请求或给后台发送socket请求
问题1:这里应该使用哪一种GCD 单例or异步?
问题2: 执行应该开辟新的线程还是在global_queue?
前用法:注意无法这样用了!!!!!!现在pod进workspace的文件已经改变 当然如果你还是用的6年前的版本当我没说。
[UIViewController aspect_hookSelector:@selector(viewWillAppear:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo, BOOL animated) { NSLog(@"View Controller %@ will appear animated: %tu", aspectInfo.instance, animated); } error:NULL];
无法执行
typedef NS_OPTIONS(int, AspectBlockFlags) { AspectBlockFlagsHasCopyDisposeHelpers = (1 << 25), 0001 << 25 = 0x2000000 AspectBlockFlagsHasSignature = (1 << 30) 0001 << 30 = 0x40000000 };
首先定义了一个Block的类型,分别有HasCopyDisposeHelpers , HasSignature
这两个属性在我之前写的iOS-从循环引用看Block文章中-目录-Block底层中有提到
block的layout结构的这几个属性
struct Block_layout{
void *isa
flags
reserve
BlockInvokeFunction invoke
Struct Block_descriptor_1 *descriptor
}
其中HasCopyDispose和Signature对应descriptor中的2,3 不熟悉的真的强烈建议回去看一遍
typedef struct _AspectBlock { __unused Class isa; AspectBlockFlags flags; __unused int reserved; void (__unused *invoke)(struct _AspectBlock *block, ...); struct { unsigned long int reserved; unsigned long int size; // requires AspectBlockFlagsHasCopyDisposeHelpers void (*copy)(void *dst, const void *src); void (*dispose)(const void *); // requires AspectBlockFlagsHasSignature const char *signature; const char *layout; } *descriptor; // imported variables } *AspectBlockRef;
这里定义了一个Aspect内部使用的block类型
flags:标识的定义分别有两个,也就是block的类型如上
后面的类型其实和block没多大差别
我们来看接下来的获取aop参数的对象方法
- (NSArray *)aop_arguments { NSMutableArray *argumentsArray = [NSMutableArray array]; for (NSUInteger idx = 2; idx < self.methodSignature.numberOfArguments; idx++) { [argumentsArray addObject:[self aop_argumentAtIndex:idx] ?: NSNull.null]; } return [argumentsArray copy]; }
初始化一个可变数组用来存储当前签名,可为什么从2开始?
这里引用https://halfrost.com/ios_aspect/ 中的解释
Type Encodings作为对Runtime的补充,编译器将每个方法的返回值和参数类型编码为一个字符串,并将其与方法的selector关联在一起。这种编码方案在其它情况下也是非常有用的,因此我们可以使用@encode编译器指令来获取它。当给定一个类型时,@encode返回这个类型的字符串编码。这些类型可以是诸如int、指针这样的基本类型,也可以是结构体、类等类型。事实上,任何可以作为sizeof()操作参数的类型都可以用于@encode()。
在Objective-C Runtime Programming Guide中的Type Encoding一节中,列出了Objective-C中所有的类型编码。需要注意的是这些类型很多是与我们用于存档和分发的编码类型是相同的。但有一些不能在存档时使用。
注:Objective-C不支持long double类型。@encode(long double)返回d,与double是一样的。
OC为支持消息的转发和动态调用,Objective-C Method 的 Type 信息以 “返回值 Type + 参数 Types” 的形式组合编码,还需要考虑到?self 和?_cmd 这两个隐含参数,所以从第三位开始才是入参。
- (id)aop_argumentAtIndex:(NSUInteger)index { const char *argType = [self.methodSignature getArgumentTypeAtIndex:index]; // Skip const type qualifier. if (argType[0] == _C_CONST) argType++; #define WRAP_AND_RETURN(type) do { type val = 0; [self getArgument:&val atIndex:(NSInteger)index]; return @(val); } while (0) if (strcmp(argType, @encode(id)) == 0 || strcmp(argType, @encode(Class)) == 0) { __autoreleasing id returnObj; [self getArgument:&returnObj atIndex:(NSInteger)index]; return returnObj; } else if (strcmp(argType, @encode(SEL)) == 0) { SEL selector = 0; [self getArgument:&selector atIndex:(NSInteger)index]; return NSStringFromSelector(selector); } else if (strcmp(argType, @encode(Class)) == 0) { __autoreleasing Class theClass = Nil; [self getArgument:&theClass atIndex:(NSInteger)index]; return theClass; // Using this list will box the number with the appropriate constructor, instead of the generic NSValue. } else if (strcmp(argType, @encode(char)) == 0) { WRAP_AND_RETURN(char); } else if (strcmp(argType, @encode(int)) == 0) { WRAP_AND_RETURN(int); } else if (strcmp(argType, @encode(short)) == 0) { WRAP_AND_RETURN(short); } else if (strcmp(argType, @encode(long)) == 0) { WRAP_AND_RETURN(long); } else if (strcmp(argType, @encode(long long)) == 0) { WRAP_AND_RETURN(long long); } else if (strcmp(argType, @encode(unsigned char)) == 0) { WRAP_AND_RETURN(unsigned char); } else if (strcmp(argType, @encode(unsigned int)) == 0) { WRAP_AND_RETURN(unsigned int); } else if (strcmp(argType, @encode(unsigned short)) == 0) { WRAP_AND_RETURN(unsigned short); } else if (strcmp(argType, @encode(unsigned long)) == 0) { WRAP_AND_RETURN(unsigned long); } else if (strcmp(argType, @encode(unsigned long long)) == 0) { WRAP_AND_RETURN(unsigned long long); } else if (strcmp(argType, @encode(float)) == 0) { WRAP_AND_RETURN(float); } else if (strcmp(argType, @encode(double)) == 0) { WRAP_AND_RETURN(double); } else if (strcmp(argType, @encode(BOOL)) == 0) { WRAP_AND_RETURN(BOOL); } else if (strcmp(argType, @encode(bool)) == 0) { WRAP_AND_RETURN(BOOL); } else if (strcmp(argType, @encode(char *)) == 0) { WRAP_AND_RETURN(const char *); } else if (strcmp(argType, @encode(void (^)(void))) == 0) { __unsafe_unretained id block = nil; [self getArgument:&block atIndex:(NSInteger)index]; return [block copy]; } else { NSUInteger valueSize = 0; NSGetSizeAndAlignment(argType, &valueSize, NULL); unsigned char valueBytes[valueSize]; [self getArgument:valueBytes atIndex:(NSInteger)index]; return [NSValue valueWithBytes:valueBytes objCType:argType]; } return nil; #undef WRAP_AND_RETURN }
这个方法是用来获取methodSignatur方法签名指定的index的type encoding的字符串。
由于第0位是函数返回值return value对应的type encoding,所以传进来的2,对应的是argument2。所以我们这里传递index = 2进来,就是过滤掉了前3个type encoding的字符串,从argument2开始比较。这就是为何循环从2开始的原因。
下面的大段的判断就是把入参都返回的过程,依次判断了id,class,SEL,接着是一大推基本类型,char,int,short,long,long long,unsigned char,unsigned int,unsigned short,unsigned long,unsigned long long,float,double,BOOL,bool,char *这些基本类型都会利用WRAP_AND_RETURN打包成对象返回。最后判断block和struct结构体,也会返回对应的对象。
#define WRAP_AND_RETURN(type) do { type val = 0; [self getArgument:&val atIndex:(NSInteger)index]; return @(val); } while (0)
WRAP_AND_RETURN是一个宏定义。这个宏定义里面调用的getArgument:atIndex:方法是用来在NSInvocation中根据index得到对应的Argument,最后return的时候把val包装成对象,返回出去。
@synthesize arguments = _arguments;
给参数arguments 设置set 和 get方法
- (id)initWithInstance:(__unsafe_unretained id)instance invocation:(NSInvocation *)invocation { NSCParameterAssert(instance); NSCParameterAssert(invocation); if (self = [super init]) { _instance = instance; _originalInvocation = invocation; } return self; }
通过对象初始化,传入对象和invocation
这里有一句注释// Lazily evaluate arguments, boxing is expensive.
有没有好心人可以帮忙翻译翻译,这要怎么理解?通过懒加载来运行,在盒子中运行的代价很昂贵?
- (NSArray *)arguments { if (!_arguments) { _arguments = self.originalInvocation.aop_arguments; } return _arguments; }
属性定义
@interface AspectIdentifier : NSObject + (instancetype)identifierWithTarget:(id)target selector:(SEL)selector position:(AspectPosition)position block:(id)block; - (void *)invokeWithObject:(AspectObject *)object; @property (nonatomic, weak) id target; @property (nonatomic, assign) SEL selector; @property (nonatomic, assign) AspectPosition position; @property (nonatomic, copy) id block; @property (nonatomic, strong) NSMethodSignature *blockSignature; @end
注释:我写这篇文章的时间是2021.4.5 其中target对应以前版本的object,麻烦复制党大佬复制的时候把原地址也带上可否?
static NSMethodSignature *aop_blockMethodSignature(id block) { AspectBlockRef layout = (__bridge void *)block; if (!(layout->flags & AspectBlockFlagsHasSignature)) { aop_log("? The block <%@> doesn't contain a type signature.", block); return nil; } void *desc = layout->descriptor; desc += 2 * sizeof(unsigned long int); if (layout->flags & AspectBlockFlagsHasCopyDisposeHelpers) { desc += 2 * sizeof(void *); } if (!desc) { aop_log("? The block <%@> doesn't has a type signature.", block); return nil; } const char *signature = (*(const char **)desc); return [NSMethodSignature signatureWithObjCTypes:signature]; }
在类方法instancetype中调用了上面的方法
+ (instancetype)identifierWithTarget:(id)target selector:(SEL)selector position:(AspectPosition)position block:(id)block { NSMethodSignature *blockSignature = aop_blockMethodSignature(block); if (!aop_isCompatibleBlockSignature(blockSignature, target, selector)) { return nil; } AspectIdentifier *identifier = nil; if (blockSignature) { identifier = [AspectIdentifier new]; identifier.target = target; identifier.selector = selector; identifier.position = position; identifier.block = block; identifier.blockSignature = blockSignature; } return identifier; }
aop_blockMethodSignature函数作用就是把传递进来的block转换成NSMethodSignature的方法签名,对应在上面提到的AspectBlock结构中
static BOOL aop_isCompatibleBlockSignature(NSMethodSignature *blockSignature, id target, SEL selector) { BOOL signaturesMatch = YES; NSMethodSignature *methodSignature = aop_methodSignature(target, selector); if (blockSignature.numberOfArguments > methodSignature.numberOfArguments) { signaturesMatch = NO; } else { if (blockSignature.numberOfArguments > 1) { const char *blockType = [blockSignature getArgumentTypeAtIndex:1]; if (blockType[0] != '@') { signaturesMatch = NO; } } // Argument 0 is self/block, argument 1 is SEL or id<AspectObject>. We start comparing at argument 2. // The block can have less arguments than the method, that's ok. if (signaturesMatch) { for (NSUInteger idx = 2; idx < blockSignature.numberOfArguments; idx++) { const char *methodType = [methodSignature getArgumentTypeAtIndex:idx]; const char *blockType = [blockSignature getArgumentTypeAtIndex:idx]; // Only compare parameter, not the optional type data. if (!methodType || !blockType || methodType[0] != blockType[0]) { signaturesMatch = NO; break; } } } } if (!signaturesMatch) { aop_log("? Block signature <%@> doesn't match <%@>.", blockSignature, methodSignature); return NO; } return signaturesMatch; }
回到AspectIdentifier中继续看instancetype方法,获取到了传入的block的方法签名之后,又调用了aop_isCompatibleBlockSignature方法用来把要替换的方法block和要替换的原方法做对比,用什么做对比?
1.传参target是原方法对象,比较参数个数target和selector分别有多少个参数
2.如果参数个数相等,再比较所替换的方法里第一个参数是不是_cmd,对应的返回值type是@如果不是就无法匹配
3.签名匹配为什么还是从2开始,上面有解释
4.最后经过上面的三层比较还是无法匹配成功,抛出exception
- (void *)invokeWithObject:(AspectObject *)object { NSInvocation *blockInvocation = [NSInvocation invocationWithMethodSignature:self.blockSignature]; NSInvocation *originalInvocation = object.originalInvocation; NSUInteger numberOfArguments = self.blockSignature.numberOfArguments; // Be extra paranoid. We already check that on hook registration. // 匹配成功后的参数个数,block里肯定不会大于原始方法里的个数的,这里是再做里一层错误判断 if (numberOfArguments > originalInvocation.methodSignature.numberOfArguments) { return NULL; } // The `self` of the block will be the AspectObject. Optional. // 把AspectObject 存入到blockInvocation中 if (numberOfArguments > 1) { [blockInvocation setArgument:&object atIndex:1]; } void *argBuf = NULL; for (NSUInteger idx = 2; idx < numberOfArguments; idx++) { const char *type = [originalInvocation.methodSignature getArgumentTypeAtIndex:idx]; NSUInteger argSize; NSGetSizeAndAlignment(type, &argSize, NULL); if (!(argBuf = reallocf(argBuf, argSize))) return NULL; [originalInvocation getArgument:argBuf atIndex:idx]; [blockInvocation setArgument:argBuf atIndex:idx]; } //for循环从originalInvocation中取出参数赋给argBuf中,然后再赋给blockInvocation里。循环从2开始的原因 //在上面已经有提到过了 [blockInvocation invokeWithTarget:self.block]; //最后self.block 赋值给了Target if (argBuf != NULL) { free(argBuf); } if (strcmp(blockInvocation.methodSignature.methodReturnType, @encode(void)) == 0) { return NULL; } void *result; [blockInvocation getReturnValue:&result]; return result; }
总结,AspectIdentifier是一个切片Aspect的具体内容。里面会包含了单个的 Aspect 的具体信息,包括执行时机,要执行 block 所需要用到的具体信息:包括方法签名、参数等等。初始化AspectIdentifier的过程实质是把我们传入的block打包成AspectIdentifier。从上面来看,更新后的aspect把object全部替换成了target。
我们来看声明
@interface NSObject () @property(nonatomic, strong) NSMutableDictionary<NSString*, AspectIdentifier*> *aop_blocks; @end
对NSObject的扩展,说明可以hook元类
使用一个可变字典来存放block指针
static BOOL aop_hookSelector(id self, SEL selector, AspectPosition position, id block) { if (!aop_isSelectorAllowedHook(self, selector)) { NSCAssert(false, @"Disallow hook selector <%@>.", NSStringFromSelector(selector)); return NO; } __block BOOL isSuccess = YES; aop_performLock(^{ Method method = class_getInstanceMethod([self class], selector); Class clazz = method ? [self class] : object_getClass([self class]); method = method ?: class_getClassMethod([self class], selector); if (method == NULL) { aop_log("? Hooked selector <%@> doesn't exist in class <%@>", NSStringFromSelector(selector), NSStringFromClass(clazz)); isSuccess = NO; return; } Method aliasMethod = class_getInstanceMethod(clazz, aop_aliasForSelector(selector)); // If alias method does exist and is not empty implementation which means //it is hooked. 如果identity方法存在且不是空的imp意味着已经被hooked住 // NOTE: If aop_blocks count is great than 0, it means the //instance has been released, does not exist duplicate hook. // 如果aopblock 计数大于0 说明对象已经被释放,且无法再次被二次hook BOOL isInstanceHook = self != [self class]; if (aliasMethod && method_getImplementation(aliasMethod) != (IMP)aop_emptyImplementationSelector) { NSString *key = NSStringFromSelector(selector); if (isInstanceHook && [self aop_blocks][key] == nil) { aop_unhookSelector(self, selector); } else { aop_log("? The selector <%@> in class <%@> has been hooked, disallow duplicate hook.", NSStringFromSelector(selector), NSStringFromClass(clazz)); isSuccess = YES; return; } } IMP imp = method_getImplementation(method); const char *types = method_getTypeEncoding(method); // If add method failed, it means the alias method //already exist in current class, just need set a new imp to it. // 如果添加方法失败,意味着别名方法早已经在类里面存在,只需要设置一个新的imp给他 if (!class_addMethod(clazz, aop_aliasForSelector(selector), imp, types)) { method_setImplementation(aliasMethod, imp); } if (![self aop_blocks]) { [self setAop_blocks:[NSMutableDictionary dictionary]]; } [self aop_blocks][NSStringFromSelector(selector)] = [AspectIdentifier identifierWithTarget:self selector:selector position:position block:block]; class_replaceMethod(clazz, @selector(forwardInvocation:), (IMP)aspect_forwardInvocation, "v@:@"); class_replaceMethod(clazz, selector, aspect_msgForwardIMP(clazz, selector), types); }); return isSuccess; }
其中调用了这个方法,挺有意思的。之前Aspect使用的是已经被Apple废弃的锁OSSpinLock
注意!!!!老版本是用的OSSpinLock 锁已经无法使用。现已经改成os_unfair_lock
os_unfair_lock用于取代不安全的OSSpinLock,从iOS10开始才支持
为什么就加锁?
因为我是hook NSObject所有的方法就不可能避免多线程访问,那么多线程访问会造成一个什么问题?资源抢夺,所以多线程访问就必须加锁。
static void aop_performLock(dispatch_block_t block) { os_unfair_lock aspect_lock = OS_UNFAIR_LOCK_INIT; os_unfair_lock_lock(&aspect_lock); //证明这里一定会被调用 block(); //block锁住当前的session来执行 os_unfair_lock_unlock(&aspect_lock); }
我们主要到函数最后还执行了两个函数
static void aspect_forwardInvocation(id self, SEL selector, NSInvocation *invocation) { SEL originalSelector = invocation.selector; SEL aliasSelector = aop_aliasForSelector(originalSelector); NSString *key = NSStringFromSelector(originalSelector); AspectIdentifier *identifier = [self aop_blocks][key] ?: [[self class] aop_blocks][key]; // Check if the selector hooked by super class // 检查 selector是否是通过 父类来hook的 AspectIdentifier *superIdentifier; Class currClass = object_getClass(self); while (superIdentifier == nil) { Class superClass = class_getSuperclass(currClass); if (superClass == currClass) { break; } currClass = superClass; superIdentifier = [currClass aop_blocks][key]; } // If the same selector is hooked by sub class and super class, and the sub class is hooked first, // must unhook the selector of super class, otherwise, it will lead to call method with infinite loop. // Since KVO will create a subclass which inherits the current class, this case doesn't belong to duplicate hook. // 如果同一个selector已经被子类hook和父类住,且子类比父类早,必须从父类unhook 这个selector,否则 // 这会导致无限循环 // 自从KVO建立类子类继承当前类,这种情况已经不属于重复hook if (identifier != nil && superIdentifier != nil && ![NSStringFromClass(object_getClass(self)) hasPrefix:@"NSKVONotifying"]) { identifier = nil; // Set with nil in order to only call original invocation //设置为nil则会调用原方法 aop_unhookSelector(self, originalSelector); aop_log("? The selector <%@> in class <%@> has been hooked, disallow duplicate hook.", NSStringFromSelector(originalSelector), NSStringFromClass(object_getClass(self))); } else if (identifier == nil && superIdentifier != nil) { identifier = superIdentifier; } // 这意味着对象的selector已经被父类所hook,并不是当前类hook的 // It means this instance's selector is hooked by super class, not the current class. if (identifier == nil) { aop_invokeOriginalInvocation(invocation, aliasSelector); return; } // 这里传入参数3个位置参数 after instead before switch (identifier.position) { case AspectPositionAfter: aop_invokeOriginalInvocation(invocation, aliasSelector); aop_invokeHookedBlock(self, identifier, invocation, aliasSelector); break; case AspectPositionBefore: aop_invokeHookedBlock(self, identifier, invocation, aliasSelector); aop_invokeOriginalInvocation(invocation, aliasSelector); break; case AspectPositionInstead: aop_invokeHookedBlock(self, identifier, invocation, aliasSelector); break; } }
static IMP aspect_msgForwardIMP(Class clazz, SEL selector) { IMP msgForwardIMP = _objc_msgForward; #if !defined(__arm64__) // As an ugly internal runtime implementation detail in the 32bit runtime, we need to determine of the method we hook returns a struct or anything larger than id. // https://developer.apple.com/library/mac/documentation/DeveloperTools/Conceptual/LowLevelABI/000-Introduction/introduction.html // https://github.com/ReactiveCocoa/ReactiveCocoa/issues/783 // http://infocenter.arm.com/help/topic/com.arm.doc.ihi0042e/IHI0042E_aapcs.pdf (Section 5.4) Method method = class_getInstanceMethod(clazz, selector); const char *encoding = method_getTypeEncoding(method); BOOL methodReturnsStructValue = encoding[0] == _C_STRUCT_B; if (methodReturnsStructValue) { @try { NSUInteger valueSize = 0; NSGetSizeAndAlignment(encoding, &valueSize, NULL); if (valueSize == 1 || valueSize == 2 || valueSize == 4 || valueSize == 8) { methodReturnsStructValue = NO; } } @catch (NSException *e) {} } if (methodReturnsStructValue) { msgForwardIMP = (IMP)_objc_msgForward_stret; } #endif return msgForwardIMP; }
这里是对32位操作系统的优化。可忽略
static void* aop_invokeOriginalInvocation(NSInvocation *invocation, SEL selector) { invocation.selector = selector; [invocation invoke]; if (strcmp(invocation.methodSignature.methodReturnType, @encode(void)) == 0) { return NULL; } void *result; [invocation getReturnValue:&result]; return result; } static void* aop_invokeHookedBlock(id self, AspectIdentifier *identifier, NSInvocation *invocation, SEL selector) { invocation.selector = selector; AspectObject *object = [[AspectObject alloc] initWithInstance:self invocation:invocation]; return [identifier invokeWithObject:object]; }
最后这几个函数就不多解释了,能坚持到这里的肯定能明白,不明白的可以在评论区留言
更新后的Aspect对比以前有很大的变化,比如object 关键字换成了 target, 舍弃了container,使用了os_unfair_lock锁来代替以前不安全的锁等等。
作者 | 楚奕 来源 | 阿里技术公众号 这篇文章主要从技术视角介绍下跨平台WebCanv...
1.在报名的路上,我看见远处的学校,轰!的一声没了。希望如此。 2.男:我一直...
1.百度是个大骗子,我抄了十几年的满分作文却从未得过满分。 2.学神在刷难题,...
背景 有时候我会碰到快速搭建测试服务的需求,比如像这样: 搭建一个 HTTP Servi...
1.某女生寝室门口贴着一个告示男生与饭盒不得入内,问何解?答曰两者都会搞大女...
本文转载自微信公众号「后端Q」,作者conan。转载本文请联系后端Q公众号。 概述 ...
创业与投资的本质,都是追寻一种能够穿越时空,抵达未来的高效方式。 德勤管理咨...
基于阿里巴巴的互联网架构、大数据技术,利用混合云架构打造全新的云化电子税 务...
3月24日,腾讯发布2020年Q4及全年财报,其中金融科技及企业服务第四季收入385亿...
前言 微服务成了互联网架构的标配模式,对微服务之间的调用的流量治理和管控就尤...