开源库信息

之前有写了一篇博客《iOS 基于JLRoutes 改造的路由》那时候比较忙所以只是简单地画了一个大致的框图,这段时间有时间所以抽空再写一篇针对JRLRoute源码解析的文章。
JLRoute代码量不大,建议大家可以跟着这篇博客简单过下整个代码:

源码解析

在对源码进行分析之前需要先了解下JLRoutes的大致结构,JLRoutes通过sheme将整个路由空间分隔开,每个sheme相当于一个命名空间,每个sheme对应一个JLRoutes,每个JLRoutes有一个属性mutableRoutes 用于存放它所能匹配的JLRRouteDefinition。每个JLRRouteDefinition针对一个路由匹配规则,负责路由的匹配。每个路由匹配都被封装为JLRRouteRequest,交给JLRRouteDefinition进行匹配,JLRRouteDefinition会针对该request 输出一个匹配结果JLRRouteResponse。里面包含了是否匹配的信息。大体就是这么一个流程。

我们下面将针对上面介绍的结构一步一步进行介绍:

通过sheme分隔路由空间

+ (instancetype)routesForScheme:(NSString *)scheme {
JLRoutes *routesController = nil;

static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
JLRGlobal_routeControllersMap = [[NSMutableDictionary alloc] init];
});
//如果之前没有注册当前scheme,则创建JLRoutes
if (!JLRGlobal_routeControllersMap[scheme]) {
routesController = [[self alloc] init];
routesController.scheme = scheme;
JLRGlobal_routeControllersMap[scheme] = routesController;
}
return JLRGlobal_routeControllersMap[scheme];;
}

+ (instancetype)globalRoutes {
return [self routesForScheme:JLRoutesGlobalRoutesScheme];
}

每个路由都要被添加到指定的scheme空间,存储在JLRGlobal_routeControllersMap中,JLRGlobal_routeControllersMap是一个字典类型,key为scheme,value为指定的JLRoutes。默认的情况下JLRoutes帮我们提供了一个全局的命名空间globalRoutes。

注册指定的路由匹配规则

- (void)addRoute:(NSString *)routePattern handler:(BOOL (^)(NSDictionary<NSString *, id> *parameters))handlerBlock {
[self addRoute:routePattern priority:0 handler:handlerBlock];
}

- (void)addRoute:(NSString *)routePattern priority:(NSUInteger)priority handler:(BOOL (^)(NSDictionary<NSString *, id> *parameters))handlerBlock {
//从路由模式中匹配可选的路由模式
NSArray <NSString *> *optionalRoutePatterns = [JLRParsingUtilities expandOptionalRoutePatternsForPattern:routePattern];
JLRRouteDefinition *route = [[JLRGlobal_routeDefinitionClass alloc] initWithPattern:routePattern priority:priority handlerBlock:handlerBlock];
if (optionalRoutePatterns.count > 0) {
// there are optional params, parse and add them
for (NSString *pattern in optionalRoutePatterns) {
JLRRouteDefinition *optionalRoute = [[JLRGlobal_routeDefinitionClass alloc] initWithPattern:pattern priority:priority handlerBlock:handlerBlock];
[self _registerRoute:optionalRoute];
}
return;
}
[self _registerRoute:route];
}

我们以****/path/:thing/(/a)(/b)(/c)**** 为例子,这里有固定path /path/ 带参数的path **/:thing/*,可选的path **/(/a)(/b)(/c)。首先我们会通过expandOptionalRoutePatternsForPattern** 将可选的部分转换成固定的或者带参数的path。比如上面的*/path/:thing/(/a)(/b)(/c)** 会转换为如下几种:

/path/:thing/a/b/c
/path/:thing/a/b
/path/:thing/a/c
/path/:thing/b/a
/path/:thing/a
/path/:thing/b
/path/:thing/c

然后再通过_registerRoute注册上面的routePattern。

- (void)_registerRoute:(JLRRouteDefinition *)route {
//如果是最高优先级或者之前都没注册过就直接添加
if (route.priority == 0 || self.mutableRoutes.count == 0) {
[self.mutableRoutes addObject:route];
} else {
NSUInteger index = 0;
BOOL addedRoute = NO;
// 按照优先级进行排序
// search through existing routes looking for a lower priority route than this one
for (JLRRouteDefinition *existingRoute in [self.mutableRoutes copy]) {
if (existingRoute.priority < route.priority) {
// if found, add the route after it
[self.mutableRoutes insertObject:route atIndex:index];
addedRoute = YES;
break;
}
index++;
}
//如果没有找到则直接插在最后
// if we weren't able to find a lower priority route, this is the new lowest priority route (or same priority as self.routes.lastObject) and should just be added
if (!addedRoute) {
[self.mutableRoutes addObject:route];
}
}
[route didBecomeRegisteredForScheme:self.scheme];
}

所有的routePattern 都会被封装到JLRRouteDefinition 然后按照优先级添加到mutableRoutes中。

路由匹配

- (BOOL)routeURL:(NSURL *)URL withParameters:(NSDictionary *)parameters {
return [self _routeURL:URL withParameters:parameters executeRouteBlock:YES];
}
- (BOOL)_routeURL:(NSURL *)URL withParameters:(NSDictionary *)parameters executeRouteBlock:(BOOL)executeRouteBlock {

//.....

BOOL didRoute = NO;

//路由选项
JLRRouteRequestOptions options = [self _routeRequestOptions];
//构建路由请求
JLRRouteRequest *request = [[JLRRouteRequest alloc] initWithURL:URL options:options additionalParameters:parameters];

for (JLRRouteDefinition *route in [self.mutableRoutes copy]) {
// check each route for a matching response
// 通过请求返回一个内部匹配的结果
JLRRouteResponse *response = [route routeResponseForRequest:request];
//不匹配继续
if (!response.isMatch) {
continue;
}
//匹配了但是强制不执行直接返回
if (!executeRouteBlock) {
return YES;
}
//调用路由block传入参数,进行处理
didRoute = [route callHandlerBlockWithParameters:response.parameters];
if (didRoute) {
//已经处理退出循环
break;
}
}

//没有匹配
if (!didRoute) {
[self _verboseLog:@"Could not find a matching route"];
}

//如果没有找到匹配的,并且允许转到Globle路由则调用全局路由
if (!didRoute && self.shouldFallbackToGlobalRoutes && ![self _isGlobalRoutesController]) {
didRoute = [[JLRoutes globalRoutes] _routeURL:URL withParameters:parameters executeRouteBlock:executeRouteBlock];
}

// 如果没有则调用unmatchedURLHandler
if (!didRoute && executeRouteBlock && self.unmatchedURLHandler) {
self.unmatchedURLHandler(self, URL, parameters);
}
return didRoute;
}

如果说路由规则是一个固定的内容,那么路由匹配则是拿着一个个具体的URL去规则里面去匹配:

路由匹配有几个阶段:


1. 将路由URL以及参数封装成一个JLRRouteRequest
2. 使用已经注册好的JLRRouteDefinition去和JLRRouteRequest进行匹配,并生成一个JLRRouteResponse,如果匹配,则调用相应的回调。
3. 如果不匹配则根据我们的需求选择是否允许转到Globle路由去匹配,如果还没找到则选择是否调用unmatchedURLHandler

JLRRouteRequest路由请求

- (instancetype)initWithURL:(NSURL *)URL options:(JLRRouteRequestOptions)options additionalParameters:(nullable NSDictionary *)additionalParameters {
if ((self = [super init])) {
self.URL = URL; // URL
self.options = options; // options
self.additionalParameters = additionalParameters; // 额外参数

BOOL treatsHostAsPathComponent = ((options & JLRRouteRequestOptionTreatHostAsPathComponent) == JLRRouteRequestOptionTreatHostAsPathComponent);

//取出路径的各个部分
NSURLComponents *components = [NSURLComponents componentsWithString:[self.URL absoluteString]];

//是否将host作为path
if (components.host.length > 0
&& (treatsHostAsPathComponent || (![components.host isEqualToString:@"localhost"] && [components.host rangeOfString:@"."].location == NSNotFound))) {
// convert the host to "/" so that the host is considered a path component
NSString *host = [components.percentEncodedHost copy];
components.host = @"/";
components.percentEncodedPath = [host stringByAppendingPathComponent:(components.percentEncodedPath ?: @"")];
}

//格式化后的路径
NSString *path = [components percentEncodedPath];

// handle fragment if needed
if (components.fragment != nil) {
BOOL fragmentContainsQueryParams = NO;
NSURLComponents *fragmentComponents = [NSURLComponents componentsWithString:components.percentEncodedFragment];

if (fragmentComponents.query == nil && fragmentComponents.path != nil) {
fragmentComponents.query = fragmentComponents.path;
}

if (fragmentComponents.queryItems.count > 0) {
// determine if this fragment is only valid query params and nothing else
fragmentContainsQueryParams = fragmentComponents.queryItems.firstObject.value.length > 0;
}

if (fragmentContainsQueryParams) {
// include fragment query params in with the standard set
components.queryItems = [(components.queryItems ?: @[]) arrayByAddingObjectsFromArray:fragmentComponents.queryItems];
}

if (fragmentComponents.path != nil && (!fragmentContainsQueryParams || ![fragmentComponents.path isEqualToString:fragmentComponents.query])) {
// handle fragment by include fragment path as part of the main path
path = [path stringByAppendingString:[NSString stringWithFormat:@"#%@", fragmentComponents.percentEncodedPath]];
}
}

// strip off leading slash so that we don't have an empty first path component
if (path.length > 0 && [path characterAtIndex:0] == '/') {
path = [path substringFromIndex:1];
}

// strip off trailing slash for the same reason
if (path.length > 0 && [path characterAtIndex:path.length - 1] == '/') {
path = [path substringToIndex:path.length - 1];
}

// split apart into path components
self.pathComponents = [path componentsSeparatedByString:@"/"];

// convert query items into a dictionary
NSArray <NSURLQueryItem *> *queryItems = [components queryItems] ?: @[];
NSMutableDictionary *queryParams = [NSMutableDictionary dictionary];
for (NSURLQueryItem *item in queryItems) {
if (item.value == nil) {
continue;
}

if (queryParams[item.name] == nil) {
// first time seeing a param with this name, set it
queryParams[item.name] = item.value;
} else if ([queryParams[item.name] isKindOfClass:[NSArray class]]) {
// already an array of these items, append it
NSArray *values = (NSArray *)(queryParams[item.name]);
queryParams[item.name] = [values arrayByAddingObject:item.value];
} else {
// existing non-array value for this key, create an array
id existingValue = queryParams[item.name];
queryParams[item.name] = @[existingValue, item.value];
}
}

self.queryParams = [queryParams copy];
}
return self;
}
@interface JLRRouteRequest : NSObject

/// The URL being routed. 需要被路由的URL
@property (nonatomic, copy, readonly) NSURL *URL;
/// The URL's path components. URL的路径部分
@property (nonatomic, strong, readonly) NSArray *pathComponents;
/// The URL's query parameters. URL的请求参数部分
@property (nonatomic, strong, readonly) NSDictionary *queryParams;
/// Route request options, generally configured from the framework global options. 请求的可选部分
@property (nonatomic, assign, readonly) JLRRouteRequestOptions options;
/// Additional parameters to pass through as part of the match parameters dictionary. 额外参数
@property (nonatomic, copy, nullable, readonly) NSDictionary *additionalParameters;

@end

每个JLRRouteRequest包含了URL它是JLRRouteRequest其他部分的来源,pathComponents是URL中的path部分,queryParams是URL中的query部分,options是外部注入的选项配置,additionalParameters是外部注入的额外参数。进入下一阶段匹配的时候都是将这些参数与JLRRouteDefinition中定义的模版进行匹配。

JLRRouteDefinition 路由规则定义

我们接下来看下JLRRouts中的路由规则定义部分,以及路由匹配过程:

在JLRoutes中每个路由规则被定义为一个JLRRouteDefinition对象,我们先来看下它的结构:

/// 当前route 的 URL scheme
@property (nonatomic, copy, readonly) NSString *scheme;
/// 当前route匹配模版.
@property (nonatomic, copy, readonly) NSString *pattern;
/// 当前路由的优先级
@property (nonatomic, assign, readonly) NSUInteger priority;
/// 当前路由的匹配模版分隔后的Components,它通过‘/’对pattern字符串进行分隔后存到patternPathComponents
@property (nonatomic, copy, readonly) NSArray <NSString *> *patternPathComponents;
/// 在每次匹配的时候都会触发这个handlerBlock
@property (nonatomic, copy, readonly) BOOL (^handlerBlock)(NSDictionary *parameters);
- (instancetype)initWithPattern:(NSString *)pattern priority:(NSUInteger)priority handlerBlock:(BOOL (^)(NSDictionary *parameters))handlerBlock {
NSParameterAssert(pattern != nil);

if ((self = [super init])) {
self.pattern = pattern;
self.priority = priority;
self.handlerBlock = handlerBlock;
if ([pattern characterAtIndex:0] == '/') {
pattern = [pattern substringFromIndex:1];
}

self.patternPathComponents = [pattern componentsSeparatedByString:@"/"];
}
return self;
}

pattern 是当前路由的匹配规则比如/path/:things/a/b/c,而patternPathComponents则是通过‘/’对pattern字符串进行分隔后的子pathPattern。我们接下来看下最重要的路由匹配过程。

- (JLRRouteResponse *)routeResponseForRequest:(JLRRouteRequest *)request {
BOOL patternContainsWildcard = [self.patternPathComponents containsObject:@"*"];

//不包含通配符的情况下传入的request与当前的pathComponents长度不相等,这种情况确定是不匹配的
if (request.pathComponents.count != self.patternPathComponents.count && !patternContainsWildcard) {
return [JLRRouteResponse invalidMatchResponse];
}

//通过patternPathComponents模版定义从request pathComponents取出具体的参数值来填充routeVariables,routeVariables就是匹配出的变量值
NSDictionary *routeVariables = [self routeVariablesForRequest:request];

if (routeVariables != nil) {
// It's a match, set up the param dictionary and create a valid match response
//匹配的情况下将所有的参数放到JLRRouteResponse返回
NSDictionary *matchParams = [self matchParametersForRequest:request routeVariables:routeVariables];
return [JLRRouteResponse validMatchResponseWithParameters:matchParams];
} else {
// nil variables indicates no match, so return an invalid match response
return [JLRRouteResponse invalidMatchResponse];
}
}

routeResponseForRequest 中最关键的部分在于routeVariablesForRequest,它会在提取path中变量的过程中,一边针对path进行匹配,一边提取,一旦发现不匹配就直接返回。所以我们重点来看下routeVariablesForRequest部分:

- (NSDictionary <NSString *, NSString *> *)routeVariablesForRequest:(JLRRouteRequest *)request {
NSMutableDictionary *routeVariables = [NSMutableDictionary dictionary];

BOOL isMatch = YES;
NSUInteger index = 0;

for (NSString *patternComponent in self.patternPathComponents /*这里存着变量名*/) {
NSString *URLComponent = nil;

//是否是通配符
BOOL isPatternComponentWildcard = [patternComponent isEqualToString:@"*"];

// 1. 取出request中同样位置的pathComponents
if (index < [request.pathComponents count]) {
URLComponent = request.pathComponents[index]/*请求这里存着变量值*/;
} else if (!isPatternComponentWildcard) {
// 请求路径参数小于或者等于的时候还包含有通配符是不可能的
// URLComponent is not a wildcard and index is >= request.pathComponents.count, so bail
isMatch = NO;
break;
}

//从patternPathComponent看当前的path是否是带参数的path,带参数的path之前都是 :xxx这种以冒号开头
if ([patternComponent hasPrefix:@":"]) {
// this is a variable, set it in the params
NSAssert(URLComponent != nil, @"URLComponent cannot be nil");
// 获取变量名---> 将开头的冒号以及结尾的#去掉
NSString *variableName = [self routeVariableNameForValue:patternComponent];
// 变量值 ---> 将结尾的#去掉
NSString *variableValue = [self routeVariableValueForValue:URLComponent];

// Consult the parsing utilities as well to do any other standard variable transformations
BOOL decodePlusSymbols = ((request.options & JLRRouteRequestOptionDecodePlusSymbols) == JLRRouteRequestOptionDecodePlusSymbols);
variableValue = [JLRParsingUtilities variableValueFrom:variableValue decodePlusSymbols:decodePlusSymbols];

//将变量名为key,值为value 存在 routeVariables中
routeVariables[variableName] = variableValue;
} else if (isPatternComponentWildcard /*包含通配符的情况*/) {
// match wildcards
NSUInteger minRequiredParams = index;
if (request.pathComponents.count >= minRequiredParams) {
// match: /a/b/c/* has to be matched by at least /a/b/c
// 将通配的部分统一放到routeVariables[JLRouteWildcardComponentsKey]中
routeVariables[JLRouteWildcardComponentsKey] = [request.pathComponents subarrayWithRange:NSMakeRange(index, request.pathComponents.count - index)];
isMatch = YES;
} else {
// not a match: /a/b/c/* cannot be matched by URL /a/b/
isMatch = NO;
}
break;
} else if (![patternComponent isEqualToString:URLComponent]) {
// break if this is a static component and it isn't a match
isMatch = NO;
break;
}
index++;
}

if (!isMatch) {
// Return nil to indicate that there was not a match
routeVariables = nil;
}

return [routeVariables copy];
}

首先我们会遍历JLRRouteDefinition中的patternPathComponents,将它的每个元素取出来,如果发现当前的patternPathComponent是以冒号开头,那么表示,当前的path是一个变量,就从request中的pathComponents对应位置取出pathComponent。再将patternPathComponen开始的冒号以及结尾如果是#的去掉作为key,将requst pathComponent 作为value添加到routeVariables。

如果当前的匹配规则patternPathComponent是通配符,并且request的pathComponents长度大于当前的index,那么将request pathComponents中剩余的值都赋给routeVariables,这时候key为通配符符号*。
为什么能够这样做,以为我们规定通配符要放在路径的最后面。

除了变量,通配符之外只剩下静态component,这种只要判断是否相等就可以了。不相等就表示直接不匹配。

所以routeVariablesForRequest其实是要完成两部分任务,一部分负责匹配,一部分负责将path中的变量提取出来。

我们再回到routeResponseForRequest,提取完path中的全部变量后会执行下面这部分代码:

NSDictionary *matchParams = [self matchParametersForRequest:request routeVariables:routeVariables];

它其实是将上面从path提取出来的参数以及request.additionalParameters,request.queryParams合并起来作为最终的参数,并且在这最终的参数中添加request.URL,以及self.scheme。我们知道JLRRoutes的参数可以通过path指定,通过request.queryParams,通过request.additionalParameters 这三个地方指定。这里就是将这写参数合并起来,传递下去。后续的处理block拿到这些参数进行对应的逻辑处理。

- (NSDictionary *)matchParametersForRequest:(JLRRouteRequest *)request routeVariables:(NSDictionary <NSString *, NSString *> *)routeVariables {
NSMutableDictionary *matchParams = [NSMutableDictionary dictionary];

// Add the parsed query parameters ('?a=b&c=d'). Also includes fragment.
// 添加query参数
BOOL decodePlusSymbols = ((request.options & JLRRouteRequestOptionDecodePlusSymbols) == JLRRouteRequestOptionDecodePlusSymbols);
[matchParams addEntriesFromDictionary:[JLRParsingUtilities queryParams:request.queryParams decodePlusSymbols:decodePlusSymbols]];

// Add the actual parsed route variables (the items in the route prefixed with ':').
// 添加路径中的参数
[matchParams addEntriesFromDictionary:routeVariables];

// Add the additional parameters, if any were specified in the request.
// 添加request中附带的额外的参数
if (request.additionalParameters != nil) {
[matchParams addEntriesFromDictionary:request.additionalParameters];
}

// Finally, add the base parameters. This is done last so that these cannot be overriden by using the same key in your route or query.
// 添加request.URL ,self.scheme这些公共参数
[matchParams addEntriesFromDictionary:[self defaultMatchParametersForRequest:request]];

return [matchParams copy];
}

- (NSDictionary *)defaultMatchParametersForRequest:(JLRRouteRequest *)request {
return @{JLRoutePatternKey: self.pattern ?: [NSNull null], JLRouteURLKey: request.URL ?: [NSNull null], JLRouteSchemeKey: self.scheme ?: [NSNull null]};
}

有了参数之后就将参数封装到JLRRouteResponse中传递给处理block:

[JLRRouteResponse validMatchResponseWithParameters:matchParams]

JLRRouteResponse 十分简单就两个属性,一个用于标记是否匹配,另一个是当前路由的所有请求:

@interface JLRRouteResponse : NSObject <NSCopying>
/// 是否匹配
@property (nonatomic, assign, readonly, getter=isMatch) BOOL match;
/// 匹配的参数
@property (nonatomic, copy, readonly, nullable) NSDictionary *parameters;
@end

我们前面在介绍_routeURL的时候看到最终的response是传递到JLRRouteDefinition中的callHandlerBlockWithParameters方法中:

didRoute = [route callHandlerBlockWithParameters:response.parameters];

而callHandlerBlockWithParameters则是调用了handlerBlock将参数传递出去,在业务层进行处理:

- (BOOL)callHandlerBlockWithParameters:(NSDictionary *)parameters {
if (self.handlerBlock == nil) {
return YES;
}

return self.handlerBlock(parameters);
}

所以整个路由的过程是这样的:

  1. 在使用路由之前先注册一系列路由规则JLRRouteDefinition,每个路由规则有对应的匹配规则pathPattern,以及如果有对应的路由请求匹配这条规则后的处理。
  2. 将匹配规则封装成一个JLRRouteRequest请求,并拿它与所属的schema中的一系列JLRRouteDefinition进行匹配,并尝试按照JLRRouteDefinition中的规则提取对应的参数。
  3. 如果匹配了哪条规则,就会触发对应规则的handlerBlock,并将所有参数传递进去。我们在handlerBlock中就可以自定义我们的业务处理。

开源代码信息

源码地址

在编程中有两种比较重要的扩展方式,一种是面向对象编程中的继承方式,一种是面向切面编程,前者是纵向扩展,而后者是横向扩展,目前大多数的流行编程语音都有这两种扩展方式,在iOS开发中使用Aspect来对现有代码进行横向扩展,Aspect用于在当前selector之前或者之后插入代码块或者使用某个代码快替换当前selector,它通过OC的消息转发机制hook消息。所以会有一些性能开销,建议不要把Aspects加到经常被使用的方法里面。

源码解析

1. Aspect对外接口

Aspect 代码比较精炼,就两个文件两个接口,我们先来看下它对外的接口:

+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
- (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;

Aspects是NSObject的一个扩展,所以原则上只要是NSObject的实例都可以通过上面两个方法进行hook。
第一个参数selector是当前类需要hook的selector
第二个参数options用于指定调用切片方法的时机:

typedef NS_OPTIONS(NSUInteger, AspectOptions) {
AspectPositionAfter = 0, /// 在原方法实现调用之后调用 Called after the original implementation (default)
AspectPositionInstead = 1, /// 将会替换原方法的实现。 Will replace the original implementation.
AspectPositionBefore = 2, /// 在原方法调用之前调用。 Called before the original implementation.
AspectOptionAutomaticRemoval = 1 << 3 /// 在第一次执行之后自动移除hook Will remove the hook after the first execution.
};

它可以指定在原方法实现之后插入,这也是默认的方式,还可以使用block替换原来方法的实现,还可以在原方法之前插入指定的block,还可以只进行一次hook,第一次hook之后就会自动被移除。
第三个参数block是用于替换被hook selector的代码块
第四个参数error用于获取hook过程中的错误。

2. Hook流程分析

static id aspect_add(id self, SEL selector/*需要hook的SEL*/, AspectOptions options/*切片的时机*/, id block/*切片的执行方法*/, NSError **error) {
//.....
__block AspectIdentifier *identifier = nil;
//aspect_performLocked是一个自旋锁。自旋锁是效率比较高的一种锁,相比@synchronized来说效率高得多。
aspect_performLocked(^{
//是否允许hook
if (aspect_isSelectorAllowedAndTrack(self, selector, options, error)) {
//添加一个aspect__selectName === AspectsContainer
AspectsContainer *aspectContainer = aspect_getContainerForObject(self, selector);
//将调用数据封装成AspectIdentifier
//得到了aspectContainer之后,就可以开始准备我们要hook方法的一些信息。这些信息都装在AspectIdentifier中,所以我们需要新建一个AspectIdentifier。
//这个instancetype方法,只有一种情况会创建失败,那就是aspect_isCompatibleBlockSignature方法返回NO。返回NO就意味着我们要替换的方法block和要替换的原方法,两者的方法签名是不相符的
//方法签名匹配成功之后,就会创建好一个AspectIdentifier。
identifier = [AspectIdentifier identifierWithSelector:selector object:self options:options block:block error:error];
if (identifier) {
//容器中添加identifier
//aspectContainer容器会把它加入到容器中。完成了容器和AspectIdentifier初始化之后,就可以开始准备进行hook了。通过options选项分别添加到容器中的
[aspectContainer addAspect:identifier withOptions:options];
//添加一个aspects__selectName === AspectsContainer(identifier, options) ==== AspectIdentifier(包含了调用所需要的所有信息)
// Modify the class to allow message interception.
aspect_prepareClassAndHookSelector(self, selector, error);
}
}
});
return identifier;
}

Hook的最开始会调用aspect_add,这里会先用一个自旋锁来保证线程的安全,自旋锁是效率比较高的一种锁,相比@synchronized来说效率高得多,紧接着会调用aspect_isSelectorAllowedAndTrack来判断是否允许hook。如果允许hook那么会使用aspect__selectName作为属性名创建一个AspectsContainer类型的关联属性,这里需要注意的是每hook一个selector就会在当前对象中多出一个对应的Aspects容器,这里面存放的都是针对一个selector的切片,因为一个selecor可能会有多个不同的切片,每种类型的AspectOptions作为一个AspectIdentifier存放在AspectsContainer容器中。
最后通过aspect_prepareClassAndHookSelector对方法进行Hook.在介绍aspect_prepareClassAndHookSelector之前我们看下上面遗留的一些问题:

  1. 如何判断是否允许Hook
  2. aspect_getContainerForObject 的逻辑,AspectsContainer结构
  3. AspectIdentifier 结构

判断是否允许Hook

static BOOL aspect_isSelectorAllowedAndTrack(NSObject *self, SEL selector, AspectOptions options, NSError **error) {
//只用初始化一次的结构
static NSSet *disallowedSelectorList;
static dispatch_once_t pred;
dispatch_once(&pred, ^{
//不允许hook的方法selector
disallowedSelectorList = [NSSet setWithObjects:@"retain", @"release", @"autorelease", @"forwardInvocation:", nil];
});

// Check against the blacklist.
NSString *selectorName = NSStringFromSelector(selector);
//如果当前的selectorName在disallowedSelectorList中不允许hook抛出错误
if ([disallowedSelectorList containsObject:selectorName]) {
NSString *errorDescription = [NSString stringWithFormat:@"Selector %@ is blacklisted.", selectorName];
AspectError(AspectErrorSelectorBlacklisted, errorDescription);
return NO;
}

// Additional checks.
AspectOptions position = options & AspectPositionFilter;
//不允许在dealloc之后 或者替换dealloc
if ([selectorName isEqualToString:@"dealloc"] && position != AspectPositionBefore) {
NSString *errorDesc = @"AspectPositionBefore is the only valid position when hooking dealloc.";
AspectError(AspectErrorSelectorDeallocPosition, errorDesc);
return NO;
}

//在指定的类中找不到需要hook的方法(如果self和self.class里面都找不到该selector,会报错找不到该方法)
if (![self respondsToSelector:selector] && ![self.class instancesRespondToSelector:selector]) {
NSString *errorDesc = [NSString stringWithFormat:@"Unable to find selector -[%@ %@].", NSStringFromClass(self.class), selectorName];
AspectError(AspectErrorDoesNotRespondToSelector, errorDesc);
return NO;
}

// Search for the current class and the class hierarchy IF we are modifying a class object
if (class_isMetaClass(object_getClass(self))) {

Class klass = [self class];
NSMutableDictionary *swizzledClassesDict = aspect_getSwizzledClassesDict();
Class currentClass = [self class];

AspectTracker *tracker = swizzledClassesDict[currentClass];
//是否在子类已经hook过了
if ([tracker subclassHasHookedSelectorName:selectorName]) {
NSSet *subclassTracker = [tracker subclassTrackersHookingSelectorName:selectorName];
NSSet *subclassNames = [subclassTracker valueForKey:@"trackedClassName"];
NSString *errorDescription = [NSString stringWithFormat:@"Error: %@ already hooked subclasses: %@. A method can only be hooked once per class hierarchy.", selectorName, subclassNames];
AspectError(AspectErrorSelectorAlreadyHookedInClassHierarchy, errorDescription);
return NO;
}

do {
//获取当前类的tracker
tracker = swizzledClassesDict[currentClass];
//查看是否有hook了当前selector
if ([tracker.selectorNames containsObject:selectorName]) {
if (klass == currentClass) {
// Already modified and topmost!
return YES;
}
//在当前类中已经hook过
NSString *errorDescription = [NSString stringWithFormat:@"Error: %@ already hooked in %@. A method can only be hooked once per class hierarchy.", selectorName, NSStringFromClass(currentClass)];
AspectError(AspectErrorSelectorAlreadyHookedInClassHierarchy, errorDescription);
return NO;
}
} while ((currentClass = class_getSuperclass(currentClass)));
//经过上面合法性hook判断和类方法不允许重复替换的检查后,到此,就可以把要hook的信息记录下来,用AspectTracker标记
//当前类tracker -- 待hook方法
// 父tracker -- 待hook方法
// 父tracker -- 待hook方法
currentClass = klass;
AspectTracker *subclassTracker = nil;
do {
//当前类的tracker
tracker = swizzledClassesDict[currentClass];
//如果没有的话则新建tracker
if (!tracker) {
tracker = [[AspectTracker alloc] initWithTrackedClass:currentClass];
swizzledClassesDict[(id<NSCopying>)currentClass] = tracker;
}

//非第一次则添加到subclassTracker
if (subclassTracker) {
[tracker addSubclassTracker:subclassTracker hookingSelectorName:selectorName];
} else {
//第一次添加到tracker
[tracker.selectorNames addObject:selectorName];
}
// All superclasses get marked as having a subclass that is modified.
subclassTracker = tracker;
}while ((currentClass = class_getSuperclass(currentClass)));
} else {
return YES;
}
return YES;
}

aspect_isSelectorAllowedAndTrack方法中主要做两件事情:

  1. 判断当前selector是否允许Hook
  2. 如果还没建立Tracker则建立Tracker的层级关系

首先会检查当前需要hook的selector是否在黑名单中,Aspect里面不允许hook “retain”, “release”, “autorelease”, “forwardInvocation:”4种方法,而且hook “dealloc”方法的时机必须是before,并且selector要能被找到,并且在对象继承层级上没有被hook过,如果这些条件都满足才允许Hook。

然后重建Tracker的层级关系,每个AspectTracker包含被跟踪的类以及这个类被hook替换的selector名称,以及它子类的tracker。

aspect_getContainerForObject 的逻辑

static AspectsContainer *aspect_getContainerForObject(NSObject *self, SEL selector) {
NSCParameterAssert(self);
SEL aliasSelector = aspect_aliasForSelector(selector);
//设置关联对象aspect__selectorName AspectsContainer
//用这个字符串标记所有的selector,都加上前缀"aspects"。然后获得其对应的AssociatedObject关联对象,如果获取不到,就创建一个关联对象。最终得到selector有"aspects"前缀,对应的aspectContainer。
AspectsContainer *aspectContainer = objc_getAssociatedObject(self, aliasSelector);
if (!aspectContainer) {
aspectContainer = [AspectsContainer new];
objc_setAssociatedObject(self, aliasSelector, aspectContainer, OBJC_ASSOCIATION_RETAIN);
}
return aspectContainer;
}

aspect_getContainerForObject 方法中会在当前对象中为每个hook的selector添加一个关联属性,属性名称为aspect__selectorName 类型为AspectsContainer,AspectsContainer里面只存储一个selector相关的所有hook信息。

AspectsContainer 和 AspectIdentifier的结构

上面介绍过了AspectsContainer 代表的是一个selector 所包含的全部hook方法,包含在原有selector之前,之后,替换原有selector的block。
所以AspectsContainer 包含三个数组 beforeAspects,insteadAspects,afterAspects:

@interface AspectsContainer : NSObject
- (void)addAspect:(AspectIdentifier *)aspect withOptions:(AspectOptions)injectPosition;
- (BOOL)removeAspect:(id)aspect;
- (BOOL)hasAspects;
@property (atomic, copy) NSArray *beforeAspects;
@property (atomic, copy) NSArray *insteadAspects;
@property (atomic, copy) NSArray *afterAspects;
@end

在添加的时候会根据插入的位置分别存储到不同的数组:

- (void)addAspect:(AspectIdentifier *)aspect withOptions:(AspectOptions)options {
NSParameterAssert(aspect);
NSUInteger position = options&AspectPositionFilter;
switch (position) {
//会往NSArray后面添加aspect后返回新的数组的地址
case AspectPositionBefore: self.beforeAspects = [(self.beforeAspects ?:@[]) arrayByAddingObject:aspect]; break;
case AspectPositionInstead: self.insteadAspects = [(self.insteadAspects?:@[]) arrayByAddingObject:aspect]; break;
case AspectPositionAfter: self.afterAspects = [(self.afterAspects ?:@[]) arrayByAddingObject:aspect]; break;
}
}

AspectIdentifier 代表一个Aspect的切片的具体内容。里面包含了单个的 Aspect 的具体信息,包括执行时机,要执行 block 所需要用到的具体信息:包括方法签名、参数等等。

@interface AspectIdentifier : NSObject
+ (instancetype)identifierWithSelector:(SEL)selector object:(id)object options:(AspectOptions)options block:(id)block error:(NSError **)error;
- (BOOL)invokeWithInfo:(id<AspectInfo>)info;
@property (nonatomic, assign) SEL selector;
@property (nonatomic, strong) id block;
@property (nonatomic, strong) NSMethodSignature *blockSignature;
@property (nonatomic, weak) id object;
@property (nonatomic, assign) AspectOptions options;
@end

我们看下它的初始化方法:

+ (instancetype)identifierWithSelector:(SEL)selector object:(id)object options:(AspectOptions)options block:(id)block error:(NSError **)error {

//这个aspect_blockMethodSignature的目的是把传递进来的AspectBlock转换成NSMethodSignature的方法签名。
NSMethodSignature *blockSignature = aspect_blockMethodSignature(block, error);
//这个函数的作用是把我们要替换的方法block和要替换的原方法,通过签名进行对比,如果不兼容则返回nil
if (!aspect_isCompatibleBlockSignature(blockSignature, object, selector, error)) {
return nil;
}
AspectIdentifier *identifier = nil;
if (blockSignature) {
identifier = [AspectIdentifier new];
identifier.selector = selector;//selector
identifier.block = block; //用于替换的block
identifier.blockSignature = blockSignature;//block签名
identifier.options = options; //hook位置
identifier.object = object; // 被hook的对象
}
return identifier;
}

aspect_blockMethodSignature 用于获取block的签名信息,大家可以看下下面的注释,关于如何从Block中获取签名可以查看Aspects框架中Block的使用这篇博客。


static NSMethodSignature *aspect_blockMethodSignature(id block, NSError **error) {
AspectBlockRef layout = (__bridge void *)block;
//把入参block强制转换成AspectBlockRef类型,然后判断是否有AspectBlockFlagsHasSignature的标志位,如果没有,报不包含方法签名的error。
if (!(layout->flags & AspectBlockFlagsHasSignature)) {
//block 不包含方法签名
NSString *description = [NSString stringWithFormat:@"The block %@ doesn't contain a type signature.", block];
AspectError(AspectErrorMissingBlockSignature, description);
return nil;
}
void *desc = layout->descriptor;
desc += 2 * sizeof(unsigned long int);
if (layout->flags & AspectBlockFlagsHasCopyDisposeHelpers) {
desc += 2 * sizeof(void *);
}
//desc就是原来block里面对应的descriptor指针。descriptor指针往下偏移2个unsigned long int的位置就指向了copy函数的地址,
//如果包含Copy和Dispose函数,那么继续往下偏移2个(void)的大小。
//这时指针肯定移动到了const char signature的位置。如果desc不存在,那么也会报错,该block不包含方法签名。
if (!desc) {
//block 不包含方法签名
NSString *description = [NSString stringWithFormat:@"The block %@ doesn't has a type signature.", block];
AspectError(AspectErrorMissingBlockSignature, description);
return nil;
}
const char *signature = (*(const char **)desc);
//到了这里,就保证有方法签名且存在。最后调用NSMethodSignature的signatureWithObjCTypes方法,返回方法签名。
return [NSMethodSignature signatureWithObjCTypes:signature];
}

拿到Block签名后就可以通过aspect_isCompatibleBlockSignature来比较block的签名和要hook的selector方法的签名是否兼容:

static BOOL aspect_isCompatibleBlockSignature(NSMethodSignature *blockSignature, id object, SEL selector, NSError **error) {

//....

BOOL signaturesMatch = YES;
NSMethodSignature *methodSignature = [[object class] instanceMethodSignatureForSelector:selector];
//先比较方法签名的参数个数是否相等
if (blockSignature.numberOfArguments > methodSignature.numberOfArguments) {
signaturesMatch = NO;
}else {
//比较我们要替换的方法里面第一个参数是不是_cmd,对应的Type就是@
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<AspectInfo>. We start comparing at argument 2.
// The block can have less arguments than the method, that's ok.
// methodSignature 和 blockSignature 的return value都是void,所以对应的都是v。
// methodSignature的argument 0 是隐含参数 self,所以对应的是@。blockSignature的argument 0 是block,所以对应的是@?。
// methodSignature的argument 1 是隐含参数 _cmd,所以对应的是:。blockSignature的argument 1 是,所以对应的是@""。
// 从argument 2开始才是方法签名后面的对应可能出现差异,需要比较的参数列表。
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;
}

aspect_isCompatibleBlockSignature首先会判断block和selector的参数个数是否一致,然后检查block的第一个参数是否是_cmd,如果这两个条件都满足后才会继续从第二个参数开始比较block和selector之间的参数是否匹配。

invokeWithInfo 是调度切片block的方法,这个留在后面介绍。

到目前位置我们做了如下工作:在hook一个selector的时候会先判断当前selector是否可以被hook,如果可以则进行下一步:
紧接着我们会使用关联属性来为当前hook方法添加一个AspectsContainer,AspectsContainer当中有三个关键数组,分别用于存放option类型为AspectPositionAfter,AspectPositionInstead,AspectPositionBefore 的 AspectIdentifier。每个AspectIdentifier都包含一个切片的全部信息,这里最关键的是在初始化的时候提取的block签名信息,并且在这里会使用block签名信息和selector签名信息进行兼容性检查。AspectIdentifier内部还包含触发对应block的方法。

接下来我们继续看下关键的Hook部分:

static void aspect_prepareClassAndHookSelector(NSObject *self, SEL selector, NSError **error) {
//创建一个以"_Aspects_"为结尾的class作为当前的class的子类
Class klass = aspect_hookClass(self, error);
//获取本意要执行的selector
Method targetMethod = class_getInstanceMethod(klass, selector);
IMP targetMethodIMP = method_getImplementation(targetMethod);

//这里是判断当前IMP是不是_objc_msgForward或者_objc_msgForward_stret,即判断当前IMP是不是消息转发。如果是消息转发的话就不做hook处理
if (!aspect_isMsgForwardIMP(targetMethodIMP)) {
//如果不是消息转发,就先获取当前原始的selector对应的IMP的方法编码typeEncoding。
const char *typeEncoding = method_getTypeEncoding(targetMethod);
//获取aspect_selector方法
SEL aliasSelector = aspect_aliasForSelector(selector);
if (![klass instancesRespondToSelector:aliasSelector]) {
//如果子类里面不能响应aspects_xxxx,就为klass添加aspects_xxxx方法,方法的实现为原生方法的实现。也就是将原始的方法添加到创建的以"_Aspects_"为结尾的class
__unused BOOL addedAlias = class_addMethod(klass, aliasSelector, method_getImplementation(targetMethod), typeEncoding);
}
//将slector指向_objc_msgForward 和_objc_msgForward_stret,可想而知,当selector被执行的时候,也会触发消息转发从而进入forwardInvocation,而我们又对forwardInvacation进行了swizzling,因此,最终转入我们自己的处理逻辑代码中。
class_replaceMethod(klass, selector, aspect_getMsgForwardIMP(self, selector), typeEncoding);
AspectLog(@"Aspects: Installed hook for -[%@ %@].", klass, NSStringFromSelector(selector));
}
}

aspect_prepareClassAndHookSelector方法中会先创建一个以_Aspects_为结尾的class作为当前的class的子类,如果当前方法不是消息分发方法_objc_msgForward或者_objc_msgForward_stret,就进行方法替换。比如我们要调用classA 的testA方法,并且testA已经被hook了,那么这时候我们会创建一个classA_Aspects_类作为classA的子类。在调用classA 的testA方法的时候会先看下classA_Aspects_是否有aspects_testA方法,如果没有则添加一个方法aspects_testA指向原来的testA,并且将classA_Aspects_的selector方法替换为aspect_getMsgForwardIMP(self, selector)走消息分发途径。消息具体怎么分发的我们看下aspect_hookClass

static Class aspect_hookClass(NSObject *self, NSError **error) {
NSCParameterAssert(self);
Class statedClass = self.class; //是获取类对象
Class baseClass = object_getClass(self); //是获取到类的isa。
NSString *className = NSStringFromClass(baseClass);//类名

if ([className hasSuffix:AspectsSubclassSuffix]) {
return baseClass;
//如果包含了@"_Aspects_"后缀,代表该类已经被hook过了,直接return。
//如果不包含@"_Aspects_"后缀,再判断是否是baseClass是否是元类,如果是元类,调用aspect_swizzleClassInPlace。
} else if (class_isMetaClass(baseClass)) {
return aspect_swizzleClassInPlace((Class)self);
// Probably a KVO'ed class. Swizzle in place. Also swizzle meta classes in place.
//如果也不是元类,再判断statedClass 和 baseClass是否相等,如果不相等,说明为KVO过的对象,因为KVO的对象isa指针会指向一个中间类。对KVO中间类调用aspect_swizzleClassInPlace。
} else if (statedClass != baseClass) {
return aspect_swizzleClassInPlace(baseClass);
}

//当className没有包含@"_Aspects_"后缀,并且也不是元类,也不是KVO的中间类
//在当前类名中添加AspectsSubclassSuffix
//hook 是在runtime中动态创建子类的基础上实现的
const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8String;
Class subclass = objc_getClass(subclassName);
//正常情况下是没有这个添加AspectsSubclassSuffix的类的
if (subclass == nil) {
//创建一个新的class,它的父类为baseClass
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;
}
//这里是关键
aspect_swizzleForwardInvocation(subclass);
//把class的实例方法替换成返回statedClass,也就是说把调用class时候的isa指向了statedClass了。
aspect_hookedGetClass(subclass, statedClass);
aspect_hookedGetClass(object_getClass(subclass), statedClass);
objc_registerClassPair(subclass);
}
//将当前实例对象的class替换为添加AspectsSubclassSuffix的类
//所有的 swizzling 操作都发生在子类,这样做的好处是你不需要去更改对象本身的类,
//也就是,当你在 remove aspects 的时候,如果发现当前对象的 aspect 都被移除了,那么,你可以将 isa 指针重新指回对象本身的类,从而消除了该对象的 swizzling ,
//同时也不会影响到其他该类的不同对象)这样对原来替换的类或者对象没有任何影响而且可以在子类基础上新增或者删除aspect。
object_setClass(self, subclass);
//hookClass阶段就完成了,成功的把self hook成了其子类 xxx_Aspects_
return subclass;
}

aspect_hookClass 方法中会通过objc_allocateClassPair来创建一个“类名+_Aspects_”结尾的新类作为baseClass的子类。并且通过aspect_swizzleForwardInvocation将当前类forwardInvocation方法的实现为__ASPECTS_ARE_BEING_CALLED__。然后将当前实例对象的class替换为添加AspectsSubclassSuffix的类,所有的 swizzling 操作都发生在子类,这样做的好处是你不需要去更改对象本身的类,也就是,当你在remove aspects 的时候,如果发现当前对象的 aspect 都被移除了,那么我们可以将 isa 指针重新指回对象本身的类,从而消除了该对象的 swizzling ,同时也不会影响到其他该类的不同对象,这样对原来替换的类或者对象没有任何影响而且可以在子类基础上新增或者删除aspect。

我们看下消息转发方法的替换:

static NSString *const AspectsForwardInvocationSelectorName = @"__aspects_forwardInvocation:";
static void aspect_swizzleForwardInvocation(Class klass) {
NSCParameterAssert(klass);
//主要作用是替换当前类forwardInvocation方法的实现为__ASPECTS_ARE_BEING_CALLED__
IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
if (originalImplementation) {
//当调用forwardInvocation进行消息分发的时候会调用__ASPECTS_ARE_BEING_CALLED__ ,__ASPECTS_ARE_BEING_CALLED__ 的selector为 __aspects_forwardInvocation
class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
}
}

如果klass没有实现forwardInvocation就会添加forwardInvocation方法并指向__ASPECTS_ARE_BEING_CALLED__实现,这种情况originalImplementation返回的是nil,所以不会走下面的流程,也就是说如果原来方法没有forwardInvocation,则会添加forwardInvocation方法,如果有的话将会替换forwardInvocation的实现为__ASPECTS_ARE_BEING_CALLED__,并新建一个方法__aspects_forwardInvocation指向原先已经实现的forwardInvocation,也就是说经过aspect_swizzleForwardInvocation处理后,klass中一定有一个forwardInvocation方法指向__ASPECTS_ARE_BEING_CALLED__,如果原来有的情况下还会多出一个__aspects_forwardInvocation指向原来的forwardInvocation。

我们再来看下****ASPECTS_ARE_BEING_CALLED****

static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) {

//获取原始的selector
SEL originalSelector = invocation.selector;
//获取带有aspects_xxxx前缀的方法
SEL aliasSelector = aspect_aliasForSelector(invocation.selector);

//将当前请求selector替换为aspects_xxxx
//比如调用sel1--->aspects_sel1--->AspectContainer
invocation.selector = aliasSelector;
//获取实例对象的容器objectContainer,这里是之前aspect_add关联过的对象。
AspectsContainer *objectContainer = objc_getAssociatedObject(self, aliasSelector);
//获取获得类对象容器classContainer
AspectsContainer *classContainer = aspect_getContainerForClass(object_getClass(self), aliasSelector);
//初始化AspectInfo,传入self、invocation参数
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 {
Class klass = object_getClass(invocation.target);
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);

//如果hook没有被正常执行,那么就应该执行原来的方法。
// If no hooks are installed, call original implementation (usually to throw an exception)
if (!respondsToAlias) {
invocation.selector = originalSelector;
SEL originalForwardInvocationSEL = NSSelectorFromString(AspectsForwardInvocationSelectorName);
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 makeObjectsPerformSelector:@selector(remove)];
}

首先进入这里的时候会通过关联属性来获取aspect_selector对应的属性值,如果有被hook的话则会获取到存放hook切片信息的切片容器AspectsContainer。我们使用invocation信息创建AspectInfo,
然后调用aspect_invoke宏来执行AspectsContainer中的beforeAspects,insteadAspects,以及afterAspects,如果insteadAspects为空的话,则直接执行子类的aliasSelector。我们之前已经提到过了,在aspect_prepareClassAndHookSelector方法中会在子类添加一个aspect_selector的方法它指向selector实现,所以这里会执行原来方法中的selector。如果respondsToAlias = NO 表示instancesRespondToSelector返回NO,表示调用了一个没有实现的方法,所以走原来类的消息转发机制。

我们最后再来看下aspect_invoke这个宏:

#define aspect_invoke(aspects, info) \
for (AspectIdentifier *aspect in aspects) {\
[aspect invokeWithInfo:info];\
if (aspect.options & AspectOptionAutomaticRemoval) { \
aspectsToRemove = [aspectsToRemove?:@[] arrayByAddingObject:aspect]; \
} \
}

这里会遍历aspects中的AspectIdentifier,并调用invokeWithInfo执行对应的block。如果切片类型为AspectOptionAutomaticRemoval,则触发后将它移除。

- (BOOL)invokeWithInfo:(id<AspectInfo>)info {
//block的调度者封装
NSInvocation *blockInvocation = [NSInvocation invocationWithMethodSignature:self.blockSignature];
//原始调度者的封装
NSInvocation *originalInvocation = info.originalInvocation;
NSUInteger numberOfArguments = self.blockSignature.numberOfArguments;

// Be extra paranoid. We already check that on hook registration.
if (numberOfArguments > originalInvocation.methodSignature.numberOfArguments) {
AspectLogError(@"Block has too many arguments. Not calling %@", info);
return NO;
}

// The `self` of the block will be the AspectInfo. Optional.
if (numberOfArguments > 1) {
[blockInvocation setArgument:&info 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))) {
AspectLogError(@"Failed to allocate memory for block invocation.");
return NO;
}
//循环把originalInvocation中取出参数,赋值到argBuf中,然后再赋值到blockInvocation里
[originalInvocation getArgument:argBuf atIndex:idx];
[blockInvocation setArgument:argBuf atIndex:idx];
}
//最后把self.block赋值给blockInvocation的Target
[blockInvocation invokeWithTarget:self.block];

if (argBuf != NULL) {
free(argBuf);
}
return YES;
}

invokeWithInfo 方法中主要将原先selector的方法参数,提取出来,传到block上,再通过invokeWithTarget执行block。

总结:

最后我们来做个总结:
在我们hook某个方法的时候会在当前类上添加一个名字为aspect_selector的关联属性,这个关联属性类型为AspectContainer它是用于存放该selector相关的切片的容器,容器里面有三个数组,分别存放着
不同AspectOptions的AspectIdentifier,每个AspectIdentifier都包含着一个切片的具体信息。紧接着会创建一个以_Aspects_结尾的类作为当前类的子类,这里类里面会添加一个方法名称为aspect_selector的方法,方法的实现指向原来的selector实现。并且会为该子类添加一个指向__ASPECTS_ARE_BEING_CALLED__的forwardInvocation方法。如果原来的类已经实现了forwardInvocation,就会添加一个__aspects_forwardInvocation方法指向原先已经实现的forwardInvocation。这个方法会在Aspect消息转发机制走不通的情况下走这里的消息转发。

这以后再将子类的selector方法调用替换为Aspect消息转发,这样我们调用这个selector的时候就会通过Aspect 消息转发进入__ASPECTS_ARE_BEING_CALLED__,在__ASPECTS_ARE_BEING_CALLED__中会先尝试取出之前的关联属性的切片容器,执行容器内的切片block,在这会如果instead切片为空的时候会调用aspect_selector,由于上面已经将aspect_selector指向原来的selector实现,所以这里会有机会调用原来类的selector方法。如果原来类不能响应selector方法,那么就走原来类的forwardInvocation,进行消息转发。

重要的类及关系介绍:

****Hook流程图:****:

源码信息

JSPatch是iOS平台上的热修复方案,至于什么是热修复以及使用JSPatch是否会被苹果官方拒绝上架,这里不做介绍,这篇博客只关注JSPatch 源码本身。带大家过下JSPatch的源码。

源码解析

用例情景

JSPatch核心代码很精简但是这不意味着简单,还是需要仔细啃才能够理解它的思想。

我们先看下JSPatchDemo中的用法例子:

[JPEngine startEngine];
NSString *sourcePath = [[NSBundle mainBundle] pathForResource:@"demo" ofType:@"js"];
NSString *script = [NSString stringWithContentsOfFile:sourcePath encoding:NSUTF8StringEncoding error:nil];[JPEngine evaluateScript:script];

在例子中首先启动JPEngine,紧接着会去加载本地的demo.js。然后在JPEngine中执行本地的demo.js脚本。

我们看下原先的RootViewController JPViewController:

#import "JPViewController.h"

@implementation JPViewController

- (void)viewDidLoad {
[super viewDidLoad];
UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(0, 100, [UIScreen mainScreen].bounds.size.width, 50)];
[btn setTitle:@"Push JPTableViewController" forState:UIControlStateNormal];
[btn addTarget:self action:@selector(handleBtn:) forControlEvents:UIControlEventTouchUpInside];
[btn setBackgroundColor:[UIColor grayColor]];
[self.view addSubview:btn];
}

- (void)handleBtn:(id)sender {

}

@end

整个界面上有一个按钮,点击它会执行handleBtn方法,但是handleBtn目前是一个空方法,但是如果你将整个demo项目跑起来后会发现点击的时候会弹出一个TableView。但是如果注释掉之前将的JPEngine相关代码,点击就不会有任何响应,所以可以确定是JPEngine搞的鬼。我们看下demo.js:

defineClass('JPViewController', {
handleBtn: function(sender) {
var tableViewCtrl = JPTableViewController.alloc().init()
self.navigationController().pushViewController_animated(tableViewCtrl, YES)
}
})

defineClass('JPTableViewController : UITableViewController <UIAlertViewDelegate>', ['data'], {
dataSource: function() {
var data = self.data();
if (data) return data;
var data = [];
for (var i = 0; i < 20; i ++) {
data.push("cell from js " + i);
}
self.setData(data)
return data;
},
numberOfSectionsInTableView: function(tableView) {
return 1;
},
tableView_numberOfRowsInSection: function(tableView, section) {
return self.dataSource().length;
},
tableView_cellForRowAtIndexPath: function(tableView, indexPath) {
var cell = tableView.dequeueReusableCellWithIdentifier("cell")
if (!cell) {
cell = require('UITableViewCell').alloc().initWithStyle_reuseIdentifier(0, "cell")
}
cell.textLabel().setText(self.dataSource()[indexPath.row()])
return cell
},
tableView_heightForRowAtIndexPath: function(tableView, indexPath) {
return 60
},
tableView_didSelectRowAtIndexPath: function(tableView, indexPath) {
var alertView = require('UIAlertView').alloc().initWithTitle_message_delegate_cancelButtonTitle_otherButtonTitles("Alert",self.dataSource()[indexPath.row()], self, "OK", null);
alertView.show()
},
alertView_willDismissWithButtonIndex: function(alertView, idx) {
console.log('click btn ' + alertView.buttonTitleAtIndex(idx).toJS())
}
})

即使你不懂JavaScript 看到上面的代码估计也可以猜出个大概,它重写了handleBtn方法,在点击的时候会push一个JPTableViewController页面。并且使用JavaScript语言实现了JPTableViewController。也就是说它可以通过动态加载一个js文件,并且在这个js文件中改变现有代码的行为。既然本地的js文件也是需要转换为string后放到JSPatchEngine引擎中执行,如果将这个文件放到服务端下发下去,那么就可以通过后台动态控制我们应用的行为了,是不是很诱人的功能,当然我们不会将它用于实现大需求,一般如果用于修修线上的一些紧急bug还是比较方便的,特别是苹果平台审核周期有时候会比较长。这种情况如果遇到线上的一些崩溃没有热修复,只能和苹果官方沟通来缩短审核的时间,紧急发布修复版本。

JSPatch 引擎初始化

+ (void)startEngine{

//1.判断是否存在 JSContext 类. ---> iOS 7.0 以下不支持 JavaScriptCore
if (![JSContext class] || _context) {
return;
}

//2.创建一个 JS 运行环境.
JSContext *context = [[JSContext alloc] init];

//3.为了使 JSPatch.js 可以访问 JPEngine 中定义的 C 函数,需为 context 注册 block.
//3.1 创建类.
context[@"_OC_defineClass"] = ^(NSString *classDeclaration, JSValue *instanceMethods, JSValue *classMethods) {
return defineClass(classDeclaration, instanceMethods, classMethods);
};

//3.2 类实现某协议.
context[@"_OC_defineProtocol"] = ^(NSString *protocolDeclaration, JSValue *instProtocol, JSValue *clsProtocol) {
return defineProtocol(protocolDeclaration, instProtocol,clsProtocol);
};

//3.3 js调用oc的实例方法.
context[@"_OC_callI"] = ^id(JSValue *obj, NSString *selectorName, JSValue *arguments, BOOL isSuper) {
return callSelector(nil, selectorName, arguments, obj, isSuper);
};

//3.4 js调用oc的类方法.
context[@"_OC_callC"] = ^id(NSString *className, NSString *selectorName, JSValue *arguments) {
return callSelector(className, selectorName, arguments, nil, NO);
};

//3.5 js 对象转 oc 对象.
context[@"_OC_formatJSToOC"] = ^id(JSValue *obj) {
return formatJSToOC(obj);
};

//3.6 oc 对象 转 js 对象.
context[@"_OC_formatOCToJS"] = ^id(JSValue *obj) {
return formatOCToJS([obj toObject]);
};

//3.7 获取对象的关联属性
context[@"_OC_getCustomProps"] = ^id(JSValue *obj) {
id realObj = formatJSToOC(obj);
return objc_getAssociatedObject(realObj, kPropAssociatedObjectKey);
};

// 3.8 给对象动态添加关联属性
context[@"_OC_setCustomProps"] = ^(JSValue *obj, JSValue *val) {
id realObj = formatJSToOC(obj);
objc_setAssociatedObject(realObj, kPropAssociatedObjectKey, val, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
};

//3.9 给 js 对象设置 weak.
context[@"__weak"] = ^id(JSValue *jsval) {
id obj = formatJSToOC(jsval);
return [[JSContext currentContext][@"_formatOCToJS"] callWithArguments:@[formatOCToJS([JPBoxing boxWeakObj:obj])]];
};

//3.10 给 js 对象设置 strong.
context[@"__strong"] = ^id(JSValue *jsval) {
id obj = formatJSToOC(jsval);
return [[JSContext currentContext][@"_formatOCToJS"] callWithArguments:@[formatOCToJS(obj)]];
};

//3.11 获取 oc 对象的父类.
context[@"_OC_superClsName"] = ^(NSString *clsName) {
Class cls = NSClassFromString(clsName);
return NSStringFromClass([cls superclass]);
};

//3.12 是否自动转换类型.
context[@"autoConvertOCType"] = ^(BOOL autoConvert) {
_autoConvert = autoConvert;
};

//3.13 oc number 转换为 string.
context[@"convertOCNumberToString"] = ^(BOOL convertOCNumberToString) {
_convertOCNumberToString = convertOCNumberToString;
};

//3.14 在JS中调用include方法,可以在一个JS文件中加载其他JS文件.
context[@"include"] = ^(NSString *filePath) {
NSString *absolutePath = [_scriptRootDir stringByAppendingPathComponent:filePath];
if (!_runnedScript) {
_runnedScript = [[NSMutableSet alloc] init];
}
if (absolutePath && ![_runnedScript containsObject:absolutePath]) {
[JPEngine _evaluateScriptWithPath:absolutePath];
[_runnedScript addObject:absolutePath];
}
};
//3.15 获取资源文件路径.
context[@"resourcePath"] = ^(NSString *filePath) {
return [_scriptRootDir stringByAppendingPathComponent:filePath];
};

//3.16 让 js 方法延迟执行.
context[@"dispatch_after"] = ^(double time, JSValue *func) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(time * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[func callWithArguments:nil];
});
};

//3.17 让js方法在 main queue dispatch async 执行.
context[@"dispatch_async_main"] = ^(JSValue *func) {
dispatch_async(dispatch_get_main_queue(), ^{
[func callWithArguments:nil];
});
};

// 3.18 让js方法在 main queue dispatch sync 执行.
context[@"dispatch_sync_main"] = ^(JSValue *func) {
if ([NSThread currentThread].isMainThread) {
[func callWithArguments:nil];
} else {
dispatch_sync(dispatch_get_main_queue(), ^{
[func callWithArguments:nil];
});
}
};

//3.19 让js方法在 global queue dispatch async 执行.
context[@"dispatch_async_global_queue"] = ^(JSValue *func) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[func callWithArguments:nil];
});
};

//3.20 释放js创建的oc对象.
context[@"releaseTmpObj"] = ^void(JSValue *jsVal) {
if ([[jsVal toObject] isKindOfClass:[NSDictionary class]]) {
void *pointer = [(JPBoxing *)([jsVal toObject][@"__obj"]) unboxPointer];
id obj = *((__unsafe_unretained id *)pointer);
@synchronized(_TMPMemoryPool) {
[_TMPMemoryPool removeObjectForKey:[NSNumber numberWithInteger:[(NSObject*)obj hash]]];
}
}
};

//3.21 js调用oc方法进行打印.
context[@"_OC_log"] = ^() {
NSArray *args = [JSContext currentArguments];
for (JSValue *jsVal in args) {
id obj = formatJSToOC(jsVal);
NSLog(@"JSPatch.log: %@", obj == _nilObj ? nil : (obj == _nullObj ? [NSNull null]: obj));
}
};

//3.22 将js捕捉到的异常交给oc方法处理.
context[@"_OC_catch"] = ^(JSValue *msg, JSValue *stack) {
_exceptionBlock([NSString stringWithFormat:@"js exception, \nmsg: %@, \nstack: \n %@", [msg toObject], [stack toObject]]);
};

//4. 注册 JSContext 执行出现异常时的回调.
context.exceptionHandler = ^(JSContext *con, JSValue *exception) {
NSLog(@"%@", exception);
_exceptionBlock([NSString stringWithFormat:@"js exception: %@", exception]);
};

//5. 创建OC中的null对象,转换成js的null对象,并设置到JSContext实例让js代码可以获取.
_nullObj = [[NSObject alloc] init];
context[@"_OC_null"] = formatOCToJS(_nullObj);

//6. 保存 context.
_context = context;

//7. oc 中的 nil 对象.
_nilObj = [[NSObject alloc] init];

//8. 同步锁.
_JSMethodSignatureLock = [[NSLock alloc] init];
_JSMethodForwardCallLock = [[NSRecursiveLock alloc] init];

//9. 在 JSPatch 中注册过的结构体定义(键:结构体名).
_registeredStruct = [[NSMutableDictionary alloc] init];
_currInvokeSuperClsName = [[NSMutableDictionary alloc] init];

//10. 注册内存警告通知.
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleMemoryWarning) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];

//11. 读取JSPatch.js,方便传入的js代码中使用JSPatch.js提供的函数.
NSString *path = [[NSBundle bundleForClass:[self class]] pathForResource:@"JSPatch" ofType:@"js"];
if (!path) _exceptionBlock(@"can't find JSPatch.js");
NSString *jsCore = [[NSString alloc] initWithData:[[NSFileManager defaultManager] contentsAtPath:path] encoding:NSUTF8StringEncoding];

//12. 加载 JSPatch.js 中的所有 js 代码到JSContext.
if ([_context respondsToSelector:@selector(evaluateScript:withSourceURL:)]) {
[_context evaluateScript:jsCore withSourceURL:[NSURL URLWithString:@"JSPatch.js"]];
} else {
[_context evaluateScript:jsCore];
}
}

由于JSPatch是基于JavaScriptCore的所以在进行JSPatch 引擎初始化的时候会先检查下当前系统中是否支持JavaScriptCore。iOS 7.0 以下不支持 JavaScriptCore 所以在iOS 7.0 以下调用startEngine不会继续执行,直接返回。否则会创建一个JSContext并开始往JSContext里面注册对应的block,这些block会在JSPatch.js 中被调用。然后会注册JSContext 执行出现异常时的回调.最后会加载 JSPatch.js 中的所有 js 代码到JSContext 运行。

在JSPatch.js中会向gloable环境下添加defineClass,require,defineProtocol,block,defineJSClass 这些方法,这些方法可以供我们在热修复js文件中调用。

我们再回到demo.js:

defineClass('JPTableViewController : UITableViewController <UIAlertViewDelegate>', ['data'], {
dataSource: function() {
var data = self.data();
if (data) return data;
var data = [];
for (var i = 0; i < 20; i ++) {
data.push("cell from js " + i);
}
self.setData(data)
return data;
},
numberOfSectionsInTableView: function(tableView) {
return 1;
},
tableView_numberOfRowsInSection: function(tableView, section) {
return self.dataSource().length;
},
tableView_cellForRowAtIndexPath: function(tableView, indexPath) {
var cell = tableView.dequeueReusableCellWithIdentifier("cell")
if (!cell) {
cell = require('UITableViewCell').alloc().initWithStyle_reuseIdentifier(0, "cell")
}
cell.textLabel().setText(self.dataSource()[indexPath.row()])
return cell
},
tableView_heightForRowAtIndexPath: function(tableView, indexPath) {
return 60
},
tableView_didSelectRowAtIndexPath: function(tableView, indexPath) {
var alertView = require('UIAlertView').alloc().initWithTitle_message_delegate_cancelButtonTitle_otherButtonTitles("Alert",self.dataSource()[indexPath.row()], self, "OK", null);
alertView.show()
},
alertView_willDismissWithButtonIndex: function(alertView, idx) {
console.log('click btn ' + alertView.buttonTitleAtIndex(idx).toJS())
}
})

上面的defineClass有三个参数,第一个参数是类定义的字符串,第二个[‘data’]代表的是一个类的属性列表,这里只有一个属性参数data。第三个参数是这个类的实例方法列表。

global.defineClass = function(declaration/*定义字符串*/, properties/*属性*/, instMethods/*实例方法*/, clsMethods/*类方法*/) {
var newInstMethods = {}/*用于存放新的实例方法*/, newClsMethods = {}/*用于存放新的类方法*/
if (!(properties instanceof Array)) {
clsMethods = instMethods
instMethods = properties
properties = null
}

if (properties) {
//为属性添加Getter/Setter方法
properties.forEach(function(name){
//在instMethods中添加属性的get方法
if (!instMethods[name]) {
instMethods[name] = _propertiesGetFun(name);
}
//在instMethods中添加属性的set方法
var nameOfSet = "set"+ name.substr(0,1).toUpperCase() + name.substr(1);
if (!instMethods[nameOfSet]) {
instMethods[nameOfSet] = _propertiesSetFun(name);
}
});
}
//取出实际类名
var realClsName = declaration.split(':')[0].trim()

//将实例方法和类方法 都转化为 key:方法名 [参数个数:方法实现]的形式 存放在newInstMethods,newClsMethods
_formatDefineMethods(instMethods, newInstMethods, realClsName)
_formatDefineMethods(clsMethods, newClsMethods, realClsName)

//调用OC的defineClass 进行定义
var ret = _OC_defineClass(declaration, newInstMethods, newClsMethods)
var className = ret['cls']
var superCls = ret['superCls']

_ocCls[className] = {
instMethods: {},
clsMethods: {},
}

if (superCls.length && _ocCls[superCls]) {
for (var funcName in _ocCls[superCls]['instMethods']) {
_ocCls[className]['instMethods'][funcName] = _ocCls[superCls]['instMethods'][funcName]
}
for (var funcName in _ocCls[superCls]['clsMethods']) {
_ocCls[className]['clsMethods'][funcName] = _ocCls[superCls]['clsMethods'][funcName]
}
}
//将自定义的js热修复代码添加到js环境
_setupJSMethod(className, instMethods, 1, realClsName)
_setupJSMethod(className, clsMethods, 0, realClsName)

return require(className)
}

JSPatch.js 的defineClass方法中会调用 JSEngine startEngine方法中向JSContext注入的_OC_defineClass block。传入的是这个类的定义,以及实例方法,类方法。其中实例方法和类方法的结构如下:

key:方法名 [参数个数:方法实现]

我们先来看下_OC_defineClass再回过来看JSPatch.js

context[@"_OC_defineClass"] = ^(NSString *classDeclaration, JSValue *instanceMethods, JSValue *classMethods) {
return defineClass(classDeclaration, instanceMethods, classMethods);
};
static NSDictionary *defineClass(NSString *classDeclaration, JSValue *instanceMethods, JSValue *classMethods) {
//================================根据JSj脚本实例化出相对应的类====================================================
//1.使用 NSScanner 分离 classDeclaration.
NSScanner *scanner = [NSScanner scannerWithString:classDeclaration];

NSString *className; //类名
NSString *superClassName; //父类名
NSString *protocolNames; //实现的协议名
[scanner scanUpToString:@":" intoString:&className];
if (!scanner.isAtEnd) {
scanner.scanLocation = scanner.scanLocation + 1;
[scanner scanUpToString:@"<" intoString:&superClassName];
if (!scanner.isAtEnd) {
scanner.scanLocation = scanner.scanLocation + 1;
[scanner scanUpToString:@">" intoString:&protocolNames];
}
}

if (!superClassName) superClassName = @"NSObject";
//类名
className = trim(className);
//父类名
superClassName = trim(superClassName);
//取出协议
NSArray *protocols = [protocolNames length] ? [protocolNames componentsSeparatedByString:@","] : nil;

//实例化对应的类
Class cls = NSClassFromString(className);
if (!cls) {
Class superCls = NSClassFromString(superClassName);
if (!superCls) {
_exceptionBlock([NSString stringWithFormat:@"can't find the super class %@", superClassName]);
return @{@"cls": className};
}
cls = objc_allocateClassPair(superCls, className.UTF8String, 0);
objc_registerClassPair(cls);
}

//为类添加协议
if (protocols.count > 0) {
for (NSString* protocolName in protocols) {
Protocol *protocol = objc_getProtocol([trim(protocolName) cStringUsingEncoding:NSUTF8StringEncoding]);
class_addProtocol (cls, protocol);
}
}
//====================================================================================
for (int i = 0; i < 2; i ++) {
BOOL isInstance = i == 0;
JSValue *jsMethods = isInstance ? instanceMethods: classMethods;
//3.若是添加实例方法,直接使用Class对象;
//若是添加类方法,需要获取元类.

Class currCls = isInstance ? cls: objc_getMetaClass(className.UTF8String);
//把js对象转换成OC的字典,从而可以取到方法名、参数个数、具体实现.
NSDictionary *methodDict = [jsMethods toDictionary];
for (NSString *jsMethodName in methodDict.allKeys) {
//方法名为键,一个数组为值。数组第一个元素为对应实现函数的参数个数,第二个元素是方法的具体实现。
JSValue *jsMethodArr = [jsMethods valueForProperty:jsMethodName];
//第一个值为 参数个数
int numberOfArg = [jsMethodArr[0] toInt32];
//将方法名转换为selectorName
NSString *selectorName = convertJPSelectorString(jsMethodName);

if ([selectorName componentsSeparatedByString:@":"].count - 1 < numberOfArg) {
selectorName = [selectorName stringByAppendingString:@":"];
}

//第二个值为 方法实现
JSValue *jsMethod = jsMethodArr[1];
if (class_respondsToSelector(currCls, NSSelectorFromString(selectorName))) {
// 4.如果要替换的类已经定义了该方法,直接对该方法替换和实现消息转发.
overrideMethod(currCls, selectorName, jsMethod, !isInstance, NULL);
} else {
BOOL overrided = NO;
for (NSString *protocolName in protocols) {
char *types = methodTypesInProtocol(protocolName, selectorName, isInstance, YES);
if (!types) types = methodTypesInProtocol(protocolName, selectorName, isInstance, NO);
if (types) {
overrideMethod(currCls, selectorName, jsMethod, !isInstance, types);
free(types);
overrided = YES;
break;
}
}
if (!overrided) {
//5.2 上述两种情况都不满足.js端请求添加一个新的方法.
if (![[jsMethodName substringToIndex:1] isEqualToString:@"_"]) {
//方法名的处理:_改为:
NSMutableString *typeDescStr = [@"@@:" mutableCopy];
for (int i = 0; i < numberOfArg; i ++) {
[typeDescStr appendString:@"@"];
}
//构造一个typeDescription为"@@:\@*"的IMP.将这个IMP添加到类中.
overrideMethod(currCls, selectorName, jsMethod, !isInstance, [typeDescStr cStringUsingEncoding:NSUTF8StringEncoding]);
}
}
}
}
}
// 6.为该类添加两个方法,使js脚本拥有设置property的方法.
class_addMethod(cls, @selector(getProp:), (IMP)getPropIMP, "@@:@");
class_addMethod(cls, @selector(setProp:forKey:), (IMP)setPropIMP, "v@:@@");

//// 7.返回字典给js脚本
return @{@"cls": className, @"superCls": superClassName};
}

我们以demo.js为例子,defineClass这里传入的classDeclaration为 JPTableViewController : UITableViewController 这个字符串。在defineClass中会从中提取类信息,父类信息,以及协议信息。
第二个参数instanceMethods和第三个参数classMethods分别为包含属性Setter/Getter在内的实例方法和类方法。上面提到了这两个参数在js方法中已经将它组合成

key:方法名 [参数个数:方法实现]

这种形式,我们以

tableView_numberOfRowsInSection: function(tableView, section) {
return self.dataSource().length;
},

为例子: key为 tableView_numberOfRowsInSection value为[2,tableView:numberOfRowsInSection:的实现] 我们知道OC中要构建一个selector需要知道这个方法是实例方法还是类方法,方法名,IMP
类名,IMP 都可以直接拿到,我们还缺一个关键的信息,就是selectorName. 我们从js带过来带的key为tableView_numberOfRowsInSection,它其实包含了selectorName所需要的必要信息,只不过它是以”“分隔,所以将将”“替换为”:”
然后检查通过”:”分隔的字符串数和传入的参数个数这个进行进行比较,如果少了则需要在最后加个”:”就可以拿到我们需要的selectorName了。

我们接下来看下热修复最关键的方法overrideMethod:

方法替换

/**
* 使用jsvalue中将要替换的方法实现来替换oc类中的方法实现
*
* @param cls 被替换的类
* @param selectorName 被替换实现的SEL
* @param function 在js中定义的将要替换的新的实现
* @param isClassMethod 是否类方法(如果是-->寻找MetaClass)
* @param typeDescription 被替换的实现方法的编码
*/
static void overrideMethod(Class cls, NSString *selectorName, JSValue *function, BOOL isClassMethod, const char *typeDescription) {
//1. 要重写的方法的SEL.
SEL selector = NSSelectorFromString(selectorName);

//2. 获取重写方法的具体实现函数的格式编码.
if (!typeDescription) {
Method method = class_getInstanceMethod(cls, selector);
typeDescription = (char *)method_getTypeEncoding(method);
}

//3.获取 class 中被重写 SEL 对应的原始IMP.
IMP originalImp = class_respondsToSelector(cls, selector) ? class_getMethodImplementation(cls, selector) : NULL;

//4.准备进入消息转发处理的系统函数实现IMP.
IMP msgForwardIMP = _objc_msgForward;
//.......

//--------------------------------------将要修复类原来的forwardInvocaiton替换成ORIGforwardInvocation--------------------------
//--------------------------------------将要修复类的SEL替换成ORIGsel添加到类---------------------------------------------------
//6.将cls中原来 forwardInvocaiton: 的实现替换成 JPForwardInvocation:函数实现.
//class_replaceMethod()返回的是替换之前的 IMP.
if (class_getMethodImplementation(cls, @selector(forwardInvocation:)) != (IMP)JPForwardInvocation) {
IMP originalForwardImp = class_replaceMethod(cls, @selector(forwardInvocation:), (IMP)JPForwardInvocation, "v@:@");
if (originalForwardImp) {
//7.为cls添加新的SEL(ORIGforwardInvocation:),指向原始 forwardInvocation: 的实现IMP.
class_addMethod(cls, @selector(ORIGforwardInvocation:), originalForwardImp, "v@:@");
}
}

[cls jp_fixMethodSignature];
if (class_respondsToSelector(cls, selector)) {
NSString *originalSelectorName = [NSString stringWithFormat:@"ORIG%@", selectorName];
SEL originalSelector = NSSelectorFromString(originalSelectorName);
if(!class_respondsToSelector(cls, originalSelector)) {
//8.为cls添加新的SEL(ORIG...:)指向被替换方法的原始实现IMP.
class_addMethod(cls, originalSelector, originalImp, typeDescription);
}
}

//9.构造替换实现后的新SEL:(JP...)
NSString *JPSelectorName = [NSString stringWithFormat:@"_JP%@", selectorName];

//10.记录新SEL对应js传过来的待替换目标方法的实现.
_initJPOverideMethods(cls);
_JSOverideMethods[cls][JPSelectorName] = function;

//11.替换原SEL的实现IMP为msgForwardIMP
//让被替换的方法调用时,直接进入“消息转发”流程(_objc_msgForward 或 _objc_msgForward_stret)
//这一步放到最后是为了避免在 overrideMethod 过程中调用原sel导致的线程问题.

// Replace the original selector at last, preventing threading issus when
// the selector get called during the execution of `overrideMethod`
class_replaceMethod(cls, selector, msgForwardIMP, typeDescription);
}

如果大家看过Aspect源码这部分会比较好理解,两者在这部分思路是类似的,首先会将当前类的forwardInvocaiton替换为JSPatch实现的JPForwardInvocation:如果原来的类已经有实现了forwardInvocaiton那么就会将forwardInvocaiton替换成ORIGforwardInvocation
当前方法的selectorName 替换为_JPselectorName,如果原来有这个方法了,这里是在覆盖原有的方法,那么会将原来的方法selectorName改为ORIGselectorName。最后将selector方法的实现替换为_objc_msgForward 或 _objc_msgForward_stret 从而在代码中调用这个方法的时候,会触发消息下发。
走forwardInvocaiton,也就是会走到JSPatch实现的JPForwardInvocation:来重新控制方法调用的规则。

static void JPForwardInvocation(__unsafe_unretained id assignSlf, SEL selector, NSInvocation *invocation) {

//1.表示oc对象是否已经被释放
BOOL deallocFlag = NO;
id slf = assignSlf;
BOOL isBlock = [[assignSlf class] isSubclassOfClass : NSClassFromString(@"NSBlock")];

//2.获取invocation中参数的数量
NSMethodSignature *methodSignature = [invocation methodSignature];
NSInteger numberOfArguments = [methodSignature numberOfArguments];

//3.转化调用的SEL为JPSEL(这是JSPatch中缓存JSValue* function的key格式)
NSString *selectorName = isBlock ? @"" : NSStringFromSelector(invocation.selector);
NSString *JPSelectorName = [NSString stringWithFormat:@"_JP%@", selectorName];

//4.判断JPSEL是否有对应的js函数的实现,如果没有就走原始方法的消息转发的流程.
JSValue *jsFunc = isBlock ? objc_getAssociatedObject(assignSlf, "_JSValue")[@"cb"] : getJSFunctionInObjectHierachy(slf, JPSelectorName);
if (!jsFunc) {
JPExecuteORIGForwardInvocation(slf, selector, invocation);
return;
}
//5.1 初始化数组,存储NSInvacation中获取的参数列表,传给对应的js函数
NSMutableArray *argList = [[NSMutableArray alloc] init];
if (!isBlock) {
if ([slf class] == slf) {
//5.2 类方法:设置__clsName标识表明这是一个类对象
[argList addObject:[JSValue valueWithObject:@{@"__clsName": NSStringFromClass([slf class])} inContext:_context]];
} else if ([selectorName isEqualToString:@"dealloc"]) {
//5.3 要被释放的对象:使用assign来保存self指针
[argList addObject:[JPBoxing boxAssignObj:slf]];
deallocFlag = YES;
} else {
//5.4 使用 weak 保存self 指针
[argList addObject:[JPBoxing boxWeakObj:slf]];
}
}

//5.5 NSInvocation 对象的前两个参数是self和_cmd,所以直接从第3个参数开始获取
for (NSUInteger i = isBlock ? 1 : 2; i < numberOfArguments; i++) {
const char *argumentType = [methodSignature getArgumentTypeAtIndex:i];
//返回值如果是const,获取encoding来判断类型.
switch(argumentType[0] == 'r' ? argumentType[1] : argumentType[0]) {
//从invocation中获取参数,添加到argList中.
#define JP_FWD_ARG_CASE(_typeChar, _type) \
case _typeChar: { \
_type arg; \
[invocation getArgument:&arg atIndex:i]; \
[argList addObject:@(arg)]; \
break; \
}
JP_FWD_ARG_CASE('c', char)
JP_FWD_ARG_CASE('C', unsigned char)
JP_FWD_ARG_CASE('s', short)
JP_FWD_ARG_CASE('S', unsigned short)
JP_FWD_ARG_CASE('i', int)
JP_FWD_ARG_CASE('I', unsigned int)
JP_FWD_ARG_CASE('l', long)
JP_FWD_ARG_CASE('L', unsigned long)
JP_FWD_ARG_CASE('q', long long)
JP_FWD_ARG_CASE('Q', unsigned long long)
JP_FWD_ARG_CASE('f', float)
JP_FWD_ARG_CASE('d', double)
JP_FWD_ARG_CASE('B', BOOL)
case '@': {
//id类型参数使用__unsafe__unretained
__unsafe_unretained id arg;
[invocation getArgument:&arg atIndex:i];
//block参数使用copy,_nilObj表示nil
if ([arg isKindOfClass:NSClassFromString(@"NSBlock")]) {
[argList addObject:(arg ? [arg copy]: _nilObj)];
} else {
[argList addObject:(arg ? arg: _nilObj)];
}
break;
}
case '{': {
// 处理结构体类型参数
// 获取结构体类型名称,把参数包装成JSValue类型
NSString *typeString = extractStructName([NSString stringWithUTF8String:argumentType]);
#define JP_FWD_ARG_STRUCT(_type, _transFunc) \
if ([typeString rangeOfString:@#_type].location != NSNotFound) { \
_type arg; \
[invocation getArgument:&arg atIndex:i]; \
[argList addObject:[JSValue _transFunc:arg inContext:_context]]; \
break; \
}
JP_FWD_ARG_STRUCT(CGRect, valueWithRect)
JP_FWD_ARG_STRUCT(CGPoint, valueWithPoint)
JP_FWD_ARG_STRUCT(CGSize, valueWithSize)
JP_FWD_ARG_STRUCT(NSRange, valueWithRange)
// 自定义类型的结构体处理
@synchronized (_context) {
NSDictionary *structDefine = _registeredStruct[typeString];
if (structDefine) {
size_t size = sizeOfStructTypes(structDefine[@"types"]);
if (size) {
void *ret = malloc(size);
[invocation getArgument:ret atIndex:i];
NSDictionary *dict = getDictOfStruct(ret, structDefine);
[argList addObject:[JSValue valueWithObject:dict inContext:_context]];
free(ret);
break;
}
}
}

break;
}
case ':': {
//selector类型处理
SEL selector;
[invocation getArgument:&selector atIndex:i];
NSString *selectorName = NSStringFromSelector(selector);
[argList addObject:(selectorName ? selectorName: _nilObj)];
break;
}
case '^':
case '*': {
//指针类型处理
void *arg;
[invocation getArgument:&arg atIndex:i];
[argList addObject:[JPBoxing boxPointer:arg]];
break;
}
case '#': {
//Class类型
Class arg;
[invocation getArgument:&arg atIndex:i];
[argList addObject:[JPBoxing boxClass:arg]];
break;
}
default: {
NSLog(@"error type %s", argumentType);
break;
}
}
}

if (_currInvokeSuperClsName[selectorName]) {
Class cls = NSClassFromString(_currInvokeSuperClsName[selectorName]);
NSString *tmpSelectorName = [[selectorName stringByReplacingOccurrencesOfString:@"_JPSUPER_" withString:@"_JP"] stringByReplacingOccurrencesOfString:@"SUPER_" withString:@"_JP"];
if (!_JSOverideMethods[cls][tmpSelectorName]) {
NSString *ORIGSelectorName = [selectorName stringByReplacingOccurrencesOfString:@"SUPER_" withString:@"ORIG"];
[argList removeObjectAtIndex:0];
id retObj = callSelector(_currInvokeSuperClsName[selectorName], ORIGSelectorName, [JSValue valueWithObject:argList inContext:_context], [JSValue valueWithObject:@{@"__obj": slf, @"__realClsName": @""} inContext:_context], NO);
id __autoreleasing ret = formatJSToOC([JSValue valueWithObject:retObj inContext:_context]);
[invocation setReturnValue:&ret];
return;
}
}
//6.将上面获得的参数列表数组转化为对应的js对象数组
NSArray *params = _formatOCToJSList(argList);
char returnType[255];
strcpy(returnType, [methodSignature methodReturnType]);

// 7.获取返回值类型
// Restore the return type
if (strcmp(returnType, @encode(JPDouble)) == 0) {
strcpy(returnType, @encode(double));
}
if (strcmp(returnType, @encode(JPFloat)) == 0) {
strcpy(returnType, @encode(float));
}

//7.1 返回值是否为const,如果是,获取后面的encoding来判断类型
switch (returnType[0] == 'r' ? returnType[1] : returnType[0]) {
#define JP_FWD_RET_CALL_JS \
JSValue *jsval; \
[_JSMethodForwardCallLock lock]; \
jsval = [jsFunc callWithArguments:params]; \
[_JSMethodForwardCallLock unlock]; \
while (![jsval isNull] && ![jsval isUndefined] && [jsval hasProperty:@"__isPerformInOC"]) { \
NSArray *args = nil; \
JSValue *cb = jsval[@"cb"]; \
if ([jsval hasProperty:@"sel"]) { \
id callRet = callSelector(![jsval[@"clsName"] isUndefined] ? [jsval[@"clsName"] toString] : nil, [jsval[@"sel"] toString], jsval[@"args"], ![jsval[@"obj"] isUndefined] ? jsval[@"obj"] : nil, NO); \
args = @[[_context[@"_formatOCToJS"] callWithArguments:callRet ? @[callRet] : _formatOCToJSList(@[_nilObj])]]; \
} \
[_JSMethodForwardCallLock lock]; \
jsval = [cb callWithArguments:args]; \
[_JSMethodForwardCallLock unlock]; \
}

#define JP_FWD_RET_CASE_RET(_typeChar, _type, _retCode) \
case _typeChar : { \
JP_FWD_RET_CALL_JS \
_retCode \
[invocation setReturnValue:&ret];\
break; \
}

#define JP_FWD_RET_CASE(_typeChar, _type, _typeSelector) \
JP_FWD_RET_CASE_RET(_typeChar, _type, _type ret = [[jsval toObject] _typeSelector];) \

#define JP_FWD_RET_CODE_ID \
id __autoreleasing ret = formatJSToOC(jsval); \
if (ret == _nilObj || \
([ret isKindOfClass:[NSNumber class]] && strcmp([ret objCType], "c") == 0 && ![ret boolValue])) ret = nil; \

#define JP_FWD_RET_CODE_POINTER \
void *ret; \
id obj = formatJSToOC(jsval); \
if ([obj isKindOfClass:[JPBoxing class]]) { \
ret = [((JPBoxing *)obj) unboxPointer]; \
}

#define JP_FWD_RET_CODE_CLASS \
Class ret; \
ret = formatJSToOC(jsval);


#define JP_FWD_RET_CODE_SEL \
SEL ret; \
id obj = formatJSToOC(jsval); \
if ([obj isKindOfClass:[NSString class]]) { \
ret = NSSelectorFromString(obj); \
}

JP_FWD_RET_CASE_RET('@', id, JP_FWD_RET_CODE_ID)
JP_FWD_RET_CASE_RET('^', void*, JP_FWD_RET_CODE_POINTER)
JP_FWD_RET_CASE_RET('*', void*, JP_FWD_RET_CODE_POINTER)
JP_FWD_RET_CASE_RET('#', Class, JP_FWD_RET_CODE_CLASS)
JP_FWD_RET_CASE_RET(':', SEL, JP_FWD_RET_CODE_SEL)

JP_FWD_RET_CASE('c', char, charValue)
JP_FWD_RET_CASE('C', unsigned char, unsignedCharValue)
JP_FWD_RET_CASE('s', short, shortValue)
JP_FWD_RET_CASE('S', unsigned short, unsignedShortValue)
JP_FWD_RET_CASE('i', int, intValue)
JP_FWD_RET_CASE('I', unsigned int, unsignedIntValue)
JP_FWD_RET_CASE('l', long, longValue)
JP_FWD_RET_CASE('L', unsigned long, unsignedLongValue)
JP_FWD_RET_CASE('q', long long, longLongValue)
JP_FWD_RET_CASE('Q', unsigned long long, unsignedLongLongValue)
JP_FWD_RET_CASE('f', float, floatValue)
JP_FWD_RET_CASE('d', double, doubleValue)
JP_FWD_RET_CASE('B', BOOL, boolValue)

case 'v': {
JP_FWD_RET_CALL_JS
break;
}

case '{': {
NSString *typeString = extractStructName([NSString stringWithUTF8String:returnType]);
#define JP_FWD_RET_STRUCT(_type, _funcSuffix) \
if ([typeString rangeOfString:@#_type].location != NSNotFound) { \
JP_FWD_RET_CALL_JS \
_type ret = [jsval _funcSuffix]; \
[invocation setReturnValue:&ret];\
break; \
}
JP_FWD_RET_STRUCT(CGRect, toRect)
JP_FWD_RET_STRUCT(CGPoint, toPoint)
JP_FWD_RET_STRUCT(CGSize, toSize)
JP_FWD_RET_STRUCT(NSRange, toRange)

@synchronized (_context) {
NSDictionary *structDefine = _registeredStruct[typeString];
if (structDefine) {
size_t size = sizeOfStructTypes(structDefine[@"types"]);
JP_FWD_RET_CALL_JS
void *ret = malloc(size);
NSDictionary *dict = formatJSToOC(jsval);
getStructDataWithDict(ret, dict, structDefine);
[invocation setReturnValue:ret];
free(ret);
}
}
break;
}
default: {
break;
}
}

if (_pointersToRelease) {
for (NSValue *val in _pointersToRelease) {
void *pointer = NULL;
[val getValue:&pointer];
CFRelease(pointer);
}
_pointersToRelease = nil;
}
//8.待替换的方法是 delloc 需要特殊处理:
if (deallocFlag) {
slf = nil;
Class instClass = object_getClass(assignSlf);
Method deallocMethod = class_getInstanceMethod(instClass, NSSelectorFromString(@"ORIGdealloc"));
//获取原delloc imp 指针,调用delloc,防止内存泄漏.
void (*originalDealloc)(__unsafe_unretained id, SEL) = (__typeof__(originalDealloc))method_getImplementation(deallocMethod);
originalDealloc(assignSlf, NSSelectorFromString(@"dealloc"));
}
}

JPForwardInvocation 会先从 NSInvocation中取出selector 名称,现在selector名称之前加上_JP,然后看下当前对象是否有对应的方法,我们在overideMethod方法中了解到,如果我们热修复js脚本中有对应的方法,那么就会在该对象中多出一个_JP开头的方法,指向热修复补丁中的实现。
所以这里会先看下当前方法中是否有对应的补丁方法,如果没有就调用JPExecuteORIGForwardInvocation走原来的消息分发机制。

static void JPExecuteORIGForwardInvocation(id slf, SEL selector, NSInvocation *invocation) {
SEL origForwardSelector = @selector(ORIGforwardInvocation:);

if ([slf respondsToSelector:origForwardSelector]) {
NSMethodSignature *methodSignature = [slf methodSignatureForSelector:origForwardSelector];
if (!methodSignature) {
_exceptionBlock([NSString stringWithFormat:@"unrecognized selector -ORIGforwardInvocation: for instance %@", slf]);
return;
}
NSInvocation *forwardInv= [NSInvocation invocationWithMethodSignature:methodSignature];
[forwardInv setTarget:slf];
[forwardInv setSelector:origForwardSelector];
[forwardInv setArgument:&invocation atIndex:2];
[forwardInv invoke];
} else {
Class superCls = [[slf class] superclass];
Method superForwardMethod = class_getInstanceMethod(superCls, @selector(forwardInvocation:));
void (*superForwardIMP)(id, SEL, NSInvocation *);
superForwardIMP = (void (*)(id, SEL, NSInvocation *))method_getImplementation(superForwardMethod);
superForwardIMP(slf, @selector(forwardInvocation:), invocation);
}
}

如果有的话,会把NSInvocation中的参数提取出来,传递给对应的补丁方法。然后通过JP_FWD_RET_CALL_JS调用js方法。通过JP_FWD_RET_CALL_JS中的

jsval = [cb callWithArguments:args]

获取到返回值后设置到NSInvocation中,完成方法的替换。

比如我们上面介绍的例子中有一个handleBtn. 原来的方法实现是空实现,后面通过JSPatch的demo.js 覆盖了这个方法,我们梳理下这是怎么实现的:

由于我们原有对象已经有了这个方法,只不过这个方法是空的,所以在overideMethod方法中会将原来的方法的selector名称改为_ORIGHandleBtn,指向原来的实现:

- (void)handleBtn:(id)sender {

}

而会在该类中添加一个_JPhandleBtn,它的实现指向补丁js中的实现。
并且由于该对象没有实现forwardInvocaiton 所以会为该对象新增加一个JPForwardInvocation,并且将原来的对handleBtn的调用转换为_objc_msgForward。这一切工作完成后我们来看下整个调用过程:
我们点击按钮后会触发handleBtn,这时候由于handleBtn的实现被替换为_objc_msgForward所以会走消息分发途径,进而走到forwardInvocaiton,由于forwardInvocaiton的实现在上面被替换为JPForwardInvocation
在JPForwardInvocation中会先尝试将消息转发到_JPhandleBtn看下是否有这个实现,这里由于有这个实现所以就直接将invocation参数提取出来,传递给对应的js方法执行。这就达到了整个方法替换的目的。

如何在JS环境下执行OC方法

我们上面了解到了怎么通过消息转发来达到热修复的目的,接下来我们还有一个问题就是如何在JS中执行OC方法,我们看下_evaluateScript,它的作用是将补丁js方法加载到JSContext中执行。我们还是以demo.js为例子

+ (JSValue *)_evaluateScript:(NSString *)script withSourceURL:(NSURL *)resourceURL {
//1. script 不存在或当前 iOS 版本低于 7.0 退出.
if (!script || ![JSContext class]) {
_exceptionBlock(@"script is nil");
return nil;
}
//在执行脚本之前会自动调用startEngine
[self startEngine];

//2. 正则式构建 (?<!\\\\)\\.\\s*(\\w+)\\s*\\(
if (!_regex) {
_regex = [NSRegularExpression regularExpressionWithPattern:_regexStr options:0 error:nil];
}
NSString *formatedScript = [NSString stringWithFormat:@";(function(){try{\n%@\n}catch(e){_OC_catch(e.message, e.stack)}})();", [_regex stringByReplacingMatchesInString:script options:0 range:NSMakeRange(0, script.length) withTemplate:_replaceStr]];
//4.将正则处理后的js代码加载到 context 执行.(进入 JavaScriptCore)
@try {
//转换 JPTableViewController.alloc().init() 为 JPTableViewController.__c("alloc")().__c("init")()
if ([_context respondsToSelector:@selector(evaluateScript:withSourceURL:)]) {
return [_context evaluateScript:formatedScript withSourceURL:resourceURL];
} else {
return [_context evaluateScript:formatedScript];
}
}
@catch (NSException *exception) {
_exceptionBlock([NSString stringWithFormat:@"%@", exception]);
}
return nil;
}

demo.js加载后会先通过正则匹配处理后再送到JSContext中,我们看下匹配替换后的formatedScript是什么样的:

;(function(){try{
defineClass('JPViewController', {
handleBtn: function(sender) {
var tableViewCtrl = JPTableViewController.__c("alloc")().__c("init")()
self.__c("navigationController")().__c("pushViewController_animated")(tableViewCtrl, YES)
}
})

defineClass('JPTableViewController : UITableViewController <UIAlertViewDelegate>', ['data'], {
dataSource: function() {
var data = self.__c("data")();
if (data) return data;
var data = [];
for (var i = 0; i < 20; i ++) {
data.__c("push")("cell from js " + i);
}
self.__c("setData")(data)
return data;
},
numberOfSectionsInTableView: function(tableView) {
return 1;
},
tableView_numberOfRowsInSection: function(tableView, section) {
return self.__c("dataSource")().length;
},
tableView_cellForRowAtIndexPath: function(tableView, indexPath) {
var cell = tableView.__c("dequeueReusableCellWithIdentifier")("cell")
if (!cell) {
cell = require('UITableViewCell').__c("alloc")().__c("initWithStyle_reuseIdentifier")(0, "cell")
}
cell.__c("textLabel")().__c("setText")(self.__c("dataSource")()[indexPath.__c("row")()])
return cell
},
tableView_heightForRowAtIndexPath: function(tableView, indexPath) {
return 60
},
tableView_didSelectRowAtIndexPath: function(tableView, indexPath) {
var alertView = require('UIAlertView').__c("alloc")().__c("initWithTitle_message_delegate_cancelButtonTitle_otherButtonTitles")("Alert",self.__c("dataSource")()[indexPath.__c("row")()], self, "OK", null);
alertView.__c("show")()
},
alertView_willDismissWithButtonIndex: function(alertView, idx) {
console.__c("log")('click btn ' + alertView.__c("buttonTitleAtIndex")(idx).__c("toJS")())
}
})
}catch(e){_OC_catch(e.message, e.stack)}})();

大家可以发现所有的xxx.XXXX 都被替换成了xxx.__c(“XXXX),为什么需要这样转换呢?我们到JSPatch.js寻找这个答案:

__c: function(methodName) {
var slf = this

if (slf instanceof Boolean) {
return function() {
return false
}
}
if (slf[methodName]) {
return slf[methodName].bind(slf);
}

if (!slf.__obj && !slf.__clsName) {
throw new Error(slf + '.' + methodName + ' is undefined')
}
if (slf.__isSuper && slf.__clsName) {
slf.__clsName = _OC_superClsName(slf.__obj.__realClsName ? slf.__obj.__realClsName: slf.__clsName);
}
var clsName = slf.__clsName
if (clsName && _ocCls[clsName]) {
var methodType = slf.__obj ? 'instMethods': 'clsMethods'
if (_ocCls[clsName][methodType][methodName]) {
slf.__isSuper = 0;
return _ocCls[clsName][methodType][methodName].bind(slf)
}
}

return function(){
var args = Array.prototype.slice.call(arguments)
//_methodFunc() 把相关信息传给OC,OC用 Runtime 接口调用相应方法,返回结果值,这个调用就结束了。
return _methodFunc(slf.__obj, slf.__clsName, methodName, args, slf.__isSuper)
}
},


* instance: 对象
* clsName: 类名
* methodName: 方法名
* args: 参数列表
* isSuper: 是否调用super父类的方法
* isPerformSelector:是否用performSelector方式调用
*/
var _methodFunc = function(instance, clsName, methodName, args, isSuper, isPerformSelector) {
var selectorName = methodName
if (!isPerformSelector) {
//处理得到OC中的方法SEL,参数
methodName = methodName.replace(/__/g, "-")
selectorName = methodName.replace(/_/g, ":").replace(/-/g, "_")
var marchArr = selectorName.match(/:/g)
var numOfArgs = marchArr ? marchArr.length : 0
if (args.length > numOfArgs) {
selectorName += ":"
}
}
//当前是否是一个实例,如果是实例调用_OC_callI否则调用_OC_callC
var ret = instance ? _OC_callI(instance, selectorName, args, isSuper):
_OC_callC(clsName, selectorName, args)
//获取OC方法执行完毕的返回值,并转化成JS对象
return _formatOCToJS(ret)
}

__c 里面会调用_methodFunc,_methodFunc方法里面会根据当前方法是实例方法还是类方法决定调用_OC_callI还是_OC_callC。最后将oc中的返回值转换为JS对象返回。
_OC_callI还是_OC_callC是在我们初始化JSPatch引擎的时候注入到JS中的。在js中调用_OC_callI或者_OC_callC会触发调用OC层的callSelector方法,通过它将js中的补丁方法中oc相关的方法交给oc层。

/**
* 完成oc中的方法调用
*
* @param className 类名(nil --> 表示实例方法)
* @param selectorName 方法SEL值
* @param arguments 方法执行参数
* @param instance 对象(js对象中的变量,如: var UIAlertView = { __clsName : 'UIAlertView'})
* @param isSuper 是否调用的是父类方法
*
* @return 方法执行后的结果值,返回给js代码中.
*/

#pragma mark -
static id callSelector(NSString *className, NSString *selectorName, JSValue *arguments, JSValue *instance, BOOL isSuper)
{
NSString *realClsName = [[instance valueForProperty:@"__realClsName"] toString];

///[1] 实例变量处理
if (instance) {
//1.将js封装的instance对象进行拆装,得到oc对象.
instance = formatJSToOC(instance);
if (class_isMetaClass(object_getClass(instance))) {
//如果调用的是类方法则获取到类名
className = NSStringFromClass((Class)instance);
instance = nil;
} else if (!instance || instance == _nilObj || [instance isKindOfClass:[JPBoxing class]]) {
//如果不是类方法,但是传入的instance 是空的则返回 nil
return @{@"__isNil": @(YES)};
}
}

///[2] 参数处理
//2.将js封装的参数列表转为oc类型.
id argumentsObj = formatJSToOC(arguments);

///[2] 解包处理
if (instance && [selectorName isEqualToString:@"toJS"]) {
// 3.如果要执行的方法是"toJS",即转化为js类型,对于NSString/NSDictory/NSArray/NSData需进行特殊处理
// 因为JSPatch中需使用JPBoxing包装OC中的上述对象,防止JavaScriptCore.framework转换类型.
if ([instance isKindOfClass:[NSString class]] || [instance isKindOfClass:[NSDictionary class]] || [instance isKindOfClass:[NSArray class]] || [instance isKindOfClass:[NSDate class]]) {
return _unboxOCObjectToJS(instance);
}
}
///[3] 获取到Class对象
//4.根据类名与selectorName获得对应的类对象与selector
Class cls = instance ? [instance class] : NSClassFromString(className);

///[3] 获取到SEL对象
SEL selector = NSSelectorFromString(selectorName);

NSString *superClassName = nil;
//5.判断是否调用的是父类的方法,如果是,走父类的方法实现
if (isSuper) {
//5.1 定义新的SEL:SUPERSEL

///[4] 构建出SUPER_seletcorName
NSString *superSelectorName = [NSString stringWithFormat:@"SUPER_%@", selectorName];
SEL superSelector = NSSelectorFromString(superSelectorName);

///[5] 构建出superCls
Class superCls;
if (realClsName.length) {
Class defineClass = NSClassFromString(realClsName);
superCls = defineClass ? [defineClass superclass] : [cls superclass];
} else {
superCls = [cls superclass];
}

Method superMethod = class_getInstanceMethod(superCls, selector);
IMP superIMP = method_getImplementation(superMethod);

// 5.2 将SUPERSEL指向superIMP的实现
///[6] 添加superSelector方法
class_addMethod(cls, superSelector, superIMP, method_getTypeEncoding(superMethod));

//5.3 查找父类中是否有添加JPSEL的实现
NSString *JPSelectorName = [NSString stringWithFormat:@"_JP%@", selectorName];
JSValue *overideFunction = _JSOverideMethods[superCls][JPSelectorName];
if (overideFunction) {
// 如果有,进行imp替换
overrideMethod(cls, superSelectorName, overideFunction, NO, NULL);
}

selector = superSelector;
superClassName = NSStringFromClass(superCls);
}


NSMutableArray *_markArray;

//6.通过类对象与selector构造对应的NSMethodSignature签名
NSInvocation *invocation;
NSMethodSignature *methodSignature;
if (!_JSMethodSignatureCache) {
_JSMethodSignatureCache = [[NSMutableDictionary alloc]init];
}
if (instance) {
[_JSMethodSignatureLock lock];
if (!_JSMethodSignatureCache[cls]) {
_JSMethodSignatureCache[(id<NSCopying>)cls] = [[NSMutableDictionary alloc]init];
}
methodSignature = _JSMethodSignatureCache[cls][selectorName];
if (!methodSignature) {
//区别在这里
methodSignature = [cls instanceMethodSignatureForSelector:selector];
methodSignature = fixSignature(methodSignature);
_JSMethodSignatureCache[cls][selectorName] = methodSignature;
}
[_JSMethodSignatureLock unlock];
if (!methodSignature) {
_exceptionBlock([NSString stringWithFormat:@"unrecognized selector %@ for instance %@", selectorName, instance]);
return nil;
}
//7.根据签名构造NSInvocation对象
invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
//8.为invocation对象设置target
[invocation setTarget:instance];
} else {
//区别在这里
methodSignature = [cls methodSignatureForSelector:selector];
methodSignature = fixSignature(methodSignature);
if (!methodSignature) {
_exceptionBlock([NSString stringWithFormat:@"unrecognized selector %@ for class %@", selectorName, className]);
return nil;
}
invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
[invocation setTarget:cls];
}
// 9.为invocation对象设置selector
[invocation setSelector:selector];

//10.根据签名得知每个参数的实际类型
NSUInteger numberOfArguments = methodSignature.numberOfArguments;
NSInteger inputArguments = [(NSArray *)argumentsObj count];
if (inputArguments > numberOfArguments - 2) {
//10.1 多参数方法仅支持 id 类型参数和 id 类型返回,直接revoke并返回.
// calling variable argument method, only support parameter type `id` and return type `id`
id sender = instance != nil ? instance : cls;
//msg_send方法
id result = invokeVariableParameterMethod(argumentsObj, methodSignature, sender, selector);
return formatOCToJS(result);
}

///参数处理
//10.2 将JS传递过来的参数进行对应的转换(如 NSNumber -> int),转换后为 NSInvoca
for (NSUInteger i = 2; i < numberOfArguments; i++) {
const char *argumentType = [methodSignature getArgumentTypeAtIndex:i];
id valObj = argumentsObj[i-2];
switch (argumentType[0] == 'r' ? argumentType[1] : argumentType[0]) {

#define JP_CALL_ARG_CASE(_typeString, _type, _selector) \
case _typeString: { \
_type value = [valObj _selector]; \
[invocation setArgument:&value atIndex:i];\
break; \
}

JP_CALL_ARG_CASE('c', char, charValue)
JP_CALL_ARG_CASE('C', unsigned char, unsignedCharValue)
JP_CALL_ARG_CASE('s', short, shortValue)
JP_CALL_ARG_CASE('S', unsigned short, unsignedShortValue)
JP_CALL_ARG_CASE('i', int, intValue)
JP_CALL_ARG_CASE('I', unsigned int, unsignedIntValue)
JP_CALL_ARG_CASE('l', long, longValue)
JP_CALL_ARG_CASE('L', unsigned long, unsignedLongValue)
JP_CALL_ARG_CASE('q', long long, longLongValue)
JP_CALL_ARG_CASE('Q', unsigned long long, unsignedLongLongValue)
JP_CALL_ARG_CASE('f', float, floatValue)
JP_CALL_ARG_CASE('d', double, doubleValue)
JP_CALL_ARG_CASE('B', BOOL, boolValue)

case ':': {
SEL value = nil;
if (valObj != _nilObj) {
value = NSSelectorFromString(valObj);
}
[invocation setArgument:&value atIndex:i];
break;
}
case '{': {
NSString *typeString = extractStructName([NSString stringWithUTF8String:argumentType]);
JSValue *val = arguments[i-2];
#define JP_CALL_ARG_STRUCT(_type, _methodName) \
if ([typeString rangeOfString:@#_type].location != NSNotFound) { \
_type value = [val _methodName]; \
[invocation setArgument:&value atIndex:i]; \
break; \
}
JP_CALL_ARG_STRUCT(CGRect, toRect)
JP_CALL_ARG_STRUCT(CGPoint, toPoint)
JP_CALL_ARG_STRUCT(CGSize, toSize)
JP_CALL_ARG_STRUCT(NSRange, toRange)
@synchronized (_context) {
NSDictionary *structDefine = _registeredStruct[typeString];
if (structDefine) {
size_t size = sizeOfStructTypes(structDefine[@"types"]);
void *ret = malloc(size);
getStructDataWithDict(ret, valObj, structDefine);
[invocation setArgument:ret atIndex:i];
free(ret);
break;
}
}

break;
}
case '*':
case '^': {
if ([valObj isKindOfClass:[JPBoxing class]]) {
void *value = [((JPBoxing *)valObj) unboxPointer];

if (argumentType[1] == '@') {
if (!_TMPMemoryPool) {
_TMPMemoryPool = [[NSMutableDictionary alloc] init];
}
if (!_markArray) {
_markArray = [[NSMutableArray alloc] init];
}
memset(value, 0, sizeof(id));
[_markArray addObject:valObj];
}

[invocation setArgument:&value atIndex:i];
break;
}
}
case '#': {
if ([valObj isKindOfClass:[JPBoxing class]]) {
Class value = [((JPBoxing *)valObj) unboxClass];
[invocation setArgument:&value atIndex:i];
break;
}
}
default: {
if (valObj == _nullObj) {
valObj = [NSNull null];
[invocation setArgument:&valObj atIndex:i];
break;
}
if (valObj == _nilObj ||
([valObj isKindOfClass:[NSNumber class]] && strcmp([valObj objCType], "c") == 0 && ![valObj boolValue])) {
valObj = nil;
[invocation setArgument:&valObj atIndex:i];
break;
}
if ([(JSValue *)arguments[i-2] hasProperty:@"__isBlock"]) {
JSValue *blkJSVal = arguments[i-2];
Class JPBlockClass = NSClassFromString(@"JPBlock");
if (JPBlockClass && ![blkJSVal[@"blockObj"] isUndefined]) {
__autoreleasing id cb = [JPBlockClass performSelector:@selector(blockWithBlockObj:) withObject:[blkJSVal[@"blockObj"] toObject]];
[invocation setArgument:&cb atIndex:i];
Block_release((__bridge void *)cb);
} else {
__autoreleasing id cb = genCallbackBlock(arguments[i-2]);
[invocation setArgument:&cb atIndex:i];
}
} else {
[invocation setArgument:&valObj atIndex:i];
}
}
}
}

if (superClassName) _currInvokeSuperClsName[selectorName] = superClassName;
//11.执行 invoke 方法,并且传递指定的参数
[invocation invoke];
if (superClassName) [_currInvokeSuperClsName removeObjectForKey:selectorName];
if ([_markArray count] > 0) {
for (JPBoxing *box in _markArray) {
void *pointer = [box unboxPointer];
id obj = *((__unsafe_unretained id *)pointer);
if (obj) {
@synchronized(_TMPMemoryPool) {
[_TMPMemoryPool setObject:obj forKey:[NSNumber numberWithInteger:[(NSObject*)obj hash]]];
}
}
}
}

char returnType[255];
strcpy(returnType, [methodSignature methodReturnType]);

// Restore the return type
if (strcmp(returnType, @encode(JPDouble)) == 0) {
strcpy(returnType, @encode(double));
}
if (strcmp(returnType, @encode(JPFloat)) == 0) {
strcpy(returnType, @encode(float));
}

id returnValue;
//12. 获取 invocation 运行返回值.
if (strncmp(returnType, "v", 1) != 0) {
if (strncmp(returnType, "@", 1) == 0) {
void *result;
[invocation getReturnValue:&result];
// 13. 将返回值封装成JS对应的对象并返回.

//For performance, ignore the other methods prefix with alloc/new/copy/mutableCopy
if ([selectorName isEqualToString:@"alloc"] || [selectorName isEqualToString:@"new"] ||
[selectorName isEqualToString:@"copy"] || [selectorName isEqualToString:@"mutableCopy"]) {
returnValue = (__bridge_transfer id)result;
} else {
returnValue = (__bridge id)result;
}
return formatOCToJS(returnValue);

} else {
switch (returnType[0] == 'r' ? returnType[1] : returnType[0]) {

#define JP_CALL_RET_CASE(_typeString, _type) \
case _typeString: { \
_type tempResultSet; \
[invocation getReturnValue:&tempResultSet];\
returnValue = @(tempResultSet); \
break; \
}

JP_CALL_RET_CASE('c', char)
JP_CALL_RET_CASE('C', unsigned char)
JP_CALL_RET_CASE('s', short)
JP_CALL_RET_CASE('S', unsigned short)
JP_CALL_RET_CASE('i', int)
JP_CALL_RET_CASE('I', unsigned int)
JP_CALL_RET_CASE('l', long)
JP_CALL_RET_CASE('L', unsigned long)
JP_CALL_RET_CASE('q', long long)
JP_CALL_RET_CASE('Q', unsigned long long)
JP_CALL_RET_CASE('f', float)
JP_CALL_RET_CASE('d', double)
JP_CALL_RET_CASE('B', BOOL)

case '{': {
NSString *typeString = extractStructName([NSString stringWithUTF8String:returnType]);
#define JP_CALL_RET_STRUCT(_type, _methodName) \
if ([typeString rangeOfString:@#_type].location != NSNotFound) { \
_type result; \
[invocation getReturnValue:&result]; \
return [JSValue _methodName:result inContext:_context]; \
}
JP_CALL_RET_STRUCT(CGRect, valueWithRect)
JP_CALL_RET_STRUCT(CGPoint, valueWithPoint)
JP_CALL_RET_STRUCT(CGSize, valueWithSize)
JP_CALL_RET_STRUCT(NSRange, valueWithRange)
@synchronized (_context) {
NSDictionary *structDefine = _registeredStruct[typeString];
if (structDefine) {
size_t size = sizeOfStructTypes(structDefine[@"types"]);
void *ret = malloc(size);
[invocation getReturnValue:ret];
NSDictionary *dict = getDictOfStruct(ret, structDefine);
free(ret);
return dict;
}
}
break;
}
case '*':
case '^': {
void *result;
[invocation getReturnValue:&result];
returnValue = formatOCToJS([JPBoxing boxPointer:result]);
if (strncmp(returnType, "^{CG", 4) == 0) {
if (!_pointersToRelease) {
_pointersToRelease = [[NSMutableArray alloc] init];
}
[_pointersToRelease addObject:[NSValue valueWithPointer:result]];
CFRetain(result);
}
break;
}
case '#': {
Class result;
[invocation getReturnValue:&result];
returnValue = formatOCToJS([JPBoxing boxClass:result]);
break;
}
}
return returnValue;
}
}
return nil;
}

callSelector代码很长但是它所处理的任务比较比较明确,它主要是通过js传过来的参数来构建一个Invocation 通过[invocation invoke]来执行对应的方法。代码细节部分已经给出了较为详细的注解大家可以看下整个流程。

我们这里回顾下整个过程;

在启动JSPatch 引擎之后会向Context注入一系列的OC方法给JS回调。
紧接着加载JSPatch.js 它里面主要定义了一些关键方法,最主要的是__c,它会从中提取到OC消息转发所必须的一系列参数,通过_OC_callI或者_OC_callC 传给 OC 层, OC 层中调用callSelector 方法,通过js传过来的参数来构建一个Invocation 通过[invocation invoke]来执行对应的方法。
而在OC层中加载补丁js脚本后会将脚本中的xxx.XXXX 正则替换为xxx.__c(“XXXX”),这样通过上面的解释后调用OC层方法,如果忽略掉这部分的整个细节,整个过程是js 补丁脚本中的 xxx.XXXX 最后会转为 OC中的[invocation invoke]
是不是有点巧妙?


总结

这篇博客主要从如何通过消息转发机制来使用js补丁方法/类来替换现有的方法,从而达到热修复的目的。以及如何将js中OC相关的代码交给OC层来处理。第一部分思路有点和Aspect类似,大家可以细细体会下,很巧妙的一个方案。下面是JSPatch的一个简单的框图。

一:关于ReactiveCocoa的知识点

1:RACSigner基础知识点


信号类(RACSiganl),只是表示当数据改变时,信号内部会发出数据,它本身不具备发送信号的能力,而是交给内部一个订阅者去发出。

默认一个信号都是冷信号,也就是值改变了,也不会触发,只有订阅了这个信号,这个信号才会变为热信号,值改变了才会触发。

如何订阅信号:调用信号RACSignal的subscribeNext就能订阅

常见的操作方法:


flattenMap map 用于把源信号内容映射成新的内容。

concat 组合 按一定顺序拼接信号,当多个信号发出的时候,有顺序的接收信号

then 用于连接两个信号,当第一个信号完成,才会连接then返回的信号。

merge 把多个信号合并为一个信号,任何一个信号有新值的时候就会调用

zipWith 把两个信号压缩成一个信号,只有当两个信号同时发出信号内容时,并且把两个信号的内容合并成一个元组,才会触发压缩流的next事件。

combineLatest:将多个信号合并起来,并且拿到各个信号的最新的值,必须每个合并的signal至少都有过一次sendNext,才会触发合并的信号。

reduce聚合:用于信号发出的内容是元组,把信号发出元组的值聚合成一个值

filter:过滤信号,使用它可以获取满足条件的信号.

ignore:忽略完某些值的信号.

distinctUntilChanged:当上一次的值和当前的值有明显的变化就会发出信号,否则会被忽略掉。

take:从开始一共取N次的信号

takeLast:取最后N次的信号,前提条件,订阅者必须调用完成,因为只有完成,就知道总共有多少信号.

takeUntil:(RACSignal *):获取信号直到某个信号执行完成

skip:(NSUInteger):跳过几个信号,不接受。

switchToLatest:用于signalOfSignals(信号的信号),有时候信号也会发出信号,会在signalOfSignals中,获取signalOfSignals发送的最新信号。

doNext: 执行Next之前,会先执行这个Block

doCompleted: 执行sendCompleted之前,会先执行这个Block

timeout:超时,可以让一个信号在一定的时间后,自动报错。

interval 定时:每隔一段时间发出信号

delay 延迟发送next

retry重试 :只要失败,就会重新执行创建信号中的block,直到成功.

replay重放:当一个信号被多次订阅,反复播放内容

throttle节流:当某个信号发送比较频繁时,可以使用节流,在某一段时间不发送信号内容,过了一段时间获取信号的最新内容发出。

2:RACSubject基础知识点

RACSubject:信号提供者,自己可以充当信号,又能发送信号  使用场景:通常用来代替代理,有了它,就不必要定义代理了

RACSubject使用步骤
1.创建信号 [RACSubject subject],跟RACSiganl不一样,创建信号时没有block。
2.订阅信号 - (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock
3.发送信号 sendNext:(id)value

RACSubject:底层实现和RACSignal不一样。
1.调用subscribeNext订阅信号,只是把订阅者保存起来,并且订阅者的nextBlock已经赋值了。
2.调用sendNext发送信号,遍历刚刚保存的所有订阅者,一个一个调用订阅者的nextBlock。

RACSubject实例进行map操作之后, 发送完毕一定要调用-sendCompleted, 否则会出现内存泄漏; 而RACSignal实例不管是否进行map操作, 不管是否调用-sendCompleted, 都不会出现内存泄漏.
原因 : 因为RACSubject是热信号, 为了保证未来有事件发生的时候, 订阅者可以收到信息, 所以需要对持有订阅者!

3:RACSequence基础知识点


RACSequence:RAC中的集合类,用于代替NSArray,NSDictionary,可以使用它来快速遍历数组和字典

通过RACSequence对数组进行操作
这里其实是三步
第一步: 把数组转换成集合RACSequence numbers.rac_sequence
第二步: 把集合RACSequence转换RACSignal信号类,numbers.rac_sequence.signal
第三步: 订阅信号,激活信号,会自动把集合中的所有值,遍历出来。

4:RACCommand基础知识点


RACCommand:RAC中用于处理事件的类,可以把事件如何处理,事件中的数据如何传递,包装到这个类中,他可以很方便的监控事件的执行过程

一、RACCommand使用步骤:
1.创建命令 initWithSignalBlock:(RACSignal * (^)(id input))signalBlock
2.在signalBlock中,创建RACSignal,并且作为signalBlock的返回值
3.执行命令 - (RACSignal *)execute:(id)input

二、RACCommand使用注意:
1.signalBlock必须要返回一个信号,不能传nil.
2.如果不想要传递信号,直接创建空的信号[RACSignal empty];
3.RACCommand中信号如果数据传递完,必须调用[subscriber sendCompleted],这时命令才会执行完毕,否则永远处于执行中。
4.RACCommand需要被强引用,否则接收不到RACCommand中的信号,因此RACCommand中的信号是延迟发送的。

三、RACCommand设计思想:内部signalBlock为什么要返回一个信号,这个信号有什么用。
1.在RAC开发中,通常会把网络请求封装到RACCommand,直接执行某个RACCommand就能发送请求。
2.当RACCommand内部请求到数据的时候,需要把请求的数据传递给外界,这时候就需要通过signalBlock返回的信号传递了。

四、如何拿到RACCommand中返回信号发出的数据。
1.RACCommand有个执行信号源executionSignals,这个是signal of signals(信号的信号),意思是信号发出的数据是信号,不是普通的类型。
2.订阅executionSignals就能拿到RACCommand中返回的信号,然后订阅signalBlock返回的信号,就能获取发出的值。

五、监听当前命令是否正在执行executing

六、使用场景,监听按钮点击,网络请求

5:RACMulticastConnection基础知识点


RACMulticastConnection:用于当一个信号,被多次订阅时,为了保证创建信号时,避免多次调用创建信号中的block,造成副作用,可以使用这个类处理
使用注意:RACMulticastConnection通过RACSignal的-publish或者-muticast:方法创建.

RACMulticastConnection使用步骤:
1.创建信号 + (RACSignal *)createSignal:(RACDisposable * (^)(id<RACSubscriber> subscriber))didSubscribe
2.创建连接 RACMulticastConnection *connect = [signal publish];
3.订阅信号,注意:订阅的不在是之前的信号,而是连接的信号。 [connect.signal subscribeNext:nextBlock]
4.连接 [connect connect]

RACMulticastConnection底层原理:
1.创建connect,connect.sourceSignal -> RACSignal(原始信号) connect.signal -> RACSubject
2.订阅connect.signal,会调用RACSubject的subscribeNext,创建订阅者,而且把订阅者保存起来,不会执行block。
3.[connect connect]内部会订阅RACSignal(原始信号),并且订阅者是RACSubject
3.1.订阅原始信号,就会调用原始信号中的didSubscribe
3.2 didSubscribe,拿到订阅者调用sendNext,其实是调用RACSubject的sendNext
4.RACSubject的sendNext,会遍历RACSubject所有订阅者发送信号。
4.1 因为刚刚第二步,都是在订阅RACSubject,因此会拿到第二步所有的订阅者,调用他们的nextBlock


需求:假设在一个信号中发送请求,每次订阅一次都会发送请求,这样就会导致多次请求。
解决:使用RACMulticastConnection就能解决.

6:RAC结合UI一般事件


rac_signalForSelector : 代替代理

rac_valuesAndChangesForKeyPath: KVO

rac_signalForControlEvents:监听事件

rac_addObserverForName 代替通知

rac_textSignal:监听文本框文字改变

rac_liftSelector:withSignalsFromArray:Signals:当传入的Signals(信号数组),每一个signal都至少sendNext过一次,就会去触发第一个selector参数的方法。

7:高阶操作知识内容

8:RAC并发编程知识点


1: subscribeOn运用

RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSLog(@"%@ 111",[NSThread currentThread]);

//可以放更新UI操作

[subscriber sendNext:@0.1];
RACDisposable *disposable = [[RACScheduler scheduler] schedule:^{
NSLog(@"%@ 5555",[NSThread currentThread]);
[subscriber sendNext:@1.1];
[subscriber sendCompleted];
}];
return disposable;
}];
[[RACScheduler scheduler] schedule:^{
NSLog(@"%@ 222",[NSThread currentThread]);
[[signal subscribeOn:[RACScheduler mainThreadScheduler]] subscribeNext:^(id x) {
NSLog(@"%@ %@",[NSThread currentThread], x);
}]; }];
NSLog(@"%@ 4444",[NSThread currentThread]);

//使用subscribeOn 可以让signal内的代码在主线程中运行,sendNext在哪个线程 则对应的订阅输出就在对应线程上,所以0.1输出是在主线程中; 所以当在signal里面可能要放一些更新UI的操作,而这些是要在主线程才能处理,而订阅者却无法确认,所以要使用subscribeOn让它在主线程中;
//能够保证didSubscribe block在指定的scheduler
//不能保证sendNext、 error、 complete在哪个scheduler


2:deliverOn运用

RACSignal *signal = [RACSignal createSignal:^RACDisposable *(id<RACSubscriber> subscriber) {
NSLog(@"%@ 111",[NSThread currentThread]);
[subscriber sendNext:@0.1];
RACDisposable *disposable = [[RACScheduler scheduler] schedule:^{
NSLog(@"%@ 555",[NSThread currentThread]);
[subscriber sendNext:@1.1];
[subscriber sendCompleted];
}];
return disposable;
}];
[[RACScheduler scheduler] schedule:^{
NSLog(@"%@ 222",[NSThread currentThread]);
[[signal deliverOn:[RACScheduler mainThreadScheduler]] subscribeNext:^(id x) {
NSLog(@"%@ %@",[NSThread currentThread], x);

//可以放UI更新操作

}]; }];

//当我们让订阅的处理代码在指定的线程中执行,而不必去关心发送信号的当前线程,就可以deliverOn

9:冷信号跟热信号知识点


Hot Observable是主动的,尽管你并没有订阅事件,但是它会时刻推送,就像鼠标移动;而Cold Observable是被动的,只有当你订阅的时候,它才会发布消息。

Hot Observable可以有多个订阅者,是一对多,集合可以与订阅者共享信息;而Cold Observable只能一对一,当有不同的订阅者,消息是重新完整发送。

热信号是主动的,即使你没有订阅事件,它仍然会时刻推送 而冷信号是被动的,只有当你订阅的时候,它才会发送消息
热信号可以有多个订阅者,是一对多,信号可以与订阅者共享信息 而冷信号只能一对一,当有不同的订阅者,消息会从新完整发送

冷信号与热信号的本质区别在于是否保持状态,冷信号的多次订阅是不保持状态的,而热信号的多次订阅可以保持状态


10:RACDisposable知识点


RACDisposable用于取消订阅信号,默认信号发送完之后就会主动的取消订阅。订阅信号使用的subscribeNext:方法返回的就是RACDisposable类型的对象

当订阅者发送信号- (void)sendNext:(id)value之后,会执行:- (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock中的nextBlock。当nextBlock执行完毕也就意味着subscribeNext方法返回了RACDisposable对象。

1.如果不强引用订阅者对象,默认情况下会自动取消订阅,我们可以拿到RACDisposable 用+ (instancetype)disposableWithBlock:(void (^)(void))block做清空资源的一些操作了。

2.如果不希望自动取消订阅,我们应该强引用RACSubscriber * subscriber。在想要取消订阅的时候用- (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock返回的RACDisposable对象去调用- (void)dispose方法

11:RACChannel知识点

RACChannelTerminal *channelA = RACChannelTo(self, valueA);
RACChannelTerminal *channelB = RACChannelTo(self, valueB);
[[channelA map:^id(NSString *value) {
if ([value isEqualToString:@"西"]) {
return @"东";
}
return value;
}] subscribe:channelB];
[[channelB map:^id(NSString *value) {
if ([value isEqualToString:@"左"]) {
return @"右";
}
return value;
}] subscribe:channelA];
[[RACObserve(self, valueA) filter:^BOOL(id value) {
return value ? YES : NO;
}] subscribeNext:^(NSString* x) {
NSLog(@"你向%@", x);
}];
[[RACObserve(self, valueB) filter:^BOOL(id value) {
return value ? YES : NO;
}] subscribeNext:^(NSString* x) {
NSLog(@"他向%@", x);
}];
self.valueA = @"西";
self.valueB = @"左";


RACChannelTerminal *characterRemainingTerminal = RACChannelTo(_loginButton, titleLabel.text);

[[self.userNameText.rac_textSignal map:^id(NSString *text) {
return [@(100 - (NSInteger)text.length) stringValue];
}] subscribe:characterRemainingTerminal];

12:RAC倒计时小实例

//倒计时的效果
RACSignal *(^counterSigner)(NSNumber *count)=^RACSignal *(NSNumber *count)
{
RACSignal *timerSignal=[RACSignal interval:1 onScheduler:RACScheduler.mainThreadScheduler];
RACSignal *counterSignal=[[timerSignal scanWithStart:count reduce:^id(NSNumber *running, id next) {
return @(running.integerValue -1);
}] takeUntilBlock:^BOOL(NSNumber *x) {
return x.integerValue<0;
}];

return [counterSignal startWith:count];
};


RACSignal *enableSignal=[self.myTextField.rac_textSignal map:^id(NSString *value) {
return @(value.length==11);
}];

RACCommand *command=[[RACCommand alloc]initWithEnabled:enableSignal signalBlock:^RACSignal *(id input) {
return counterSigner(@10);
}];

RACSignal *counterStringSignal=[[command.executionSignals switchToLatest] map:^id(NSNumber *value) {
return [value stringValue];
}];

RACSignal *resetStringSignal=[[command.executing filter:^BOOL(NSNumber *value) {
return !value.boolValue;
}] mapReplace:@"点击获得验证码"];

//[self.myButton rac_liftSelector:@selector(setTitle:forState:) withSignals:[RACSignal merge:@[counterStringSignal,resetStringSignal]],[RACSignal return:@(UIControlStateNormal)],nil];

//上面也可以写成下面这样
@weakify(self);
[[RACSignal merge:@[counterStringSignal,resetStringSignal]] subscribeNext:^(id x) {
@strongify(self);
[self.myButton setTitle:x forState:UIControlStateNormal];
}];

self.myButton.rac_command=command;


//编写关于委托的编写方式 是在self上面进行rac_signalForSelector
[[self
rac_signalForSelector:@selector(textFieldShouldReturn:)
fromProtocol:@protocol(UITextFieldDelegate)]
subscribeNext:^(RACTuple *tuple) {
@strongify(self)
if (tuple.first == self.myTextField)
{
NSLog(@"触发");
};
}];

self.myTextField.delegate = self;

13:常见的宏定义运用


1
RAC(TARGET, [KEYPATH, [NIL_VALUE]]):用于给某个对象的某个属性绑定
只要文本框文字改变,就会修改label的文字
RAC(self.labelView,text) = _textField.rac_textSignal;

2:
RACObserve(self, name):监听某个对象的某个属性,返回的是信号。
[RACObserve(self.view, center) subscribeNext:^(id x) {
NSLog(@"%@",x);
}];


当RACObserve放在block里面使用时一定要加上weakify,不管里面有没有使用到self;否则会内存泄漏,因为RACObserve宏里面就有一个self
@weakify(self);
RACSignal *signal3 = [anotherSignal flattenMap:^(NSArrayController *arrayController) {
//Avoids a retain cycle because of RACObserve implicitly referencing self
@strongify(self);
return RACObserve(arrayController, items);
}];

3:
@weakify(Obj)和@strongify(Obj),一般两个都是配套使用,在主头文件(ReactiveCocoa.h)中并没有导入,需要自己手动导入,RACEXTScope.h才可以使用。但是每次导入都非常麻烦,只需要在主头文件自己导入就好了

4:
RACTuplePack:把数据包装成RACTuple(元组类)
把参数中的数据包装成元组
RACTuple *tuple = RACTuplePack(@10,@20);

5:
RACTupleUnpack:把RACTuple(元组类)解包成对应的数据
把参数中的数据包装成元组
RACTuple *tuple = RACTuplePack(@"xmg",@20);

解包元组,会把元组的值,按顺序给参数里面的变量赋值
name = @"xmg" age = @20
RACTupleUnpack(NSString *name,NSNumber *age) = tuple;

二:关于使用ReactiveCocoa结合MVVM模式的实例;

MVVM模式和MVC模式一样,主要目的是分离视图(View)和模型(Model),有几大优点

  1. 低耦合。视图(View)可以独立于Model变化和修改,一个ViewModel可以绑定到不同的”View”上,当View变化的时候Model可以不变,当Model变化的时候View也可以不变。

  2. 可重用性。你可以把一些视图逻辑放在一个ViewModel里面,让很多view重用这段视图逻辑。

  3. 独立开发。开发人员可以专注于业务逻辑和数据的开发(ViewModel),设计人员可以专注于页面设计。

  4. 可测试。界面素来是比较难于测试的,而现在测试可以针对ViewModel来写。

三:单元测试知识

单元测试这边主要采用两种方式,一种是XCode自动的XCTestCase进行,如下面这些就是它所对应的断言等,另外一种是采有KIWI的插件进行测试;项目中有针对viewController、viewModel、帮助类等的测试实例;运用快捷键(command+U)可以运行单元测试实例;


//知识点一:
//方法在XCTestCase的测试方法调用之前调用,可以在测试之前创建在test case方法中需要用到的一些对象等
//- (void)setUp ;
//当测试全部结束之后调用tearDown方法,法则在全部的test case执行结束之后清理测试现场,释放资源删除不用的对象等
//- (void)tearDown ;
//测试代码执行性能
//- (void)testPerformanceExample


//知识点二:
//通用断言
XCTFail(format…)
//为空判断,a1为空时通过,反之不通过;
XCTAssertNil(a1, format...)
//不为空判断,a1不为空时通过,反之不通过;
XCTAssertNotNil(a1, format…)
//当expression求值为TRUE时通过;
XCTAssert(expression, format...)
//当expression求值为TRUE时通过;
XCTAssertTrue(expression, format...)
//当expression求值为False时通过;
XCTAssertFalse(expression, format...)
//判断相等,[a1 isEqual:a2]值为TRUE时通过,其中一个不为空时,不通过;
XCTAssertEqualObjects(a1, a2, format...)
//判断不等,[a1 isEqual:a2]值为False时通过;
XCTAssertNotEqualObjects(a1, a2, format...)
//判断相等(当a1和a2是 C语言标量、结构体或联合体时使用,实际测试发现NSString也可以);
XCTAssertEqual(a1, a2, format...)
//判断不等(当a1和a2是 C语言标量、结构体或联合体时使用);
XCTAssertNotEqual(a1, a2, format...)
//判断相等,(double或float类型)提供一个误差范围,当在误差范围(+/-accuracy)以内相等时通过测试;
XCTAssertEqualWithAccuracy(a1, a2, accuracy, format...)
//判断不等,(double或float类型)提供一个误差范围,当在误差范围以内不等时通过测试;
XCTAssertNotEqualWithAccuracy(a1, a2, accuracy, format...)
//异常测试,当expression发生异常时通过,反之不通过;
XCTAssertThrows(expression, format...)
//异常测试,当expression发生specificException异常时通过;反之发生其他异常或不发生异常均不通过
XCTAssertThrowsSpecific(expression, specificException, format...)
//异常测试,当expression发生具体异常、具体异常名称的异常时通过测试,反之不通过;
XCTAssertThrowsSpecificNamed(expression, specificException, exception_name, format...)
//异常测试,当expression没有发生异常时通过测试;
XCTAssertNoThrow(expression, format…)
//异常测试,当expression没有发生具体异常、具体异常名称的异常时通过测试,反之不通过;
XCTAssertNoThrowSpecific(expression, specificException, format...)
//异常测试,当expression没有发生具体异常、具体异常名称的异常时通过测试,反之不通过
XCTAssertNoThrowSpecificNamed(expression, specificException, exception_name, format...)

采用KiWi的单元测试效果:


#import <Kiwi/Kiwi.h>
//把原本在项目pch中那些第三方插件的头文件也要引入
#import <ReactiveCocoa/ReactiveCocoa.h>

//测试LogInViewController
#import "RACTestLoginViewController.h"


SPEC_BEGIN(LoginViewControllerSpec)

describe(@"RACTestLoginViewController", ^{
__block RACTestLoginViewController *controller = nil;

beforeEach(^{
controller = [RACTestLoginViewController new];
[controller view];
});

afterEach(^{
controller = nil;
});

describe(@"Root View", ^{

context(@"when view did load", ^{
it(@"should bind data", ^{
controller.userNameText.text=@"wujunyang";
controller.passWordTest.text=@"123456";
//
//一定要调用sendActionsForControlEvents方法来通知UI已经更新 因为RAC是监听这个输入框的变化
[controller.userNameText sendActionsForControlEvents:UIControlEventEditingChanged];
[controller.passWordTest sendActionsForControlEvents:UIControlEventEditingChanged];

[[controller.myLoginViewModel.username should] equal:controller.userNameText.text];
[[controller.myLoginViewModel.password should] equal:controller.passWordTest.text];
});
});

});
});

SPEC_END

关于kiwi中的操作类型可以直接查看:https://github.com/allending/Kiwi/wiki/Expectations

注意:发现在进行单元测试时,针对RAC就会报[RACStream(Operations) reduceEach:]_block_invoke,后来发现是Pod引入写法有问题,导致的【it usually means RAC is being linked twice. Make sure it’s only in your app target.】 所以测试的MobileProjectTests特别要注意;


platform :ios, '7.0'

abstract_target 'MobileProjectDefault' do
pod 'AFNetworking', '~>2.6.0'
pod 'SDWebImage', '~>3.7'
pod 'JSONModel', '~> 1.0.1'
pod 'Masonry','~>0.6.1'
pod 'FMDB/common' , '~>2.5'
pod 'FMDB/SQLCipher', '~>2.5'
pod 'CocoaLumberjack', '~> 2.0.0-rc'
pod 'ReactiveCocoa', '2.5'
pod 'CYLTabBarController'
pod 'MLeaksFinder' #可以把它放在MobileProject_Local的target中 这样就不会影响到产品环境
pod 'RealReachability'

target 'MobileProject_Local' do

end

target 'MobileProject' do

target 'MobileProjectTests' do
inherit! :search_paths
pod 'Kiwi', '~> 2.3.1'
end
end
end

四:ReactiveCocoa知识分享地址


ReactiveCocoa 和 MVVM 入门 http://yulingtianxia.com/blog/2015/05/21/ReactiveCocoa-and-MVVM-an-Introduction/

MVVM Tutorial with ReactiveCocoa http://southpeak.github.io/blog/2014/08/08/mvvmzhi-nan-yi-:flickrsou-suo-shi-li/

ReactiveCocoa 1-官方readme文档翻译 http://cindyfn.com/reactivecocoa/2014/12/01/ios-frame-use-ReactiveCocoa.html

这样好用的ReactiveCocoa,根本停不下来 http://www.cocoachina.com/ios/20150817/13071.html

ReactiveCocoa基本组件:深入浅出RACCommand http://www.tuicool.com/articles/nYJRvu

ReactiveCocoa自述:工作原理和应用 http://www.cocoachina.com/ios/20150702/12302.html

RACSignal的巧克力工厂 http://www.cnblogs.com/sunnyxx/p/3547763.html

ReactiveCocoa一些概念讲解 http://www.thinksaas.cn/group/topic/347067/

细说ReactiveCocoa的冷信号与热信号(二):为什么要区分冷热信号 http://www.tuicool.com/articles/e2uMzyq

细说ReactiveCocoa的冷信号与热信号(三):怎么处理冷信号与热信号 http://www.tuicool.com/articles/emIVZjY

最快让你上手ReactiveCocoa之基础篇 http://www.jianshu.com/p/87ef6720a096

最快让你上手ReactiveCocoa之进阶篇 http://www.jianshu.com/p/e10e5ca413b7

ReactiveCocoa基础:理解并使用RACCommand http://www.yiqivr.com/2015/10/19/%E8%AF%91-ReactiveCocoa%E5%9F%BA%E7%A1%80%EF%BC%9A%E7%90%86%E8%A7%A3%E5%B9%B6%E4%BD%BF%E7%94%A8RACCommand/

RAC一些代码总结:https://github.com/shuaiwang007/RAC

ReactiveCocoa小总结 http://www.jianshu.com/p/8fd6c8349774

如何在ReactiveCocoa中写单元测试 http://www.jianshu.com/p/412875512bd1

TDD的iOS开发初步以及Kiwi使用入门 https://onevcat.com/2014/02/ios-test-with-kiwi/

0. 开篇叨叨

今天给大家讲的主题是iOS组件化,提起组件化让我想起了在读研的时候用的一款图形化编程语言NI LabVIEW,它是由美国国家仪器公司开发的,它编写的程序是由一个个图形模块堆叠起来的,我们可以很轻松地将几个模块通过数据流连接起来构成一个比较复杂的程序,当然它还有和硬件通信控制硬件的功能,但这不是我们的重点。从整体角度来看,它编写的程序主要由下面几部分构成:

模块         -- 提供某个功能服务
模块间的连线 -- 数据事件沿着连线流动
逻辑块 -- 控制数据怎样沿着连线在这些功能模块上流动

我们在组件化某个项目的时候也就是也就是在搭建类似NI LabVIEW平台的过程,在这个过程中我们需要考虑如何划分和封装模块,模块间数据怎么交互,如何注册/查找对应服务的模块。

开发人员在组件化的项目上编写代码的时候只需要提供输入数据,然后调用合适的组件,将数据送入组件处理,组件处理完后将处理后的数据按照约定的交付形式输出,然后再流入下个组件,在项目中能够通过便捷的方式路由到任何一个组件,并且组件间没有任何耦合,拆开组件在不修改任何代码的情况下在另一个项目中复用组件。

那么大家思考过没:什么是组件化? 为啥要在项目上使用组件化技术?

在我看来组件化指的是将项目代码进行分层,解耦,并制定模块间通信的方式,从而实现解耦,复用。不可否认有些项目的组件化是KPI产物,但是很大程度来说组件化都是十分必要的,为啥?它有啥好处?

  • 首先组件化带来的最大好处就是项目成果积累,一般一个公司都会不只一个项目,如果采用组件化将可共享的模块从各个项目中抽离出来,形成一个组件,那么其他项目需要这个功能的时候就可以直接复用,而不用重复开发,并且这些组件在多个项目的维护下会越来越健壮,这种积累从长远角度看对于一个公司来说是一个十分可观的成果。

  • 其次从宏观角度上看,每个组件都相当于一个黑盒,我们只关心我们注入哪些数据,组件吐出哪些数据,里面的细节对于组件使用者来说不用太多关照,这样就会使得整个项目的结构更加清晰。同时这种黑盒结构很容易通过测试用例对组件进行测试。

  • 在多人协作的团队可以将整个项目分割成多个模块分配给开发成员。

  • 由于组件是可以单独编译的,开发测试过程中,可以只编译自己那部分代码,不需要编译整个项目代码,从这个角度出发,组件化可以很大程度上加快整个项目的编译速度。从测试角度可以在开发过程中先提交组件测试,测试通过后再进行组件集成测试,从开发流程角度可以更好得进行独立并行开发,各个组件的负责人在功能尚未开发完之前可以按照约定的组件接口结构,Mock假数据给调用者,这样我们在开发组件的同时,依赖方可以同时开发他们的组件,等到我们的组件开发完毕后使用真实数据替换Mock假数据就可以很轻松得将两个组件对接起来。

  • 不同组件的开发人员可以选择自己喜欢的架构模式 (当然这里不推荐这样做,一个项目尽量保持架构一致)

  • 可以针对单个组件或者模块进行编译测试,更好定位问题,我们可以通过对组件间接口进行测试,如果发现异常就可以针对组件进行排查。

  • 组件化架构在各个模块之间天然形成了一道屏障,避免某个开发人员偷懒直接引用头文件,产生组件间的耦合,破坏整体架构,同时,因为每个人都负责自己的组件,代码提交也只提交自己负责模块的仓库,所以代码冲突的问题会变得很少,还有好处就是假设以后某个业务发生大的改变,需要对相关代码进行重构,可以在单个组件内进行重构,只要保证对外接口准确无误就可以了,组件化架构降低了重构的风险,保证了代码的健壮性。

但是组件化又有那些缺点呢?

其实倒不是组件化有啥缺点,组件化的唯一缺点就是会给项目带来复杂性,比如我们在非组件化项目中从一个对象拿到另一个对象是十分容易的,但是组件化项目中有可能这两个对象是跨组件的,这种情况下可能就需要绕个大弯去获取,这对于项目初期进度很赶的时候是十分恼火的一个问题,并且某个项目刚刚成立之前,往往对于项目的整体形态都没形成一个很系统的认知,这种情况下对项目进行组件化规划是比较困难的,因为如果组件划分不合理调整起来是很困难的一件事情,这对需求的把握能力以及前瞻能力都有很高的要求,这就导致一般项目初期很少考虑组件化,到了项目第一个demo出来,并且人员到齐的情况下才会将组件化纳入到项目日程。

1. 组件化要思考的关键问题

组件简单说就是如何组织组件,如何添加组件,如何发现组件,如何交付数据,如何传递动作,把这些理清楚了组件框架就可以理出个头绪了,往详细得说组件化需要考虑如下几点:


* 如何划分组件/组件解耦
* 组件间数据通信的时候数据怎么交付
* 如何设计组件路由
* 如何设计消息总线

2. 用组件化视角审视项目结构

在用组件化视角审视项目结构之前我们看下一般一个应用的通用部分有哪些,下面是我自己设计的一个框架的结构图,该框架是我的练手项目,目前完成大部分的基础工作,还有一部分正在开发中,后续整个完善后开源给大家,大家一起完善,一起学习,目前暂定名字为IDLFundation,之前有打算叫GKFundation(Geek Fundation) 但是后面觉得俗气,所以想了个更俗气的名字IDLFundation(idealist Fundation 理想主义者)

2.1 数据层

数据层主要包括两大部分:DataSource以及Data Processor。DataSource 又可以分成Local DataSource 以及Remote DataSource

  • Local DataSource 包括
* 数据库:一般用于存储结构性数据
* 缓存:  一般用于存储有时效性的数据,超过某个过期时间或者超过一定容量限制就会被清除
* 文件管理:一般用于管理文件
* KeyChain: 它的特点是除非恢复出厂设置否则即使卸载应用数据也都会保留着,很适合存储密码等关键信息。
* UserDefault: 一般用于存储极为小型的数据,比如某个标志位等,它会随着应用的卸载而丢失
* ImageLoader: 实质上也是一种缓存,只不过存储的是图片数据。
  • Remote DataSource 包括
* Http/Https: 这个是大多数的远程数据来源
* WebSocket:  WebSocket一般用于后端主动下发消息
* Downloader/Uploader: 一般用于上传和下载大型的数据,比如图像或者文件等

在拿到数据后或者在存储数据之前一般会对数据做相应处理后交付给使用方:

常用的数据处理包括:

* 数据加解密: 一般在存储或者传输敏感数据的时候需要对数据进行一次加密,但是加密有时候会比较耗时,所以对于非关键数据一般不用加密传输。
* 数据压缩/解压缩:对于传输比较大的数据一般都会在传输前做压缩解压处理
* 序列化/反序列化:这个是最常见的数据处理方式,在拿到后台的数据后我们通常会对数据进行反序列化成一个对象后传递给上层进行处理,而在向服务端发送数据对象之前需要将数据进行序列化后进行传输。目前比较常用的序列化和反序列化方式有JSON以及Probuf,JSON的特点是比较直观,而Probuf是基于二进制的数据,所以数据量会比JSON小,但是调试的时候比较不直观。

在MVVM架构中和数据层关系最密切的就是Model层了,Model中一般都是一些获取数据的方法,在获取完数据后经过处理后放在Model层待使用方取用,Model层状态发生变化View层一般也会同步变化,比如开始获取数据的时候一般会呈现一个加载动画,数据拿到后会呈现一个用于显示数据的界面,获取数据失败会有错误信息界面,这里我们用一个ViewAction来向View层同步状态

2.2 视图层

视图层在各个项目中往往差异比较大,但是还是有可以提炼出的共同点:

* UI Components: 大多数项目中会用到列表,闪屏,Banner,按钮,圆角图片,弹窗,面板,Toast的界面相关的控件,可以将这些控件的公共部分给抽象出来,使用的时候通过配置就可以应用在不同项目中。
* Theme Manager: 主题管理包括图片,颜色,字体等多种资源管理,一般在主题发生切换的时候还需要往外部传递信号,供外界监听。
* LayoutKit : 布局库,目前比较流行的就是Masory了,除了它还有一系列基于Flex模式的布局库,比如Yoga,ComponentKit等,一般比较高效的布局库会带有异步计算布局参数,缓存布局参数的功能。
* Device UI Adapter: 随着越来越多的屏幕分辨率出现,适配也成为了视图层中一个很重要的工作,所以一个项目中一般会有视图适配工具类。

视图层的工作比较简单,IDLFundation 中会管理三种状态的视图分别是Loading View,Content View, Empty/Exception View,这三者的作用从名称上都可以看出来是干什么用的,它受ViewAction信号控制
如果说数据层是生产数据的,那么视图层的作用就是生产交互事件,在IDLFunation中会将事件通过RACCommand暴露出去,可以在ViewController中对其进行相关工作的绑定。
视图还有一个功能就是布局它的子控件。嗯,视图层大致就这么简单。

2.3 ViewController层

在IDLFundation中 ViewController层相当于MVVM的ViewModel层,它负责绑定View 和 Model层,同时处理View层生成的动作,它会监听一系列事件,比如主题更换,语言更换,网络状态切换,ViewController生命周期变化,通过感知这些事件触发对应的逻辑处理。

2.4 APM && Quality Center

介绍完IDLFundation最核心的MVVM框架后,仅次于它的一个部分就是APM && Quality Center 模块,在这里包含了一系列性能检测,以及异常收集,Logger上报,埋点数据上报等工具,这个模块用于对整个应用进行检测。所以个人将这个模块也归入核心框架层。

2.5 应用配置层

应用配置层会以两种形式对应用进行配置,一种是本地项目配置,一种是云指令控制,后者可以根据服务端下发的配置文件对应用的功能进行配置。

2.6 AppContext

这个一般同于存储一些全局的应用变量,比如登录后的登录态等,多个地方都需要使用,并且方便取用。

2.7 应用路由

这里指的是路由的框架层,一般一个路由可以分成两部分一部分是强应用相关的部分,包括应用定义的路由协议,以及对某个协议的处理。
另一部分是通用的部分,包括路由的分发匹配,路由策略等,这部分是可以抽出来对多个项目共用的。

2.8 Hot Fix

说到Hot Fix 大家一定会联想到JSPatch,但是其实Hot Fix 除了JSPath外还可以结合动态下发路由表来修复线上问题,这个后续有时间会向大家介绍下。

2.9 ABTest

目前一般项目都会使用到ABTest据说这种开发方式在字节跳动会比较常用,也就是说同一个功能客户端会同时开发几套效果,通过对用户进行不同的分组,对不同的用户使用不同的展示方式,ABTest比较强调公平性,不然很难说服产品们他们的策略谁优谁劣,
但是一般小型项目一个功能往往只有一个产品负责,这种ABTest往往比较不那么严格,只要随机分组就好。对于字节跳动那种模式,分组的公平性就很重要,不然产品们吵着吵着就会将矛头指向ABTest开发工程师。哈哈,毕竟KPI相关。

2.10 Web Componente

我把这个成为应用的动态化模块,主要包括基于JavaScripCore的 JS桥,以及React Native.这些是可以动态变化的,动态指的是同一个地址里面的内容可以随时变化而不用通过发布版本的方式。

2.11 AutoMation Tools

我们很经常在玩笑啥时候能给自动化需求,哈哈,估计等不到那一天吧,不过我们确实可以将开发中很大一部分工作通过自动化来完成,比如埋点key,参数,颜色表,字符表,资源索引等。这些脚本很容易提高我们的开发效率,并且不容易出错,个人感觉很值得。

2.12 Common Module

这部分就相对比较大的模块,一般我们会将这些常用模块公共部分抽象出来,通过在业务层配置,达到复用的目的。

3 组件化设计细节

在我们介绍完一个项目的大致结构后我们重新回到组件化这个问题,在组件化这个问题上没有固定的模式,需要依照项目的实际情况来确定,但是在设计组件化框架的时候有一些问题是必须要思考的:

* 如何划分组件/组件如何解耦
* 如何设计组件路由
* 组件化消息总线设计
* 组件集成
3.1 组件划分

关于组件划分我们需要思考如下几个问题:

* 划分组件需要遵循那些规则
* 如何能够做到某个组件单独拎出来在不修改的情况下在另一个项目中使用。
* 如何能够在只修改极少数代码的情况下将某个组件移除,并且主工程能够正常运行。
3.1.1 组件划分规则

一般我们在项目中会把项目划分成:业务模块,功能组件以及基础组件。

  • 业务模块

业务模块一般是一个具体的业务比如酒店业务模块,机票订阅业务模块,每个模块都是较大的一个需求,每一个模块都是由多个功能组件组合而成。通过调用不同的功能组件来获得相应的支持,数据从一个功能组件流向另一个功能组件,每个功能组件提供对应的服务,最终展现给用户。一般业务模块粒度比较粗,但是数量不宜太多。这一层往往变动比较频繁。一般会有单独的人来维护。

  • 功能组件

功能组件指的是从业务角度来看是一个相对独立的不可再分的部件,每个功能组件都能够提供某项独立的服务,比如搜索组件,比如用户信息管理组件。一般我们在开发过程中会不断从业务模块中去提取,一旦有较为稳定的可以归并为一个功能的逻辑出现的时候,我们就会将这部分逻辑封装后下沉到功能组件。

  • 基础组件

基础组件一般指的是和业务无关的,可以在多个项目中复用的,比如应用主题管理组件。网络组件等等。这个层的组件强调的是多项目的可复用性,所以对稳定性要求一般是最高的。一般考虑到它的可复用性在划分粒度上都会分得比较细。功能也会比较单一,基础层组件相互之间不应该产生任何依赖。

  • 资源组件

对于图片资源,可以单独剥出一个组件,这个组件中只存放图片文件不包含任何代码,这些图片可以通过Bundle,image Assets进行管理,并按不同业务模块建立不同的Bundle或者image Assets。

组件拆分原则:

对于组件的拆分,一般需要依据各个组件的特点,首先总体上应该去中心化,形成层级从上而下单向依赖,也就是说业务模块依赖功能组件,功能组件依赖基础组件。在发现有多个组件可以共用的逻辑或者数据模型的情况下,要考虑将这些公用的部分下沉,这种层级结构还有一种特点就是:越是上层的组件,越贴近业务,也就越不稳定,因此我们在划分的时候一般业务模块粒度会相对粗一些,功能组件划分粒度一般会细一点,而基础组件一般会考虑它的稳定性,只有十分必要的情况下才会考虑把它纳入到基础组件。对于项目中存在的公共资源和代码,应该将其下沉到下层。

总结:

在划分的过程中遵循:
1. 去中心化,组件结构层级化,依赖从上而下,平级组件无依赖。
2. 业务模块粗粒度,功能组件细粒度,基础组件强调高稳定性,高复用性。
3. 目标:独立模块组件可以编译运行,每个模块可随意插拔。
3.1.2 组件解耦

在组件化过程中,主要有两部分会产生耦合:一个是组件间调用,一个是组件间数据交付形式,这部分需要思考如下几个问题:

* 组件间通信选用何种形式才能降低耦合
* 组件间数据通信的时候数据怎么交付,以什么形式交付才不会带来耦合
* 如何交付非常规数据

关于组件解耦推荐大家阅读下bang的iOS 组件化方案探索,这篇博客很清晰地介绍了组件化组件间依赖的解耦过程。

组件间通信以及组件间数据交付形式是产生组件耦合的最根源原因。组件间通信一般有URL形式,class-protocals 形式,target-action形式。我们来看下理想的依赖模式是怎样的:

理想的依赖关系应该如上图所示,调用方依赖Mediator及对应模块的Mediator分类,但是被调用组件中不知道Mediator以及调用方的存在(只让其他模块对中间层产生耦合关系,中间层不对其他模块发生耦合),假定我们要移除被调用方直接移除就好,Mediator在没有找到被调用者的时候只是不响应而已不会对其他部分有任何影响,而被调用者由于不感知Mediator所以从项目中提取出来也不会对它有任何影响。对于调用者要从项目中移出只需要调整与Mediator相关的代码即可,甚至可以带着Mediator移出。到新环境中的时候只要对接Mediator接口即可。具体的实现方式可以看casatwy的iOS应用架构谈 组件化方案.在这种模式下,可以通过Mediator解决组件间头文件直接引用、依赖混乱的问题。需要注意的是Mediator应该只负责挂接节点的通信任务,不应该包含涉及具体业务的逻辑。也就是说Mediator中只应该负责找到响应方,将参数打包抛给响应方即可,至于具体的逻辑由调用方和响应方做具体的处理。

下面是CTMediator的大致结构:

还有一处可能会带来耦合的地方是组件间的数据交付形式,如果不用组件化的话我们一般在两个对象数据产生交付的时候一般会以一个具体的Model形式交付,但是应用组件化后这些Model存放在哪里就是个问题,模型类放在调用方和被调方的哪个组件都不太合适,因为以具体Model形式交付的化双方就都会依赖这个Model,这就导致必须将这个Model下沉。但是同一个Model可能是多个模块之间交付的数据形式,这就导致需要将所有的Model放置到一个单独的下层组件中去。也就是说每个层都会有一个Model层供上层使用,在这种情况下抽出某个组件都会有藕断丝连痛不欲生的感觉。可能大家会退而求其次在用到这个模型对象的所有组件中,都分别维护一份相同的模型类,或者各自维护不同结构的模型类,但是这样业务发生改变模型类就会很麻烦,这是不可取的。所以比较合理的方式是去Model化,以Dictionary形式进行交互,参数Dictionary在Mediator中封装。这样所有的变化都集中在Mediator中。这里需要注意的是组件内部还是可以使用Model的。

总结:

在解耦方面,主要有两部分会产生耦合:
* 一个是组件间调用,一个是组件间数据交付形式
* 前者可以借助runtime + target-action形式解决,后者采用去Model化Dictionary进行交付。
3.2 组件路由
3.2.1 组件路由的作用

路由是组件化中很重要的一部分,我们有时候会自嘲我们日常做的工作就是从一个界面点击后跳转到另一界面拉取数据后,再次点击后再重复上面的工作,这个侧面反应了路由跳转再整个应用中的重要程度,但是我们再从比较宽泛的角度来看,其实通过路由不一定只是解决在页面之间的跳转问题,它其实是一个资源的索引,通过它不但可以跳转页面,还可以获得某些资源数据。

3.2.2 评判组件路由的标准

一个组件路由设计得好还是不好往往会有一定地标准去评判,下面列出个人认为的一些标准:

  • 准确高效
    路由最基本的要求就是准确高效,如果这一点都不能保证那么这个路由一般不会在项目中采用。

  • 灵活跳转

在设计路由接口的时候一般需要思考如下几个点:

  • 如何对外提供远程访问的功能:
    这些接口包括:Web端访问的接口,其他App调用接口。

  • 如何在应用内部提供应用内模块(组件)间的页面之间访问的功能。

  • 如何统一同一个产品下不同端,不同页面形式(RN,Weex,H5,Native页面)之间的路由方式。

  • 如何通过路由访问资源

  • 如何通过路由进行模块调度,组件加载

  • 动态性

一般我们在设计路由的时候都会有个路由映射表,可能你会说target-action方式没有这种路由映射表,但实际上它的target以及action selector就是它的路由映射表,有了路由映射表的概念,我们就可以通过动态下发对应的路由映射表来实现路由的动态化,在跳转之前查询下如果有在路由映射表里面那么就按照表里面的进行跳转,如果不在那么就按照原样跳转,通过这种机制在跳转前进行拦截,替换,这有什么好处呢?比如我们线上某个页面有奔溃异常那么可以将这个界面的路由指定到一个统一的异常页面页面,或者用某个特定的H5页面作为降级页面,这样就可以达到简单的热修复的目的。

在这里需要补充一点:有了路由后我们可以在路由上做很多工作,除了上面提到了热修复策略,页面黑白名单策略,还可以在路由上进行统一跳转埋点,鉴权,配合灰度进行风控逻辑。

  • 安全性

跨应用时,需要注意注入攻击,做到敏感参数加密防篡改,同时需要注意路由层应提供能够实现风控的机制。
跨业务系统的时候,访问敏感页面或资源的时候需要通过Token等认证信息来实现路由层的身份认证。

下面是目前开源的一些比较好的路由开源代码,大家在设计过程中可以参照这些项目进行设计符合自己项目的路由器:

JLRoutes 5029 star
CTMediator 2638 star
MGJRouter 1946 star
routable-ios 1773 star
DeepLinkKit 3236 star
ABRouter 136 star
HHRouter 1574 star
FFRouter 157 star
STCRouter 19 star

上面的路由大体可以归为三类:URL形式,class-protocals 形式,target-action形式。个人比较偏向于target-action形式,关于target-action形式的组件化介绍可以看下iOS应用架构谈 组件化方案以及iOS 组件化方案探索。URL形式,class-protocals 形式大家可以看下蘑菇街 App 的组件化之路 蘑菇街 App 的组件化之路·续

下面我们分别简单对比下三种路由方式:

  • URL形式

这种方式是借鉴前端的路由思想,它将应用内任何资源与一个URL对应在一起。它最大的优点就是可以统一三端路由形式。具有天然的动态性,适合经常开展运营活动的应用,它的缺点也是比较致命的:这种方式不能在模块间传递非常规数据。所以一般会在多端路由的情况下或者暴露给外部应用调用的情况下使用这种形式。

  • Class-Protocals 形式

这种方式的优点是没有硬编码,但是它的缺点也比较明显处理不好会产生模块间耦合。

  • Target-Action形式

Target-Action方式的优点是无需注册,能够做到被调用模块不被依赖。但是它有个比较不足的地方就是在参数打包的时候会有硬编码,但是可以通过在Mediator中包装一层将这部分归并到Mediator统一处理。

4 组件化消息总线设计

下图是个人设计的一个组件化消息总线:

整个总线是集中式下发,消息中心包含了各个消息的注册信息,最主要包括某个事件的监听组件列表,每个eventType为key,value为当前事件的监听组件列表,另一个注册信息是某个事件的响应selector字符串,key为eventType,value为seletorString, 当某个模块需要发送消息的时候调用triggEvent方法,将要触发的事件以及要携带的参数传递到消息中心,消息中心通过查表找出哪些模块需要通知,然后再查着该事件的响应selector,通过performSeletor来调用监听模块的响应方法,从而达到通知消息的目的。

5 组件集成

在实际项目中一般会将每个组件都分割成一个单独工程,通过git统一管理,主工程通过Cocoapods集成各个组件,关于Cocoapods的时候大家可以看之前的博客,之前有专门拎出一篇博客进行介绍,但是除了Cocoapods之外还可以有其他的方式。一种是在项目中直接集成源码这种的优点是在主工程调试比较方便,可以看到组件的内部实现,另一种是framework,它的好处是可以加快编译速度,并且每个组件的代码是不可见的,对于比较机密敏感的模块可以选用这种方式。

6 组件化步骤

在项目相对稳定成型的时候我们就可以向项目引入组件化了,有了上面的介绍相信大家对整个组件化有了一定的了解了,作为文章的最后给出一个组件化的一个步骤供大家在项目实际开发过程中参考:

  • 6.1 项目模块划分

组件化过程不同人有不同的方式,有的人喜欢从下往上,先从细粒度的基本组件开始,然后再封装功能组件,而后才是业务模块,但是个人比较不推荐这种方式,这种方式比较容易陷入到细节中没有宏观的概念,建议找个纸和笔画出整个项目可以划分成哪些模块,这些模块就是我们的业务模块部分,然后在划分这部分模块的时候,再考虑这个模块有那些功能,一一穷举出来,这时候就大概知道有那些功能模块了,到了这一步就可以开始动手从代码层面上进行组件划分了,在划分过程中再把通用的部分下沉,形成一个一个基础组件。不断封装不断下沉公共部分。通过定义一些组件间接口,这些接口后续会在路由中实现,步骤完成后确认组件和其他部分代码没有耦合后就可以将这部分组件提交到组件库中进行管理。

基础层组件则在集成后直接依赖,例如资源文件和配置文件,这些都是直接在主工程或组件中使用的。第三方库则是通过功能组件层的业务封装,封装后由路由进行通信,业务组件层是直接依赖第三方库源码的。

  • 6.2 设计组件路由,进行组件解耦

个人建议在内部模块之间使用Target-Action方式,而对外接口或者有些需要H5跳转到客户端的采用URL形式(这主要考虑到H5跳客户端需要保持两端共同的跳转方式,而URL是这些之中唯一能够做到多端统一的路由方式)。
在开始解耦之前先列出上一步划分的组件之间的交互接口,然后通过组件路由来桥接这些接口,传递数据,这里数据的交付应该是去Model化的形式进行交付。

  • 6.3 组件消息梳理

梳理出各个组件哪个组件发出那些消息,哪些组件监听这些消息,都将这些信息注册到组件化消息总线中。

  • 6.4 组件测试

经过上述的组件化,需要我们对整个项目进行自测,首先从各个组件内部先进行测试,然后再扩展到组件间的接口部分进行测试,最后再对项目整体进行测试。

  • 6.5 新起项目如何做组件化

上面介绍的主要针对项目成型的情况下,通过组件化来重构项目,如果是新起的项目,我们会先将项目配置文件等集成到主工程中,做一些基础的项目配置,随后集成需要的组件。之后各个业务根据业务需求实现各个业务组件,一旦遇到需要依赖基础组件库中的服务的时候,就往主项目中引入基础组件库,遇到要从某个组件开发者那里获得某项服务的时候可以到接口需求后台提单,支持组件开发人员根据需求单来提供服务。在这个过程中可以通过Mock数据来并行开发,组件开发结束后,测试先介入对组件进行功能测试,在所有组件完成后再进行组件集成测试。

总结:
这篇文章是写得算是比较久的一篇博客,主要涵盖了:什么是组件化,组件化的优缺点,从项目结构角度分析了一个项目该有的部分,而后正式从如何划分组件,如何从路由角度,数据交付角度来对组件解耦,再接着介绍了路由的功能,三种常见的路由实现方式以及如何设计路由。如何设计消息总线,组件的集成。最后给出了组件化的大致步骤。相信通过上面的介绍结合给出的开源方案,大家都应该会对组件化有个进一步的理解。组件化是一个很强义务相关的话题,需要对自己的项目业务有比较深刻的理解才能设计出比较优雅的组件化框架,本文只是针对比较通用的一些方面进行总结,文章的最后结合手机淘宝的客户端架构探索之路来做个结尾,我自己的组件化也从无锋的这篇文章中借鉴了很多思想,所以想分享给大家,首先建议大家先通读原文,然后再看接下来的内容。

首先我们看下整个方案的大题结构图:

这里涉及到了一切皆Bundle(组件)的概念,整个方案是围绕Bundle展开的,分成三层,最顶层是一系列Bundle,第二层是总线层,用于数据的交付,事件的通知,资源的路由。最后一层则是容器层.
如果将容器看成一个OS,那么一个个Bundle就相当于一个个App一样,它是可被部署的单元,里面可以涉及UI的部分,服务的部分。Bundle对容器是没有依赖的,它只会依赖中间的总线层。容器有完全独立的三大职能:启动加载、生命周期管理、组件管理。其中生命周期管理,负责将容器的生命周期通知到各个Bundle,让Bundle能够感知这些生命周期事件,并作出响应。组件管理器负责添加、删除、替换。在启动的时候,会首先由容器完成整个应用的引导流程,容器的初始化,核心中间件的初始化,启动入口的Bundle(入口Bundle是可配置的,一旦被配置就表示用户点击图标后首先会看到的界面).

总线方面主要分成三大部分,UI总线,服务总线,消息总线:

UI总线和上述的组件路由类似,负责资源的选址,为了三端统一,使用了以跨平台统一的URL作为路由方式,这里比较有意思的是自动降级机制:

没有Bundle承载的URL,将自动以Web 容器加载。这样就可以实现,当一个新的业务到达手机当中的时候,当手机客户端还没有实现这个功能,就会自动以Web的形式把它运作出来。当想要保证轻量化的时候,可能会裁剪掉一些Bundle,这时没有了这些Bundle,这些功能将会自动以Web的方式替换掉,使得在整个流程里面它是完整的。体验可能会有一些差别,但是功能是不受影响的。

服务总线主要用于提供某项服务的总线,通过往总线传入数据后,经过某项服务处理后将数据传出。
消息总线这里的消息总线Android里面使用Broadcast,iOS使用NSNotification。个人比较偏向于使用该博客上面介绍的那种消息总线的方式来通知事件。这里对手淘的具体项目需求不是很清楚所以不做评判。但是这三种总线的划分十分值得借鉴的。

最后手淘在组件化架构的基础上,淘宝提出Bundle App的概念,可以通过已有组件,进行简单配置后就可以组成一个新的app出来。解决了多个应用业务复用的问题,防止重复开发同一业务或功能。

组件化相关的较好文章