iOS面试题iOS开发札记iOS开发高级

iOS 基于CTMediator组件化实践

2018-09-18  本文已影响408人  郭小弟

逆水行舟,不进则退

这段时间处于项目空档期,别提有多开心了(如果让老大看到我这样估计我会死的很惨),开心并不是因为懒,而是为终于有了可以自由翱翔的时间

最近看了一些组件化方面的文章,感触良多,今天主要是基于CTMediator组件化方案进行分享,大家如果觉得我理解的不对可以留言或者直接去看Caca写的 iOS应用架构谈 组件化方案

一:CTMediator源码

源码里面代码不是特别的多,大概就是200多行
先看一下.h文件

#import <UIKit/UIKit.h>

extern NSString * const kCTMediatorParamsKeySwiftTargetModuleName;

@interface CTMediator : NSObject

+ (instancetype)sharedInstance;

// 远程App调用入口
- (id)performActionWithUrl:(NSURL *)url completion:(void(^)(NSDictionary *info))completion;
// 本地组件调用入口
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget;
- (void)releaseCachedTargetWithTargetName:(NSString *)targetName;

@end

+ (instancetype)sharedInstance;:单例,返回CTMediator对象
performActionWithUrl:这个方法主要是用于远程APP调用,比如从A应用传递一个URL到B应用,在B应用的openURL方法中去处理url
performTarget : 本地组件调用,使用RunTime处理target和action,shouldCacheTarget是否对传入的target进行缓存
releaseCachedTargetWithTargetName:把传入的target从缓存中删除

接下来去.m文件中看看具体是怎么实现的
sharedInstance,这里就不多说了

- (id)performActionWithUrl:(NSURL *)url completion:(void (^)(NSDictionary *))completion
{
    NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
    NSString *urlString = [url query];
    for (NSString *param in [urlString componentsSeparatedByString:@"&"]) {
        NSArray *elts = [param componentsSeparatedByString:@"="];
        if([elts count] < 2) continue;
        [params setObject:[elts lastObject] forKey:[elts firstObject]];
    }
    
    // 这里这么写主要是出于安全考虑,防止黑客通过远程方式调用本地模块。这里的做法足以应对绝大多数场景,如果要求更加严苛,也可以做更加复杂的安全逻辑。
    NSString *actionName = [url.path stringByReplacingOccurrencesOfString:@"/" withString:@""];
    if ([actionName hasPrefix:@"native"]) {
        return @(NO);
    }
    
    // 这个demo针对URL的路由处理非常简单,就只是取对应的target名字和method名字,但这已经足以应对绝大部份需求。如果需要拓展,可以在这个方法调用之前加入完整的路由逻辑
    id result = [self performTarget:url.host action:actionName params:params shouldCacheTarget:NO];
    if (completion) {
        if (result) {
            completion(@{@"result":result});
        } else {
            completion(nil);
        }
    }
    return result;
}

这个方法主要是针对远程APP的互相调起,通过openURL实现APP之间的跳转,通过URL进行数据传递

image.png
一个完整的URL就像上图一样,上面的代码中,优先从URL中获取到query中的数据,然后进行遍历然后把对应的参数的key和value添加到字典中,然后从URL中取出actionName,也就是要调用的方法名,最后通过performTarget方法去实现方法的调用,根据返回值处理回调
- (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget
{
    NSString *swiftModuleName = params[kCTMediatorParamsKeySwiftTargetModuleName];
    
    // generate target
    NSString *targetClassString = nil;
    if (swiftModuleName.length > 0) {
        targetClassString = [NSString stringWithFormat:@"%@.Target_%@", swiftModuleName, targetName];
    } else {
        targetClassString = [NSString stringWithFormat:@"Target_%@", targetName];
    }
    NSObject *target = self.cachedTarget[targetClassString];
    if (target == nil) {
        Class targetClass = NSClassFromString(targetClassString);
        target = [[targetClass alloc] init];
    }

    // generate action
    NSString *actionString = [NSString stringWithFormat:@"Action_%@:", actionName];
    SEL action = NSSelectorFromString(actionString);
    
    if (target == nil) {
        // 这里是处理无响应请求的地方之一,这个demo做得比较简单,如果没有可以响应的target,就直接return了。实际开发过程中是可以事先给一个固定的target专门用于在这个时候顶上,然后处理这种请求的
        [self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
        return nil;
    }
    
    if (shouldCacheTarget) {
        self.cachedTarget[targetClassString] = target;
    }

    if ([target respondsToSelector:action]) {
        return [self safePerformAction:action target:target params:params];
    } else {
        // 这里是处理无响应请求的地方,如果无响应,则尝试调用对应target的notFound方法统一处理
        SEL action = NSSelectorFromString(@"notFound:");
        if ([target respondsToSelector:action]) {
            return [self safePerformAction:action target:target params:params];
        } else {
            // 这里也是处理无响应请求的地方,在notFound都没有的时候,这个demo是直接return了。实际开发过程中,可以用前面提到的固定的target顶上的。
            [self NoTargetActionResponseWithTargetString:targetClassString selectorString:actionString originParams:params];
            [self.cachedTarget removeObjectForKey:targetClassString];
            return nil;
        }
    }
}

根据传递的targetName在缓存中查找,没有找到就通过NSClassFromString获取这个类,如果tatget==nil进行错误处理,如果传入的shouldCacheTarget为YES就把target添加到集合中缓存起来,然后判断target是否可以响应传进来的方法,不能响应错误处理,可以响应就调用safePerformAction这个方法

- (id)safePerformAction:(SEL)action target:(NSObject *)target params:(NSDictionary *)params
{
    NSMethodSignature* methodSig = [target methodSignatureForSelector:action];
    if(methodSig == nil) {
        return nil;
    }
    const char* retType = [methodSig methodReturnType];

    if (strcmp(retType, @encode(void)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        [invocation setArgument:&params atIndex:2];
        [invocation setSelector:action];
        [invocation setTarget:target];
        [invocation invoke];
        return nil;
    }

    if (strcmp(retType, @encode(NSInteger)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        [invocation setArgument:&params atIndex:2];
        [invocation setSelector:action];
        [invocation setTarget:target];
        [invocation invoke];
        NSInteger result = 0;
        [invocation getReturnValue:&result];
        return @(result);
    }

    if (strcmp(retType, @encode(BOOL)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        [invocation setArgument:&params atIndex:2];
        [invocation setSelector:action];
        [invocation setTarget:target];
        [invocation invoke];
        BOOL result = 0;
        [invocation getReturnValue:&result];
        return @(result);
    }

    if (strcmp(retType, @encode(CGFloat)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        [invocation setArgument:&params atIndex:2];
        [invocation setSelector:action];
        [invocation setTarget:target];
        [invocation invoke];
        CGFloat result = 0;
        [invocation getReturnValue:&result];
        return @(result);
    }

    if (strcmp(retType, @encode(NSUInteger)) == 0) {
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSig];
        [invocation setArgument:&params atIndex:2];
        [invocation setSelector:action];
        [invocation setTarget:target];
        [invocation invoke];
        NSUInteger result = 0;
        [invocation getReturnValue:&result];
        return @(result);
    }

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    return [target performSelector:action withObject:params];
#pragma clang diagnostic pop
}

这段代码主要是判断返回值类型,如果是void,NSInteger,BOOL,CGFloat,NSUInteger就进行特殊处理,不是的话就直接返回performSelector的返回值类型

二:使用CTMediator实战

CTMediator笔者用的是cocopods进行的组件化管理,我这边用的是framework进行的

image.png

首页是一个单独的模块,按照原来的开发方式,如果把这个模块从项目中删除,肯定就会报错,因为项目中有几个地方是对首页进行引用的,如何才能做到删除它而对项目不产生影响呢?下面开始介绍我做了哪些操作:

分类里面的实现
NSString * const kCTMediatorTargetA = @"HomeVCAction";

NSString * const kCTMediatorActionNativFetchDetailViewController = @"nativeFetchDetailViewController";


@implementation CTMediator (CTMediatorModuleAActions)

- (UIViewController *)CTMediator_viewControllerForDetail:(NSDictionary *)dict
{
    UIViewController *viewController = [self performTarget:kCTMediatorTargetA
                                                    action:kCTMediatorActionNativFetchDetailViewController
                                                    params:dict
                                         shouldCacheTarget:NO
                                        ];
    if ([viewController isKindOfClass:[UIViewController class]]) {
        // view controller 交付出去之后,可以由外界选择是push还是present
        return viewController;
    } else {
        // 这里处理异常场景,具体如何处理取决于产品
        return [[UIViewController alloc] init];
    }
}

在整个过程中只有一处对CTMediator分类的引用,如果传递的参数错误或者找不到类,可以在CTMediator中进行统一处理,如果需要修改代码可以回到自己的framework中进行修改,修改完成后只需要把framework更新一下就可以了,,项目一天天的变的庞大起来,每次编译都会耗费很长的时间,对自己也是一种折磨,这样做可以大大的减少项目的编译时间了
这种通过Target-Action的组件化方案,我个人觉得挺好的,只是多了一些硬编码,但是方便各模块传值,使用URL路由跳转的话,传递对象就没那么简单了

大家有意见欢迎提出,帮助别人成长的同时,也是对自己的一次锤炼

上一篇下一篇

猜你喜欢

热点阅读