内容简介:大家可以试试以下代码结果是,通过
Aspects
一直在项目中使用,主要使用AOP方式进行tracking,但还没好好看一看,最近研究了一下源码,十分推荐大家阅读一下,如果只是一味的看 Runtime
源码,很难真正的掌握还容易忘,配合着看像 Aspects
这样优秀的框架有助于形成知识体系,而且代码量也不大,本文只是分析主要的一些源码,可以到我的github上看更多源码注释 AspectsAnalysis 。
NSMethodSignature和NSInvocation
大家可以试试以下代码
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { // 获取某个类的实例方法签名有两种 NSMethodSignature *signature = [self methodSignatureForSelector:@selector(test:)]; // NSMethodSignature *signature = [ViewController instanceMethodSignatureForSelector:@selector(test:)]; // 获取某个类的类方法只有一种 // NSMethodSignature *signature = [ViewController methodSignatureForSelector:@selector(test:)]; // 获取方法签名对应的invocation NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; // 设置消息接受者,与[invocation setArgument:(__bridge void * _Nonnull)(self) atIndex:0]等价 [invocation setTarget:self]; // 设置要执行的selector, 与[invocation setArgument:@selector(test:) atIndex:1] [invocation setSelector:@selector(test:)]; //设置参数 NSString *str = @"hello world"; [invocation setArgument:&str atIndex:2]; //开始执行 [invocation invoke]; } - (void)test:(NSString*)string{ NSLog(@"test %@",string); } 复制代码
结果是,通过 NSMethodSignature
和 NSInvocation
也能完成实例对象的方法调用
2019-04-20 16:00:57.027828+0800 Test[7439:627386] test hello world 复制代码
NSMethodSignature
一个 NSMethodSignature
对象记录着某个方法的返回值类型信息以及参数类型信息。它用于转发消息接收者无法响应的消息。
上面代码中提供了获得类方法和实例方法签名的方式,也可以使用 signatureWithObjCTypes
创建方法签名。
@interface NSMethodSignature + (nullable NSMethodSignature *)signatureWithObjCTypes:(const char *)types; @end 复制代码
NSMethodSignature对象是根据字符串创建的,这里的字符串代表了某个方法的返回值类型以及参数类型的字符串编码( Objective-C type encodings )。
个方法签名包含代表方法返回值的一个或多个字符,后面跟上隐式参数self以及_cmd的字符串编码,然后后面再跟上零个或多个明确的参数的字符编码。可以通过methodReturnType属性获取返回值类型的字符编码,可以通过methodReturnLength属性获取返回值类型的长度。
例如:NSString的实例方法containsString:的方法签名包含以下参数:
BOOL
NSInvocation
NSInvocation
封装了方法调用对象、方法选择器、参数、返回值等,可以给对象发送一个参数大于两个的消息,可以直接设置这些元素中的每一个,并在 NSInvocation
调度对象时自动设置返回值。
Objective-C方法调用过程
在 Objective-C
的方法调用过程中,如果 selector
有对应的 IMP
,则直接执行。如果没有,在抛出异常之前还有一些弥补机会,依次有 resolveInstanceMethod
、 forwardingTargetForSelector
、 forwardInvocatio
- resolveInstanceMethod (或resolveClassMethod) :实现该方法,可以通过
class_addMethod
添加方法,返回YES的话系统在运行时就会重新启动一次消息发送的过程,NO的话会继续执行下一个方法。
+ (BOOL)resolveInstanceMethod:(SEL)sel{ if (sel == @selector(runTo:)) { class_addMethod(self, sel, (IMP)dynamicMethodIMPRunTo, "v@:@"); return YES; } return [super resolveInstanceMethod:sel]; } //动态添加的@selector(runTo:) 对应的实现 static void dynamicMethodIMPRunTo(id self, SEL _cmd,id place){ NSLog(@"dynamicMethodIMPRunTo %@",place); } 复制代码
- forwardingTargetForSelector :实现该方法可以将消息转发给其他对象,只要这个方法返回的不是
nil
或self
,也会重启消息发送的过程,把这消息转发给其他对象来处理。
-(id)forwardingTargetForSelector:(SEL)aSelector{ if (aSelector == @selector(dynamicSelector) && [self.myObj respondsToSelector:@selector(dynamicSelector)]) { return self.myObj; }else{ return [super forwardingTargetForSelector:aSelector]; } } 复制代码
如果上面两步都无法完成这个 SEL
的处理,就会通过 forwardInvocation
进行消息转发
- methodSignatureForSelector :会去获取一个方法签名,如果没有获取到的话就回直接挑用
doesNotRecognizeSelector
,如果能获取的话系统就会创建一个NSlnvocation
传给forwardInvocation
方法。
- (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector{ //判断selector是否为需要转发的,如果是则手动生成方法签名并返回。 if (aSelector == @selector(dynamicSelector)){ return [NSMethodSignature signatureWithObjCTypes:"v@:@"]; } return [super forwardingTargetForSelector:aSelector]; } 复制代码
- forwardInvocation :该方法是上一个步传进来的
NSlnvocation
,然后调用NSlnvocation
的invokeWithTarget
方法,转发到对应的Target
。
- (void)forwardInvocation:(NSInvocation *)anInvocation{ //判断待处理的anInvocation是否为我们要处理的 if (anInvocation.selector == @selector(dynamicSelector)){ }else{ } } 复制代码
- doesNotRecognizeSelector :抛出
unrecognized selector sent to …
异常
上面描述的是正常的方法调用过程,如果想手动出发消息转发怎么办呢? _objc_msgForward
或者 _objc_msgForward_stret
,那他们区别是什么呢?
IMP msgForwardIMP = _objc_msgForward; #if !defined(__arm64__) if (typeDescription[0] == '{') { //In some cases that returns struct, we should use the '_stret' API: //http://sealiesoftware.com/blog/archive/2008/10/30/objc_explain_objc_msgSend_stret.html //NSMethodSignature knows the detail but has no API to return, we can only get the info from debugDescription. NSMethodSignature *methodSignature = [NSMethodSignature signatureWithObjCTypes:typeDescription]; if ([methodSignature.debugDescription rangeOfString:@"is special struct return? YES"].location != NSNotFound) { msgForwardIMP = (IMP)_objc_msgForward_stret; } } 复制代码
JSPatch
通过判断方法签名的 debugDescription
是不是包含特定字符串 is special struct return? YES
,进而决定是否使用 _objc_msgForward_stret
JSPatch
作者解释
大多数CPU在执行C函数时会把前几个参数放进寄存器里,对 obj_msgSend 来说前两个参数固定是 self / _cmd,它们会放在寄存器上,在最后执行完后返回值也会保存在寄存器上,取这个寄存器的值就是返回值。普通的返回值(int/pointer)很小,放在寄存器上没问题,但有些 struct 是很大的,寄存器放不下,所以要用另一种方式,在一开始申请一段内存,把指针保存在寄存器上,返回值往这个指针指向的内存写数据,所以寄存器要腾出一个位置放这个指针,self / _cmd 在寄存器的位置就变了。objc_msgSend 不知道 self / _cmd 的位置变了,所以要用另一个方法 objc_msgSend_stret 代替。原理大概就是这样。在 NSMethodSignature 的 debugDescription 上打出了是否 special struct,只能通过这字符串判断。所以最终的处理是,在非 arm64 下,是 special struct 就走 _objc_msgForward_stret,否则走 _objc_msgForward。
static IMP aspect_getMsgForwardIMP(NSObject *self, SEL selector) { IMP msgForwardIMP = _objc_msgForward; #if !defined(__arm64__) Method method = class_getInstanceMethod(self.class, 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 (__unused NSException *e) {} } if (methodReturnsStructValue) { msgForwardIMP = (IMP)_objc_msgForward_stret; } #endif return msgForwardIMP; } 复制代码
Aspects
是判断方法返回值的内存大小,来决定是否使用 _objc_msgForward_stret
Aspects基本原理
- 将hook的
selector
指向objc_msgForward / _objc_msgForward_stret
。 - 生成
aliasSelector
指向原来的selector
的IMP。 - 将
forwardInvocation
指向自定义的__ASPECTS_ARE_BEING_CALLED__
。 - 生成
__aspects_forwardInvocation
指向原来forwardInvocation
的IMP
Aspects源码分析
有前面的铺垫,再看源码就会轻松很多,源码只会分析一些比较重要的部分。
Aspects一些内部结构和协议
这里只简单介绍一下,详细结构可以查看源码。
- AspectToken:用于注销 Hook。
- AspectInfo:block的第一个参数。
- AspectIdentifier:每进行一个hook,都会生成一个AspectIdentifier对象,包含:方法,block,签名信息等。
- AspectsContainer:用于盛放AspectIdentifier对象。一个对象或者类对应一个AspectsContainer对象,有三个数组,beforeAspects,insteadAspects,afterAspects。
- AspectTracker:每一个class对应一个AspectTracker。 在一个继承链上一个selector只能被hook一次。
_AspectBlock
因为没法直接拿到 block
的签名信息,所以创建 _AspectBlock
目的是拿到block的签名信息,然后就可以使用NSInvocation调用这个block。
我们先来看看苹果源码Block_private.h的 Block
内存结构,是一个结构体。
struct Block_layout { void *isa; volatile int32_t flags; // contains ref count int32_t reserved; void (*invoke)(void *, ...); struct Block_descriptor_1 *descriptor; // imported variables }; 复制代码
// Values for Block_layout->flags to describe block objects enum { BLOCK_DEALLOCATING = (0x0001), // runtime BLOCK_REFCOUNT_MASK = (0xfffe), // runtime BLOCK_NEEDS_FREE = (1 << 24), // runtime BLOCK_HAS_COPY_DISPOSE = (1 << 25), // compiler BLOCK_HAS_CTOR = (1 << 26), // compiler: helpers have C++ code BLOCK_IS_GC = (1 << 27), // runtime BLOCK_IS_GLOBAL = (1 << 28), // compiler BLOCK_USE_STRET = (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE BLOCK_HAS_SIGNATURE = (1 << 30), // compiler BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31) // compiler }; 复制代码
再来看看 _AspectBlock
,可以很清晰的看到 Aspects
仿照系统定义的。
typedef NS_OPTIONS(int, AspectBlockFlags) { // 捕获外界变量 AspectBlockFlagsHasCopyDisposeHelpers = (1 << 25), // 方法有签名信息,Block也有签名信息 AspectBlockFlagsHasSignature = (1 << 30) }; 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_blockMethodSignature
方法是用来获得 blocK
的签名。原理是因为没法直接拿到 block
的签名信息,所以将 block
强制类型转换为 AspectBlockRef
,根据标志位和结构体的结构,获取 signature
。
static NSMethodSignature *aspect_blockMethodSignature(id block, NSError **error) { // 将block强制转为AspectBlockRef AspectBlockRef layout = (__bridge void *)block; if (!(layout->flags & AspectBlockFlagsHasSignature)) { NSString *description = [NSString stringWithFormat:@"The block %@ doesn't contain a type signature.", block]; AspectError(AspectErrorMissingBlockSignature, description); return nil; } // descriptor,指针移动 void *desc = layout->descriptor; desc += 2 * sizeof(unsigned long int); // 如果捕获外界变量 if (layout->flags & AspectBlockFlagsHasCopyDisposeHelpers) { desc += 2 * sizeof(void *); } if (!desc) { NSString *description = [NSString stringWithFormat:@"The block %@ doesn't has a type signature.", block]; AspectError(AspectErrorMissingBlockSignature, description); return nil; } const char *signature = (*(const char **)desc); // 根据类型编码获得签名信息 return [NSMethodSignature signatureWithObjCTypes:signature]; } 复制代码
这里稍微解释指针移动,我们知道指针是指向一块内存的首地址, desc += 2 * sizeof(unsigned long int);
是因为需要偏移下面两个 unsigned long int)
内存大小。
unsigned long int reserved; unsigned long int size; 复制代码
如果捕获外界变量,这两个 Void
指针是有值得,所以需要偏移两个 Void
指针的内存大小 desc += 2 * sizeof(void *);
void (*copy)(void *dst, const void *src); void (*dispose)(const void *); 复制代码
我们看一看 block
签名信息是什么样
[UIViewController aspect_hookSelector:@selector(viewDidLoad) withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo> info){ NSLog(@"viewDidLoad"); } error:nil]; 复制代码
返回值是 Void
,第一个参数是 @?
,表示是 Block
,第二个参数 @"<AspectInfo>"
,表示遵循了 AspectInfo
协议,我们看到 block
签名和方法签名是不同的,所以需要比较签名信息。
aspect_isCompatibleBlockSignature
static BOOL aspect_isCompatibleBlockSignature(NSMethodSignature *blockSignature, id object, SEL selector, NSError **error) { NSCParameterAssert(blockSignature); NSCParameterAssert(object); NSCParameterAssert(selector); BOOL signaturesMatch = YES; // viewWillAppear: (v @ : c) NSMethodSignature *methodSignature = [[object class] instanceMethodSignatureForSelector:selector]; // block签名参数一定是小于方法签名参数 if (blockSignature.numberOfArguments > methodSignature.numberOfArguments) { signaturesMatch = NO; }else { if (blockSignature.numberOfArguments > 1) { const char *blockType = [blockSignature getArgumentTypeAtIndex:1]; // 遵循AspectInfo协议对象 if (blockType[0] != '@') { signaturesMatch = NO; } } // Argument 0 is self/block, argument 1 is SEL or id<AspectInfo>. 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) { NSString *description = [NSString stringWithFormat:@"Block signature %@ doesn't match %@.", blockSignature, methodSignature]; AspectError(AspectErrorIncompatibleBlockSignature, description); return NO; } return YES; } 复制代码
从注释可以看到签名信息参数前两位是默认的, Argument 0
是 self/block
, argument 1
是 SEL or id<AspectInfo>
,所以从index = 2开始校验。
aspect_add
// 这里的self可以是实例对象 也可以是类对象 static id aspect_add(id self, SEL selector, AspectOptions options, id block, NSError **error) { NSCParameterAssert(self); NSCParameterAssert(selector); NSCParameterAssert(block); __block AspectIdentifier *identifier = nil; aspect_performLocked(^{ // 判断selector是否允许进行hook操作 // 1."retain"、"release"、"autorelease"、"forwardInvocation"这几个方法是不被允许的。 // 2.如果方法是dealloc,则他的切入点必须是Before。 // 3.判断当前实例对象和类对象是否能响应方法。 // 4.是否是类对象,如果是则判断继承体系中方法是否已经被Hook,而实例则不用。 if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) { // 通过objc_getAssociatedObject 获取selector方法的容器 AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector); // 将block封装到AspectIdentifier对象中 identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error]; if (identifier) { // 通过options将identifier添加到容器对应的beforeAspects,insteadAspects,afterAspects数组中 [aspectContainer addAspect:identifier withOptions:options]; // HookSelector和HookClass aspect_prepareClassAndHookSelector(self, selector, error); } } }); return identifier; } 复制代码
aspect_prepareClassAndHookSelector
static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) { NSCParameterAssert(selector); // Hook Class,进行swizzleForwardInvocation // klass类为刚创建的具有_Aspects_后缀的子类 Class klass = aspect_hookClass(self, error); // 在创建的时候指定类他的父类,所以我们可以获取到selector这个方法 Method targetMethod = class_getInstanceMethod(klass, selector); IMP targetMethodIMP = method_getImplementation(targetMethod); // 如果selector的实现是_objc_msgForward或者_objc_msgForward_stret,就不进行method swizzle 了。 if (!aspect_isMsgForwardIMP(targetMethodIMP)) { // 获得原生方法的类型编码 const char *typeEncoding = method_getTypeEncoding(targetMethod); // 给原selector方法名添加前缀,并返回。 这个添加前缀的selector的实现就是原selector的实现 SEL aliasSelector = aspect_aliasForSelector(selector); if (![klass instancesRespondToSelector:aliasSelector]) { // 没有使用aliasSelector 保存selector原来的实现 __unused BOOL addedAlias = class_addMethod(klass, aliasSelector, method_getImplementation(targetMethod), typeEncoding); NSCAssert(addedAlias, @"Original implementation for %@ is already copied to %@ on %@", NSStringFromSelector(selector), NSStringFromSelector(aliasSelector), klass); } // 将被hook方法的实现改为forwardInvocation(消息转发) class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding); AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector)); } } 复制代码
总结一下:
- 将
forwardInvocation
的实现替换为 自定义方法__ASPECTS_ARE_BEING_CALLED__
,并添加__aspects_forwardInvocation
的实现为forwardInvocation
原来的实现。 需要进行消息转发的selector
都会执行__ASPECTS_ARE_BEING_CALLED__
- 将
hook
的selector
的实现替换为_objc_msgForward
或者_objc_msgForward_stret
,同时添加aspect_aliasForSelector
的实现为selector
原来的实现。 此时调用selector
时就会进行消息转发。
aspect_hookClass
static Class aspect_hookClass(NSObject *self, NSError **error) { NSCParameterAssert(self); // 当self是instance(对象)获取当前Class(类对象),当self是Class(类对象)返回自身 Class statedClass = self.class; // 获取isa指针 Class baseClass = object_getClass(self); NSString *className = NSStringFromClass(baseClass); // 当hook一个对象的selector时会生成一个子类,子类前缀就是AspectsSubclassSuffix。当self对应的类就是生成的子类,直接返回 if ([className hasSuffix:AspectsSubclassSuffix]) { return baseClass; // 判断是否为类对象,如果是,则直接在当前类中进行swizzle }else if (class_isMetaClass(baseClass)) { return aspect_swizzleClassInPlace((Class)self); // 判断是否为KVO过的对象,因为KVO的对象ISA指针指向一个中间类,则直接在这个间接勒种进行swizzle }else if (statedClass != baseClass) { return aspect_swizzleClassInPlace(baseClass); } /** 当hook一个对象的selector时,实现原理与KVO相似。 1,生成一个子类;2,aspect_swizzleForwardInvocation*/ // 默认情况下,动态创建子类,拼接子类后缀为AspectsSubclassSuffix const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String; // 获取子类isa Class subclass = objc_getClass(subclassName); if (subclass == nil) { subclass = objc_allocateClassPair(baseClass, subclassName, 0); if (subclass == nil) { NSString *errrorDesc = [NSString stringWithFormat:@"objc_allocateClassPair failed to allocate class %s.", subclassName]; AspectError(AspectErrorFailedToAllocateClassPair, errrorDesc); return nil; } // 替换当前类的forwardInvocation的方法实现为__ASPECTS_ARE_BEING_CALLED__ aspect_swizzleForwardInvocation(subclass); // 把生成子类的isa指针指向原生的类 aspect_hookedGetClass(subclass, statedClass); // 把生成子类的元类的isa指向原生的类 aspect_hookedGetClass(object_getClass(subclass), statedClass); // 注册当前生成的子类 objc_registerClassPair(subclass); } // 将当前对象的isa指针指向刚生成的类 object_setClass(self, subclass); return subclass; } 复制代码
总结一下:
- 动态创建子类
- 将子类的
forwardInvocation
的实现替换成__ASPECTS_ARE_BEING_CALLED__
- 把子类的元类的
isa
和子类的元类的isa
指向原生的类 - 注册子类
- 把
self
对象isa
指针指向子类
图片来自 Aspects关联&调用流程浅析
对某个类的所有实例进行hook
某个类实例进行hook
ASPECTS_ARE_BEING_CALLED
// 消息经过转发后都会来到这里(这里包括手动消息转发和自动消息转发)在这里进行统一的处理: 调用block,执行原方法实现 // This is the swizzled forwardInvocation: method. static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) { NSCParameterAssert(self); NSCParameterAssert(invocation); // 拿到originalSelector SEL originalSelector = invocation.selector; // originalSelector 加前缀得到 aliasSelector,含有前缀的方法aspects_ SEL aliasSelector = aspect_aliasForSelector(invocation.selector); // 用 aliasSelector 替换 invocation.selector invocation.selector = aliasSelector; // Instance 的容器 AspectsContainer *objectContainer = objc_getAssociatedObject(self, aliasSelector); // Class 的容器 AspectsContainer *classContainer = aspect_getContainerForClass(object_getClass(self), aliasSelector); AspectInfo *info = [[AspectInfo alloc] initWithInstance:self invocation:invocation]; NSArray *aspectsToRemove = nil; // Before hooks. aspect_invoke(classContainer.beforeAspects, info); aspect_invoke(objectContainer.beforeAspects, info); // Instead hooks. BOOL respondsToAlias = YES; if (objectContainer.insteadAspects.count || classContainer.insteadAspects.count) { aspect_invoke(classContainer.insteadAspects, info); aspect_invoke(objectContainer.insteadAspects, info); }else { // 没有Instead hooks时就执行selector 被hook之前的实现。 Class klass = object_getClass(invocation.target); // 遍历 invocation.target及其superClass找到实例可以响应 aliasSelector的invocation invoke do { if ((respondsToAlias = [klass instancesRespondToSelector:aliasSelector])) { [invocation invoke]; break; } }while (!respondsToAlias && (klass = class_getSuperclass(klass))); } // After hooks. aspect_invoke(classContainer.afterAspects, info); aspect_invoke(objectContainer.afterAspects, info); // 调用一个没有实现的selector会触发 自动消息转发,在这种情况下整个继承链中都不会响应aliasSelector也就导致respondsToAlias=false, 开始执行下面的方法 // If no hooks are installed, call original implementation (usually to throw an exception) if (!respondsToAlias) { invocation.selector = originalSelector; SEL originalForwardInvocationSEL = NSSelectorFromString(AspectsForwardInvocationSelectorName); // 如果实现了forwardInvocation,执行原来的消息转发,否则调用doesNotRecognizeSelector,抛出异常。 if ([self respondsToSelector:originalForwardInvocationSEL]) { ((void( *)(id, SEL, NSInvocation *))objc_msgSend)(self, originalForwardInvocationSEL, invocation); }else { [self doesNotRecognizeSelector:invocation.selector]; } } // Remove any hooks that are queued for deregistration. // 移除 aspectsToRemove 队列中的 AspectIdentifier,执行 remove [aspectsToRemove makeObjectsPerformSelector:@selector(remove)]; } 复制代码
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 以太坊源码分析(36)ethdb源码分析
- [源码分析] kubelet源码分析(一)之 NewKubeletCommand
- libmodbus源码分析(3)从机(服务端)功能源码分析
- [源码分析] nfs-client-provisioner源码分析
- [源码分析] kubelet源码分析(三)之 Pod的创建
- Spring事务源码分析专题(一)JdbcTemplate使用及源码分析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Programming Rust
Jim Blandy / O'Reilly Media / 2016-8-25 / GBP 47.99
This practical book introduces systems programmers to Rust, the new and cutting-edge language that’s still in the experimental/lab stage. You’ll learn how Rust offers the rare and valuable combination......一起来看看 《Programming Rust》 这本书的介绍吧!