WKWebView的图片二维码使用

From: https://www.cnblogs.com/gongyuhonglou/p/8888487.html

(未验证, 看到此文,后续有需要求再看)

WKWebView的图片二维码使用:

1.长按手势识别二维码并保存
2.识别二维码跳转;不是链接显示内容点击网址跳转
3.解决url包含中文不能编码的问题
4.文字带链接网址,点击跳转
5.纯文本-文字html展示可拷贝,查询
6.解决html页面适配屏幕宽度的问题和保留源文件的格式
7.判断是web网页图片否存在二维码并进行识别

代码:

// 添加长按手势识别二维码

    [self WKWebViewHandleLongPress:_detailWebView];



    // 识别二维码跳转;不是链接显示内容点击网址跳转

    if ([self.m_url hasPrefix:@"http"]) {

//        NSString *urlStr = [self.m_url stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]];

        // 解决url包含中文不能编码的问题

        NSString *urlStr = (NSString *)CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault,(CFStringRef)self.m_url,(CFStringRef)@"!$&'()*+,-./:;=?@_~%#[]",NULL,kCFStringEncodingUTF8));

        NSURL *url = [NSURL URLWithString:urlStr];

        NSMutableURLRequest *request =[NSMutableURLRequest requestWithURL:url];

        [request setHTTPShouldHandleCookies:YES];

        [_detailWebView loadRequest:request];

    } else {

        self.topTitleLabel.text = @"扫描结果";

        QRCodeLabel = [[ZXLabel alloc]initWithFrame:CGRectMake(10, 10, SCREENWIDTH-20, 50) fontSize:16 text:@"" textColor:[UIColor colorWithHexString:@"#000000"] textAlignment:NSTextAlignmentLeft numberOfLines:0];

        [_detailWebView addSubview:QRCodeLabel];



        if ([self urlValidation:[NSString stringWithFormat:@"%@",self.m_url]]==YES) {//网址,点击跳转

            [self textColour];

            QRurlStr = [NSString stringWithFormat:@"http://%@",[NSString stringWithFormat:@"%@",self.m_url]];

        } else {



    // 文字html可拷贝,查询

    //            "<html><head><meta charset='UTF-8'><meta name='viewport content=initial-scale=1,maximum-scale=1, minimum-scale=1'><meta name='apple-mobile-web-app-capable' content='yes'><meta name='apple-mobile-web-app-status-bar-style' content='black'><meta name='format-detection' content='telephone=no'><title></title><style>*{margin: 0;padding: 0;}html,body {width: 100%;height: 100%;}p {width: 90%;font-size: 16px;color: #333;text-align: justify;}</style></head><body><p></p ></body></html>"



    //            @"<html><head><meta http-equiv='Content-Type' content='text/html; charset=utf-8'><meta name='viewport' content='width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no'><style>*{margin:5px;padding:0px;}</style><title></title></head<body><pre>%@</pre></body></html>"



    //            @"<html><head><meta charset='UTF-8'><meta name='viewport' content='initial-scale=1,maximum-scale=1, minimum-scale=1'><meta name='apple-mobile-web-app-capable' content='yes'><meta name='apple-mobile-web-app-status-bar-style' content='black'><meta name='format-detection' content='telephone=no'><title></title><style>*{margin: 0;padding: 0;box-sizing: border-box;}p {width: 6.4rem;padding: .2rem;font-size: .2rem;color: #333;text-align: justify;}</style></head><body><p>%@</p ></body><script>(function() { function getViewPort()  {if(document.compatMode == 'BackCompat') { return {width: document.body.clientWidth,height: document.body.clientHeight}; } else {return {width: document.documentElement.clientWidth,height: document.documentElement.clientHeight};} }function screenZoom() {var _obj = getViewPort(); var Width = _obj.width;var Height = _obj.height;if (Width>640) { Width = 640;}document.documentElement.style.fontSize = Width/6.4 + 'px';}screenZoom();window.onresize = function() {screenZoom();};})();</script></html>"

            // 解决html页面适配屏幕宽度的问题和保留源文件的格式

            QRurlStr = [NSString stringWithFormat:

                        @"<html><head><meta charset='UTF-8'><meta name='viewport' content='initial-scale=1,maximum-scale=1, minimum-scale=1'><meta name='apple-mobile-web-app-capable' content='yes'><meta name='apple-mobile-web-app-status-bar-style' content='black'><meta name='format-detection' content='telephone=no'><title></title><style>*{margin: 0;padding: 0;box-sizing: border-box;}pre {/* width: 6.4rem; */padding: 10px;font-size: 15px;color: #333;text-align: justify;white-space: pre-wrap; /*css-3*/white-space: -moz-pre-wrap; /*Mozilla,since1999*/ white-space: -pre-wrap; /*Opera4-6*/white-space: -o-pre-wrap; /*Opera7*/word-wrap: break-word; /*InternetExplorer5.5+*/ }</style></head><body><pre>%@</pre></body><script>(function() { function getViewPort()  {if(document.compatMode == 'BackCompat') { return {width: document.body.clientWidth,height: document.body.clientHeight}; } else {return {width: document.documentElement.clientWidth,height: document.documentElement.clientHeight};} }function screenZoom() {var _obj = getViewPort(); var Width = _obj.width;var Height = _obj.height;if (Width>640) { Width = 640;}document.documentElement.style.fontSize = Width/6.4 + 'px';}screenZoom();window.onresize = function() {screenZoom();};})();</script></html>",[NSString stringWithFormat:@"%@",self.m_url]];

            [_detailWebView loadHTMLString:QRurlStr  baseURL:Nil];

        }

    }





#pragma mark -- 识别图中二维码

// app内部识别二维码

/**

 *  网址正则验证

 *

 *  @param string 要验证的字符串

 *

 *  @return 返回值类型为BOOL

 */

- (BOOL)urlValidation:(NSString *)string {

    NSError *error;

    NSString *regulaStr = @"((http[s]{0,1}|ftp)://[a-zA-Z0-9\\.\\-]+\\.([a-zA-Z]{2,4})(:\\d+)?(/[a-zA-Z0-9\\.\\-~!@#$%^&*+?:_/=<>]*)?)|(www.[a-zA-Z0-9\\.\\-]+\\.([a-zA-Z]{2,4})(:\\d+)?(/[a-zA-Z0-9\\.\\-~!@#$%^&*+?:_/=<>]*)?)|^(?=^.{3,255}$)[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+$";

    NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:regulaStr options:NSRegularExpressionCaseInsensitive error:&error];

    NSArray *arrayOfAllMatches = [regex matchesInString:string options:0 range:NSMakeRange(0, [string length])];

    for (NSTextCheckingResult *match in arrayOfAllMatches){

        NSString* substringForMatch = [string substringWithRange:match.range];

        NSLog(@"匹配--%@",substringForMatch);

        return YES;

    }

    return NO;

}



- (void)textColour {

    NSMutableAttributedString *abs = [[NSMutableAttributedString alloc]initWithString:[NSString stringWithFormat:@"%@",self.m_url]];

    [abs beginEditing];

    //字体大小

    //        [abs addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:20.0] range:NSMakeRange(0, 2)];

    //字体颜色

    [abs addAttribute:NSForegroundColorAttributeName value:[UIColor blueColor]  range:NSMakeRange(0, [NSString stringWithFormat:@"%@",self.m_url].length)];

    //下划线

    [abs addAttribute:NSUnderlineStyleAttributeName  value:@(NSUnderlineStyleSingle) range:NSMakeRange(0, [NSString stringWithFormat:@"%@",self.m_url].length)];

    QRCodeLabel.attributedText = abs;

    UITapGestureRecognizer *LabelTap = [[UITapGestureRecognizer alloc]initWithTarget:self action:@selector(capchaBtn:)];

    QRCodeLabel.userInteractionEnabled = YES;

    [QRCodeLabel addGestureRecognizer:LabelTap];

}



// 链接跳转

- (void)capchaBtn:(UITapGestureRecognizer *)sendr{

    NSLog(@"跳转网页~~");

    [QRCodeLabel removeFromSuperview];

    NSString *urlStr = (NSString *)CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault,(CFStringRef)QRurlStr,(CFStringRef)@"!$&'()*+,-./:;=?@_~%#[]",NULL,kCFStringEncodingUTF8));

    NSURL *url =[NSURL URLWithString:urlStr];

    NSMutableURLRequest *request =[NSMutableURLRequest requestWithURL:url];

    [request setHTTPShouldHandleCookies:YES];

    [_detailWebView loadRequest:request];

}







#pragma mark -- common WKWebView

-(void)WKWebViewHandleLongPress:(WKWebView *)webView {

    UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(webViewHandleLongPress:)];

    longPress.minimumPressDuration = 0.2;

    longPress.delegate = self;

    m_webView = webView;

    [webView addGestureRecognizer:longPress];

}



- (void)webkitTouchCallout:(WKWebView *)webView

{

    // 不执行前段界面弹出列表的JS代码

    [webView evaluateJavaScript:@"document.documentElement.style.webkitTouchCallout='none';" completionHandler:nil];

    [webView evaluateJavaScript:@"document.documentElement.style.webkitUserSelect='none'" completionHandler:nil];

}



// 是否允许支持多个手势,默认是不支持:NO

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer{

    return YES;

}



// 网页内长按识别二维码

- (void)webViewHandleLongPress:(UILongPressGestureRecognizer *)sender{

    if (sender.state == UIGestureRecognizerStateBegan) {



        CGPoint touchPoint = [sender locationInView:m_webView];

        // 获取长按位置对应的图片url的JS代码

        NSString *imgJS = [NSString stringWithFormat:@"document.elementFromPoint(%f, %f).src", touchPoint.x, touchPoint.y];

        // 执行对应的JS代码 获取url

        [m_webView evaluateJavaScript:imgJS completionHandler:^(id _Nullable imgUrl, NSError * _Nullable error) {

            if (imgUrl) {

                NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:imgUrl]];

                UIImage *image = [UIImage imageWithData:data];

                if (!image) {

                    NSLog(@"读取图片失败");

                    return;

                }

                _saveImage = image;



                // 禁用选中效果

                [self webkitTouchCallout:m_webView];



                if ([self isAvailableQRcodeIn:image]) {

                    [self filterPopViewWithTag:100002 WithTitleArray:[NSMutableArray arrayWithObjects:@"保存图片",@"识别图中二维码",nil]];

                } else {

                    [self filterPopViewWithTag:100001 WithTitleArray:[NSMutableArray arrayWithObjects:@"保存图片",nil]];

                }



            } else {

                // 选中效果

                [m_webView evaluateJavaScript:@"document.documentElement.style.webkitUserSelect='text'" completionHandler:nil];

                [m_webView evaluateJavaScript:@"document.documentElement.style.webkitTouchCallout='text'" completionHandler:nil];

            }

        }];

    }

}



#pragma mark -- RomAlertViewDelegate 弹框识别图中二维码

// 判断是web网页图片否存在二维码

- (BOOL)isAvailableQRcodeIn:(UIImage *)img {



    //方法:一

//    UIGraphicsBeginImageContextWithOptions(img.size, NO, 3);//0,获取当前屏幕分辨率[UIScreen mainScreen].scale

//    CGContextRef context = UIGraphicsGetCurrentContext();

//    [self.view.layer renderInContext:context];

//    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();

//    UIGraphicsEndImageContext();



    //方法:二

    UIImage *image = [self snapshot:self.view];



    //方法:三

//    UIImage *image = [self imageByInsetEdge:UIEdgeInsetsMake(-20, -20, -20, -20) withColor:[UIColor lightGrayColor] withImage:img];





    CIImage *ciImage = [[CIImage alloc] initWithCGImage:image.CGImage options:nil];

    CIContext *ciContext = [CIContext contextWithOptions:@{kCIContextUseSoftwareRenderer : @(YES)}]; // 软件渲染

    CIDetector *detector = [CIDetector detectorOfType:CIDetectorTypeQRCode context:ciContext options:@{CIDetectorAccuracy : CIDetectorAccuracyHigh}];// 二维码识别



    NSArray *features = [detector featuresInImage:ciImage];

    if (features.count > 0) {

        //        for (CIQRCodeFeature *feature in features) {

        //        NSLog(@"qrCodeUrl = %@",feature.messageString); // 打印二维码中的信息

        //        qrCodeUrl = feature.messageString;

        //    }

        CIQRCodeFeature *feature = [features objectAtIndex:0];

        qrCodeUrl = [feature.messageString copy];

        NSLog(@"二维码信息:%@", qrCodeUrl);

        return YES;

    } else {

        NSLog(@"图片中没有二维码");

        return NO;

    }

}



// you can also implement by UIView category

- (UIImage *)snapshot:(UIView *)view

{

    UIGraphicsBeginImageContextWithOptions(view.bounds.size, NO, 3);//view.bounds.size, YES, view.window.screen.scale



    if ([view respondsToSelector:@selector(drawViewHierarchyInRect:afterScreenUpdates:)]) {

        [view drawViewHierarchyInRect:view.bounds afterScreenUpdates:YES];

    }

    UIImage* image = UIGraphicsGetImageFromCurrentImageContext();



    UIGraphicsEndImageContext();



    return image;

}

// you can also implement by UIImage category

- (UIImage *)imageByInsetEdge:(UIEdgeInsets)insets withColor:(UIColor *)color withImage:(UIImage *)image

{

    CGSize size = image.size;

    size.width -= insets.left + insets.right;

    size.height -= insets.top + insets.bottom;

    if (size.width <= 0 || size.height <= 0) {

        return nil;

    }

    CGRect rect = CGRectMake(-insets.left, -insets.top, image.size.width, image.size.height);

    UIGraphicsBeginImageContextWithOptions(size, NO, image.scale);

    CGContextRef context = UIGraphicsGetCurrentContext();

    if (color) {

        CGContextSetFillColorWithColor(context, color.CGColor);

        CGMutablePathRef path = CGPathCreateMutable();

        CGPathAddRect(path, NULL, CGRectMake(0, 0, size.width, size.height));

        CGPathAddRect(path, NULL, rect);

        CGContextAddPath(context, path);

        CGContextEOFillPath(context);

        CGPathRelease(path);

    }

    [image drawInRect:rect];

    UIImage *insetEdgedImage = UIGraphicsGetImageFromCurrentImageContext();

    UIGraphicsEndImageContext();



    return insetEdgedImage;

}



// 网页内部识别二维码

- (void)alertview:(RomAlertView *)alertview didSelectWebRowAtIndexPath:(NSIndexPath *)indexPath

{

    if (alertview.tag == 100001) {

        if ([alertview.otherTitles[indexPath.row]  isEqualToString:@"保存图片"]) {

            NSLog(@"保存图片");

//            UIImageWriteToSavedPhotosAlbum(_saveImage, self, @selector(image:didFinishSavingWithError:contextInfo:), nil);

            [self saveWebLongPressed];



        }

    } else if (alertview.tag == 100002) {

        if ([alertview.otherTitles[indexPath.row]  isEqualToString:@"保存图片"]) {

            NSLog(@"保存图片");

//            UIImageWriteToSavedPhotosAlbum(_saveImage, self, @selector(image:didFinishSavingWithError:contextInfo:), nil);

            [self saveWebLongPressed];



        }else if ([alertview.otherTitles[indexPath.row] isEqualToString:@"识别图中二维码"]){

            NSLog(@"识别图中二维码");



            ADWebViewViewController *controller = [[ADWebViewViewController alloc] init];

            controller.m_url = qrCodeUrl;

            controller.hidesBottomBarWhenPushed = YES;

            [self.navigationController pushViewController:controller animated:YES];

        }

    }

}



//- (void)image:(UIImage *)image didFinishSavingWithError:(NSError *)error contextInfo:(void *)contextInfo{

//    NSString *message = @"Succeed";

//    if (error) {

//        message = @"Fail";

//    }

//    NSLog(@"save result :%@", message);

//}



#pragma mark --web保存图片

//保存

- (void)saveWebLongPressed {

//    if (webPhotoSave == YES) { // 图片已经保存到相册 提示

//        [self.view makeToast:@"该图片已经保存到相册" duration:2 position:CSToastPositionCenter];

//        return;

//    }

    [self saveWebPhoto];

}



- (void)saveWebPhoto {



    PHAuthorizationStatus oldStatus = [PHPhotoLibrary authorizationStatus];

    [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {

        dispatch_async(dispatch_get_main_queue(), ^{

            switch (status) {

                case PHAuthorizationStatusAuthorized: {

                    //  保存图片到相册

                    [self saveWebImageIntoAlbum];

                    break;

                }

                case PHAuthorizationStatusDenied: {

                    if (oldStatus == PHAuthorizationStatusNotDetermined) return;

                    NSLog(@"提醒用户打开相册的访问开关");

                    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"无法保存"        message:@"请在iPhone的“设置-隐私-照片”选项中,允许访问你的照片。" delegate:self  cancelButtonTitle:@"确定" otherButtonTitles:nil];

                    [alert show];

                    break;

                }

                case PHAuthorizationStatusRestricted: {

                    UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"无法保存"        message:@"因系统原因,无法访问相册!" delegate:self  cancelButtonTitle:@"确定" otherButtonTitles:nil];

                    [alert show];

                    break;

                }

                default:

                    break;

            }

        });

    }];

}





// 获得刚才添加到【相机胶卷】中的图片

- (PHFetchResult<PHAsset *> *)createdAssets {



    __block NSString *createdAssetId = nil;

    // 添加图片到【相机胶卷】

    [[PHPhotoLibrary sharedPhotoLibrary] performChangesAndWait:^{

        createdAssetId = [PHAssetChangeRequest creationRequestForAssetFromImage:_saveImage].placeholderForCreatedAsset.localIdentifier;

    } error:nil];

    if (createdAssetId == nil) return nil;

    // 在保存完毕后取出图片

    return [PHAsset fetchAssetsWithLocalIdentifiers:@[createdAssetId] options:nil];

}



//获得【自定义相册】

-(PHAssetCollection *)createdCollection {

    // 获取软件的名字作为相册的标题(如果需求不是要软件名称作为相册名字就可以自己把这里改成想要的名称)

    NSString *title = [NSBundle mainBundle].infoDictionary[(NSString *)kCFBundleNameKey];

    // 获得所有的自定义相册

    PHFetchResult<PHAssetCollection *> *collections = [PHAssetCollection fetchAssetCollectionsWithType:PHAssetCollectionTypeAlbum subtype:PHAssetCollectionSubtypeAlbumRegular options:nil];

    for (PHAssetCollection *collection in collections) {

        if ([collection.localizedTitle isEqualToString:title]) {

            return collection;

        }

    }

    // 代码执行到这里,说明还没有自定义相册

    __block NSString *createdCollectionId = nil;

    // 创建一个新的相册

    [[PHPhotoLibrary sharedPhotoLibrary] performChangesAndWait:^{

        createdCollectionId = [PHAssetCollectionChangeRequest creationRequestForAssetCollectionWithTitle:title].placeholderForCreatedAssetCollection.localIdentifier;

    } error:nil];

    if (createdCollectionId == nil) return nil;

    // 创建完毕后再取出相册

    return [PHAssetCollection fetchAssetCollectionsWithLocalIdentifiers:@[createdCollectionId] options:nil].firstObject;

}



//保存图片到相册

- (void)saveWebImageIntoAlbum {

    // 获得相片

    PHFetchResult<PHAsset *> *createdAssets = self.createdAssets;

    // 获得相册

    PHAssetCollection *createdCollection = self.createdCollection;

    if (createdAssets == nil || createdCollection == nil) {



        [self.view makeToast:@"图片保存失败!" duration:2 position:CSToastPositionCenter];

        return;

    }

    // 将相片添加到相册

    NSError *error = nil;

    [[PHPhotoLibrary sharedPhotoLibrary] performChangesAndWait:^{

        PHAssetCollectionChangeRequest *request = [PHAssetCollectionChangeRequest changeRequestForAssetCollection:createdCollection];

        [request insertAssets:createdAssets atIndexes:[NSIndexSet indexSetWithIndex:0]];

    } error:&error];

    // 保存结果

    NSString *msg = nil ;

    if(error){

        msg = @"图片保存失败!";

        [self.view makeToast:msg duration:2 position:CSToastPositionCenter];

    }else{

        msg = @"已成功保存到系统相册";

//        webPhotoSave = YES;

        [self.view makeToast:msg duration:2 position:CSToastPositionCenter];

    }

}

PS:WKWebView官网

iOS应用架构谈 组件化方案1

转自 CTMediator的组件化方案, 个人觉得相对入手不难,老项目可接入, 项目侵入性小

From: https://casatwy.com/iOS-Modulization.html

iOS应用架构谈 开篇
iOS应用架构谈 view层的组织和调用方案
iOS应用架构谈 网络层设计方案
iOS应用架构谈 本地持久化方案及动态部署
iOS应用架构谈 组件化方案

前几天的一个晚上在infoQ的微信群里,来自蘑菇街的Limboy做了一个分享,讲了蘑菇街的组件化之路。我不认为这条组件化之路蘑菇街走对了。分享后我私聊了Limboy,Limboy似乎也明白了问题所在,我答应他我会把我的方案写成文章,于是这篇文章就出来了。

另外,按道理说组件化方案也属于iOS应用架构谈的一部分,但是当初构思架构谈时,我没打算写组件化方案,因为我忘了还有这回事儿。。。后来写到view的时候才想起来,所以在view的那篇文章最后补了一点内容。而且觉得这个组件化方案太简单,包括实现组件化方案的组件也很简单,代码算上注释也才100行,我就偷懒放过了,毕竟写一篇文章好累的啊。

本文的组件化方案demo在这里https://github.com/casatwy/CTMediator 拉下来后记得pod install 拉下来后记得pod install 拉下来后记得pod install,这个Demo对业务敏感的边界情况处理比较简单,这需要根据不同App的特性和不同产品的需求才能做,所以只是为了说明组件化架构用的。如果要应用在实际场景中的话,可以根据代码里给出的注释稍加修改,就能用了。

蘑菇街的原文地址在这里:《蘑菇街 App 的组件化之路》,没有耐心看完原文的朋友,我在这里简要介绍一下蘑菇街的组件化是怎么做的:

  1. App启动时实例化各组件模块,然后这些组件向ModuleManager注册Url,有些时候不需要实例化,使用class注册。
  2. 当组件A需要调用组件B时,向ModuleManager传递URL,参数跟随URL以GET方式传递,类似openURL。然后由ModuleManager负责调度组件B,最后完成任务。

这里的两步中,每一步都存在问题。

第一步的问题在于,在组件化的过程中,注册URL并不是充分必要条件,组件是不需要向组件管理器注册Url的。而且注册了Url之后,会造成不必要的内存常驻,如果只是注册Class,内存常驻量就小一点,如果是注册实例,内存常驻量就大了。至于蘑菇街注册的是Class还是实例,Limboy分享时没有说,文章里我也没看出来,也有可能是我看漏了。不过这还并不能算是致命错误,只能算是小缺陷。

真正的致命错误在第二步。在iOS领域里,一定是组件化的中间件为openUrl提供服务,而不是openUrl方式为组件化提供服务。

什么意思呢?

也就是说,一个App的组件化方案一定不是建立在URL上的,openURL的跨App调用是可以建立在组件化方案上的。当然,如果App还没有组件化,openURL方式也是可以建立的,就是丑陋一点而已。

为什么这么说?

因为组件化方案的实施过程中,需要处理的问题的复杂度,以及拆解、调度业务的过程的复杂度比较大,单纯以openURL的方式是无法胜任让一个App去实施组件化架构的。如果在给App实施组件化方案的过程中是基于openURL的方案的话,有一个致命缺陷:非常规对象无法参与本地组件间调度。关于非常规对象我会在详细讲解组件化方案时有一个辨析。

实际App场景下,如果本地组件间采用GET方式的URL调用,就会产生两个问题:

  • 根本无法表达非常规对象

比如你要调用一个图片编辑模块,不能传递UIImage到对应的模块上去的话,这是一个很悲催的事情。 当然,这可以通过给方法新开一个参数,然后传递过去来解决。比如原来是:

[a openUrl:"http://casa.com/detail?id=123&type=0"];

同时就也要提供这样的方法:

[a openUrl:"http://casa.com/detail" params:@{
    @"id":"123",
    @"type":"0",
    @"image":[UIImage imageNamed:@"test"]
}]

如果不像上面这么做,复杂参数和非常规参数就无法传递。如果这么做了,那么事实上这就是拆分远程调用和本地调用的入口了,这就变成了我文章中提倡的做法,也是蘑菇街方案没有做到的地方。

另外,在本地调用中使用URL的方式其实是不必要的,如果业务工程师在本地间调度时需要给出URL,那么就不可避免要提供params,在调用时要提供哪些params是业务工程师很容易懵逼的地方。。。在文章下半部分给出的demo代码样例已经说明了业务工程师在本地间调用时,是不需要知道URL的,而且demo代码样例也阐释了如何解决业务工程师遇到传params容易懵逼的问题。

  • URL注册对于实施组件化方案是完全不必要的,且通过URL注册的方式形成的组件化方案,拓展性和可维护性都会被打折

注册URL的目的其实是一个服务发现的过程,在iOS领域中,服务发现的方式是不需要通过主动注册的,使用runtime就可以了。另外,注册部分的代码的维护是一个相对麻烦的事情,每一次支持新调用时,都要去维护一次注册列表。如果有调用被弃用了,是经常会忘记删项目的。runtime由于不存在注册过程,那就也不会产生维护的操作,维护成本就降低了。

由于通过runtime做到了服务的自动发现,拓展调用接口的任务就仅在于各自的模块,任何一次新接口添加,新业务添加,都不必去主工程做操作,十分透明。

小总结

蘑菇街采用了openURL的方式来进行App的组件化是一个错误的做法,使用注册的方式发现服务是一个不必要的做法。而且这方案还有其它问题,随着下文对组件化方案介绍的展开,相信各位自然心里有数。

先来看一下方案的架构图

             --------------------------------------
             | [CTMediator sharedInstance]        |
             |                                    |
             |                openUrl:       <<<<<<<<<  (AppDelegate)  <<<<  Call From Other App With URL
             |                                    |
             |                   |                |
             |                   |                |
             |                   |/               |
             |                                    |
             |                parseUrl            |
             |                                    |
             |                   |                |
             |                   |                |
.................................|...............................
             |                   |                |
             |                   |                |
             |                   |/               |
             |                                    |
             |  performTarget:action:params: <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<  Call From Native Module
             |                                    |
             |                   |                |
             |                   |                |
             |                   |                |
             |                   |/               |
             |                                    |
             |             -------------          |
             |             |           |          |
             |             |  runtime  |          |
             |             |           |          |
             |             -------------          |
             |               .       .            |
             ---------------.---------.------------
                           .           .
                          .             .
                         .               .
                        .                 .
                       .                   .
                      .                     .
                     .                       .
                    .                         .
-------------------.-----------      ----------.---------------------
|                 .           |      |          .                   |
|                .            |      |           .                  |
|               .             |      |            .                 |
|              .              |      |             .                |
|                             |      |                              |
|           Target            |      |           Target             |
|                             |      |                              |
|         /   |   \           |      |         /   |   \            |
|        /    |    \          |      |        /    |    \           |
|                             |      |                              |
|   Action Action Action ...  |      |   Action Action Action ...   |
|                             |      |                              |
|                             |      |                              |
|                             |      |                              |
|Business A                   |      | Business B                   |
-------------------------------      --------------------------------

这幅图是组件化方案的一个简化版架构描述,主要是基于Mediator模式和Target-Action模式,中间采用了runtime来完成调用。这套组件化方案将远程应用调用和本地应用调用做了拆分,而且是由本地应用调用为远程应用调用提供服务,与蘑菇街方案正好相反。

调用方式

先说本地应用调用,本地组件A在某处调用[[CTMediator sharedInstance] performTarget:targetName action:actionName params:@{...}]CTMediator发起跨组件调用,CTMediator根据获得的target和action信息,通过objective-C的runtime转化生成target实例以及对应的action选择子,然后最终调用到目标业务提供的逻辑,完成需求。

在远程应用调用中,远程应用通过openURL的方式,由iOS系统根据info.plist里的scheme配置找到可以响应URL的应用(在当前我们讨论的上下文中,这就是你自己的应用),应用通过AppDelegate接收到URL之后,调用CTMediatoropenUrl:方法将接收到的URL信息传入。当然,CTMediator也可以用openUrl:options:的方式顺便把随之而来的option也接收,这取决于你本地业务执行逻辑时的充要条件是否包含option数据。传入URL之后,CTMediator通过解析URL,将请求路由到对应的target和action,随后的过程就变成了上面说过的本地应用调用的过程了,最终完成响应。

针对请求的路由操作很少会采用本地文件记录路由表的方式,服务端经常处理这种业务,在服务端领域基本上都是通过正则表达式来做路由解析。App中做路由解析可以做得简单点,制定URL规范就也能完成,最简单的方式就是scheme://target/action这种,简单做个字符串处理就能把target和action信息从URL中提取出来了。

组件仅通过Action暴露可调用接口

所有组件都通过组件自带的Target-Action来响应,也就是说,模块与模块之间的接口被固化在了Target-Action这一层,避免了实施组件化的改造过程中,对Business的侵入,同时也提高了组件化接口的可维护性。

         --------------------------------
         |                              |
         |           Business A         |
         |                              |
         ---  ----------  ----------  ---
           |  |        |  |        |  |
           |  |        |  |        |  |
...........|  |........|  |........|  |...........
.          |  |        |  |        |  |          .
.          |  |        |  |        |  |          .
.        ---  ---    ---  ---    ---  ---        .
.        |      |    |      |    |      |        .
.        |action|    |action|    |action|        .
.        |      |    |      |    |      |        .
.        ---|----    -----|--    --|-----        .
.           |             |        |             .
.           |             |        |             .
.       ----|------     --|--------|--           .
.       |         |     |            |           .
.       |Target_A1|     |  Target_A2 |           .
.       |         |     |            |           .
.       -----------     --------------           .
.                                                .
.                                                .
..................................................

大家可以看到,虚线圈起来的地方就是用于跨组件调用的target和action,这种方式避免了由BusinessA直接提供组件间调用会增加的复杂度,而且任何组件如果想要对外提供调用服务,直接挂上target和action就可以了,业务本身在大多数场景下去进行组件化改造时,是基本不用动的。

复杂参数和非常规参数,以及组件化相关设计思路

这里我们需要针对术语做一个理解上的统一:

复杂参数是指由普通类型的数据组成的多层级参数。在本文中,我们定义只要是能够被json解析的类型就都是普通类型,包括NSNumber, NSString, NSArray, NSDictionary,以及相关衍生类型,比如来自系统的NSMutableArray或者你自己定义的都算。

总结一下就是:在本文讨论的场景中,复杂参数的定义是由普通类型组成的具有复杂结构的参数。普通类型的定义就是指能够被json解析的类型。

非常规参数是指由普通类型以外的类型组成的参数,例如UIImage等这些不能够被json解析的类型。然后这些类型组成的参数在文中就被定义为非常规参数

总结一下就是:非常规参数是包含非常规类型的参数。非常规类型的定义就是不能被json解析的类型都叫非常规类型。

边界情况:

  • 假设多层级参数中有存在任何一个内容是非常规参数,本文中这种参数就也被认为是非常规参数。
  • 如果某个类型当前不能够被json解析,但通过某种转化方式能够转化成json,那么这种类型在场景上下文中,我们也称为普通类型。

举个例子就是通过json描述的自定义view。如果这个view能够通过某个组件被转化成json,那么即使这个view本身并不是普通类型,在具有转化器的上下文场景中,我们依旧认为它是普通类型。

  • 如果上下文场景中没有转化器,这个view就是非常规类型了。
  • 假设转化出的json不能够被还原成view,比如组件A有转化器,组件B中没有转化器,因此在组件间调用过程中json在B组件里不能被还原成view。在这种调用方向中,只要调用者能将非常规类型转化成json的,我们就依然认为这个view是普通类型。如果调用者是组件A,转化器在组件B中,A传递view参数时是没办法转化成json的,那么这个view就被认为是非常规类型,哪怕它在组件B中能够被转化成json。

然后我来解释一下为什么应该由本地组件间调用来支持远程应用调用:

在远程App调用时,远程App是不可能通过URL来提供非常规参数的,最多只能以json string的方式经过URLEncode之后再通过GET来提供复杂参数,然后再在本地组件中解析json,最终完成调用。在组件间调用时,通过performTarget:action:params:是能够提供非常规参数的,于是我们可以知道,远程App调用时的上下文环境以及功能是本地组件间调用时上下文环境以及功能的子集

因此这个逻辑注定了必须由本地组件间调用来为远程App调用来提供服务,只有符合这个逻辑的设计思路才是正确的组件化方案的设计思路,其他跟这个不一致的思路一定就是错的。因为逻辑上子集为父集提供服务说不通,所以强行这么做的话,用一个成语来总结就叫做倒行逆施。

另外,远程App调用和本地组件间调用必须要拆分开,远程App调用只能走CTMediator提供的专用远程的方法,本地组件间调用只能走CTMediator提供的专用本地的方法,两者不能通过同一个接口来调用。

这里有两个原因:

  • 远程App调用处理入参的过程比本地多了一个URL解析的过程,这是远程App调用特有的过程。这一点我前面说过,这里我就不细说了。

  • 架构师没有充要条件条件可以认为远程App调用对于无响应请求的处理方式和本地组件间调用无响应请求的处理方式在未来产品的演进过程中是一致的

在远程App调用中,用户通过url进入app,当app无法为这个url提供服务时,常见的办法是展示一个所谓的404界面,告诉用户”当前没有相对应的内容,不过你可以在app里别的地方再逛逛”。这个场景多见于用户使用的App版本不一致。比如有一个URL只有1.1版本的app能完整响应,1.0版本的app虽然能被唤起,但是无法完成整个响应过程,那么1.0的app就要展示一个404了。

在组件间调用中,如果遇到了无法响应的请求,就要分两种场景考虑了。

场景1

如果这种无法响应的请求发生场景是在开发过程中,比如两个组件同时在开发,组件A调用组件B时,组件B还处于旧版本没有发布新版本,因此响应不了,那么这时候的处理方式可以相对随意,只要能体现B模块是旧版本就行了,最后在RC阶段统测时是一定能够发现的,只要App没发版,怎么处理都来得及。

场景2

如果这种无法响应的请求发生场景是在已发布的App中,有可能展示个404就结束了,那这就跟远程App调用时的404处理场景一样。但也有可能需要为此做一些额外的事情,有可能因为做了额外的事情,就不展示404了,展示别的页面了,这一切取决于产品经理。

那么这种场景是如何发生的呢?

我举一个例子:当用户在1.0版本时收藏了一个东西,然后用户升级App到1.1版本。1.0版本的收藏项目在本地持久层存入的数据有可能是会跟1.1版本收藏时存入的数据是不一致的。此时用户在1.1版本的app中对1.0版本收藏的东西做了一些操作,触发了本地组件间调用,这个本地间调用又与收藏项目本身的数据相关,那么这时这个调用就是有可能变成无响应调用,此时的处理方式就不见得跟以前一样展示个404页面就结束了,因为用户已经看到了收藏了的东西,结果你还告诉他找不到,用户立刻懵逼。。。这时候的处理方式就会用很多种,至于产品经理会选择哪种,你作为架构师是没有办法预测的。如果产品经理提的需求落实到架构上,对调用入口产生要求然而你的架构又没有拆分调用入口,对于你的选择就只有两个:要么打回产品需求,要么加个班去拆分调用入口。

当然,架构师可以选择打回产品经理的需求,最终挑选一个自己的架构能够承载的需求。但是,如果这种是因为你早期设计架构时挖的坑而打回的产品需求,你不觉得丢脸么?

鉴于远程app调用和本地组件间调用下的无响应请求处理方式不同,以及未来不可知的产品演进,拆分远程app调用入口和本地组件间调用入口是功在当代利在千秋的事情。


组件化方案中的去model设计

组件间调用时,是需要针对参数做去model化的。如果组件间调用不对参数做去model化的设计,就会导致业务形式上被组件化了,实质上依然没有被独立

假设模块A和模块B之间采用model化的方案去调用,那么调用方法时传递的参数就会是一个对象。

如果对象不是一个面向接口的通用对象,那么mediator的参数处理就会非常复杂,因为要区分不同的对象类型。如果mediator不处理参数,直接将对象以范型的方式转交给模块B,那么模块B必然要包含对象类型的声明。假设对象声明放在模块A,那么B和A之间的组件化只是个形式主义。如果对象类型声明放在mediator,那么对于B而言,就不得不依赖mediator。但是,大家可以从上面的架构图中看到,对于响应请求的模块而言,依赖mediator并不是必要条件,因此这种依赖是完全不需要的,这种依赖的存在对于架构整体而言,是一种污染。

如果参数是一个面向接口的对象,那么mediator对于这种参数的处理其实就没必要了,更多的是直接转给响应方的模块。而且接口的定义就不可能放在发起方的模块中了,只能放在mediator中。响应方如果要完成响应,就也必须要依赖mediator,然而前面我已经说过,响应方对于mediator的依赖是不必要的,因此参数其实也并不适合以面向接口的对象的方式去传递。

因此,使用对象化的参数无论是否面向接口,带来的结果就是业务模块形式上是被组件化了,但实质上依然没有被独立。

在这种跨模块场景中,参数最好还是以去model化的方式去传递,在iOS的开发中,就是以字典的方式去传递。这样就能够做到只有调用方依赖mediator,而响应方不需要依赖mediator。然而在去model化的实践中,由于这种方式自由度太大,我们至少需要保证调用方生成的参数能够被响应方理解,然而在组件化场景中,限制去model化方案的自由度的手段,相比于网络层和持久层更加容易得多。

因为组件化天然具备了限制手段:参数不对就无法调用!无法调用时直接debug就能很快找到原因。所以接下来要解决的去model化方案的另一个问题就是:如何提高开发效率。

在去model的组件化方案中,影响效率的点有两个:调用方如何知道接收方需要哪些key的参数?调用方如何知道有哪些target可以被调用?其实后面的那个问题不管是不是去model的方案,都会遇到。为什么放在一起说,因为我接下来要说的解决方案可以把这两个问题一起解决。

解决方案就是使用category

mediator这个repo维护了若干个针对mediator的category,每一个对应一个target,每个category里的方法对应了这个target下所有可能的调用场景,这样调用者在包含mediator的时候,自动获得了所有可用的target-action,无论是调用还是参数传递,都非常方便。接下来我要解释一下为什么是category而不是其他:

  • category本身就是一种组合模式,根据不同的分类提供不同的方法,此时每一个组件就是一个分类,因此把每个组件可以支持的调用用category封装是很合理的。

  • 在category的方法中可以做到参数的验证,在架构中对于保证参数安全是很有必要的。当参数不对时,category就提供了补救的入口。

  • category可以很轻松地做请求转发,如果不采用category,请求转发逻辑就非常难做了。

  • category统一了所有的组件间调用入口,因此无论是在调试还是源码阅读上,都为工程师提供了极大的方便。

  • 由于category统一了所有的调用入口,使得在跨模块调用时,对于param的hardcode在整个App中的作用域仅存在于category中,在这种场景下的hardcode就已经变成和调用宏或者调用声明没有任何区别了,因此是可以接受的。

这里是业务方使用category调用时的场景,大家可以看到非常方便,不用去记URL也不用纠结到底应该传哪些参数。

if (indexPath.row == 0) {
    UIViewController *viewController = [[CTMediator sharedInstance] CTMediator_viewControllerForDetail];

    // 获得view controller之后,在这种场景下,到底push还是present,其实是要由使用者决定的,mediator只要给出view controller的实例就好了
    [self presentViewController:viewController animated:YES completion:nil];
}

if (indexPath.row == 1) {
    UIViewController *viewController = [[CTMediator sharedInstance] CTMediator_viewControllerForDetail];
    [self.navigationController pushViewController:viewController animated:YES];
}

if (indexPath.row == 2) {
    // 这种场景下,很明显是需要被present的,所以不必返回实例,mediator直接present了
    [[CTMediator sharedInstance] CTMediator_presentImage:[UIImage imageNamed:@"image"]];
}

if (indexPath.row == 3) {
    // 这种场景下,参数有问题,因此需要在流程中做好处理
    [[CTMediator sharedInstance] CTMediator_presentImage:nil];
}

if (indexPath.row == 4) {
    [[CTMediator sharedInstance] CTMediator_showAlertWithMessage:@"casa" cancelAction:nil confirmAction:^(NSDictionary *info) {
        // 做你想做的事
        NSLog(@"%@", info);
    }];
}

本文对应的demo展示了如何使用category来实现去model的组件调用。上面的代码片段也是摘自这个demo。


基于安全考虑

我们需要防止黑客通过URL的方式调用本属于native的组件,比如支付宝的个人财产页面。如果在调用层级上没有区分好,没有做好安全措施,黑客就有通过safari查看任何人的个人财产的可能。

安全措施其实有很多,大部分取决于App本身以及产品的要求。在架构层面要做的最基础的一点就是区分调用是来自于远程App还是本地组件,我在demo中的安全措施是采用给action添加native前缀去做的,凡是带有native前缀的就都只允许本地组件调用,如果在url阶段发现调用了前缀为native的方法,那就可以采取响应措施了。这也是将远程app调用入口和本地组件调用入口区分开来的重要原因之一。

当然,为了确保安全的做法有很多,但只要拆出远程调用和本地调用,各种做法就都有施展的空间了。

基于动态调度考虑

动态调度的意思就是,今天我可能这个跳转是要展示A页面,但是明天可能同样的跳转就要去展示B页面了。这个跳转有可能是来自于本地组件间跳转也有可能是来自于远程app。

做这个事情的切点在本文架构中,有很多个:

  1. 以url parse为切点
  2. 以实例化target时为切点
  3. 以category调度方法为切点
  4. 以target下的action为切点

如果以url parse为切点的话,那么这个动态调度就只能够对远程App跳转产生影响,失去了动态调度本地跳转的能力,因此是不适合的。

如果以实例化target时为切点的话,就需要在代码中针对所有target都做一次审查,看是否要被调度,这是没必要的。假设10个调用请求中,只有1个要被动态调度,那么就必须要审查10次,只有那1次审查通过了,才走动态调度,这是一种相对比较粗暴的方法。

如果以category调度方法为切点的话,那动态调度就只能影响到本地件组件的跳转,因为category是只有本地才用的,所以也不适合。

以target下的action为切点是最适合的,因为动态调度在一般场景下都是有范围的,大多数是活动页需要动态调度,今天这个活动明天那个活动,或者今天活动正在进行明天活动就结束了,所以产生动态调度的需求。我们在可能产生动态调度的action中审查当前action是否需要被动态调度,在常规调度中就没必要审查了,例如个人主页的跳转,商品详情的跳转等,这样效率就能比较高。

大家会发现,如果要做类似这种效率更高的动态调度,target-action层被抽象出来就是必不可少的,然而蘑菇街并没有抽象出target-action层,这也是其中的一个问题。

当然,如果你的产品要求所有页面都是存在动态调度需求的,那就还是以实例化target时为切点去调度了,这样能做到审查每一次调度请求,从而实现动态调度。

说完了调度切点,接下来要说的就是如何完成审查流程。完整的审查流程有几种,我每个都列举一下:

  1. App启动时下载调度列表,或者定期下载调度列表。然后审查时检查当前action是否存在要被动态调度跳转的action,如果存在,则跳转到另一个action
  2. 每一次到达新的action时,以action为参数调用API获知是否需要被跳转,如果需要被跳转,则API告知要跳转的action,然后再跳转到API指定的action

这两种做法其实都可以,如果产品对即时性的要求比较高,那么采用第二种方案,如果产品对即时性要求不那么高,第一种方案就可以了。由于本文的方案是没有URL注册列表的,因此服务器只要给出原始target-action和对应跳转的target-action就可以了,整个流程不是只有注册URL列表才能达成的,而且这种方案比注册URL列表要更易于维护一些。

另外,说采用url rewrite的手段来进行动态调度,也不是不可以。但是这里我需要辨析的是,URL的必要性仅仅体现在远程App调度中,是没必要蔓延到本地组件间调用的。这样,当我们做远程App的URL路由时(目前的demo没有提供URL路由功能,但是提供了URL路由操作的接入点,可以根据业务需求插入这个功能),要关心的事情就能少很多,可以比较干净。在这种场景下,单纯以URL rewrite的方式其实就与上文提到的以url parse为切点没有区别了。

  • 蘑菇街没有拆分远程调用和本地间调用

不拆分远程调用和本地间调用,就使得后续很多手段难以实施,这个我在前文中都已经有论述了。另外再补充一下,这里的拆分不是针对来源做拆分。比如通过URL来区分是远程App调用还是本地调用,这只是区分了调用者的来源。

这里说的区分是指:远程调用走远程调用路径,也就是openUrl->urlParse->perform->target-action。本地组件间调用就走本地组件间调用路径:perform->target-action。这两个是一定要作区分的,蘑菇街方案并没有对此做好区分。

  • 蘑菇街以远程调用的方式为本地间调用提供服务

这是本末倒置的做法,倒行逆施导致的是未来架构难以为业务发展提供支撑。因为前面已经论述过,在iOS场景下,远程调用的实现是本地调用实现的子集,只有大的为小提供服务,也就是本地调用为远程调用提供服务,如果反过来就是倒行逆施了。

  • 蘑菇街的本地间调用无法传递非常规参数,复杂参数的传递方式非常丑陋

注意这里复杂参数非常规参数的辨析。

由于采用远程调用的方式执行本地调用,在前面已经论述过两者功能集的关系,因此这种做法无法满足传递非常规参数的需求。而且如果基于这种方式不变的话,复杂参数的传递也只能依靠经过urlencode的json string进行,这种方式非常丑陋,而且也不便于调试。

  • 蘑菇街必须要在app启动时注册URL响应者

这个条件在组件化方案中是不必要条件,demo也已经证实了这一点。这个不必要的操作会导致不必要的维护成本,如果单纯从只要完成业务就好的角度出发,这倒不是什么大问题。这就看架构师对自己是不是要求严格了。

  • 新增组件化的调用路径时,蘑菇街的操作相对复杂

在本文给出的组件化方案中,响应者唯一要做的事情就是提供Target和Action,并不需要再做其它的事情。蘑菇街除此之外还要再做很多额外不必要措施,才能保证调用成功。

  • 蘑菇街没有针对target层做封装

这种做法使得所有的跨组件调用请求直接hit到业务模块,业务模块必然因此变得臃肿难以维护,属于侵入式架构。应该将原本属于调用相应的部分拿出来放在target-action中,才能尽可能保证不将无关代码侵入到原有业务组件中,才能保证业务组件未来的迁移和修改不受组件调用的影响,以及降低为项目的组件化实施而带来的时间成本。

本文提供的组件化方案是采用Mediator模式和苹果体系下的Target-Action模式设计的。

然而这款方案有一个很小的缺陷在于对param的key的hardcode,这是为了达到最大限度的解耦和灵活度而做的权衡。在我的网络层架构和持久层架构中,都没有hardcode的场景,这也从另一个侧面说明了组件化架构的特殊性。

权衡时,考虑到这部分hardcode的影响域仅仅存在于mediator的category中。在这种情况下,hardcode对于调用者的调用是完全透明的。对于响应者而言,处理方式等价于对API返回的参数的处理方式,且响应者的处理方式也被限制在了Action中

因此这部分的hardcode的存在虽然确实有点不干净,但是相比于这些不干净而带来的其他好处而言,在权衡时是可以接受的,如果不采用hardcode,那势必就会导致请求响应方也需要依赖mediator,然而这在逻辑上是不必要的。另外,在我的各个项目的实际使用过程中,这部分hardcode是没有影响的。

另外要谈的是,之所以会在组件化方案中出现harcode,而网络层和持久层的去model化都没有发生hardcode情况,是因为组件化调用的所有接受者和调用者都在同一片上下文里。网络层有一方在服务端,持久层有一方在数据库。再加上设计时针对hardcode部分的改进手段其实已经超出了语言本身的限制。也就是说,harcode受限于语言本身。objective-C也好,swift也好,它们的接口设计哲学是存在缺陷的。如果我们假设在golang的背景下,是完全可以用golang的接口体系去做一个最优美的架构方案出来的。不过这已经不属于本文的讨论范围了,有兴趣的同学可以去了解一下相关知识。架构设计有时就是这么无奈。

组件化方案在App业务稳定,且规模(业务规模和开发团队规模)增长初期去实施非常重要,它助于将复杂App分而治之,也有助于多人大型团队的协同开发。但组件化方案不适合在业务不稳定的情况下过早实施,至少要等产品已经经过MVP阶段时才适合实施组件化。因为业务不稳定意味着链路不稳定,在不稳定的链路上实施组件化会导致将来主业务产生变化时,全局性模块调度和重构会变得相对复杂。

当决定要实施组件化方案时,对于组件化方案的架构设计优劣直接影响到架构体系能否长远地支持未来业务的发展,对App的组件化不只是仅仅的拆代码和跨业务调页面,还要考虑复杂和非常规业务参数参与的调度,非页面的跨组件功能调度,组件调度安全保障,组件间解耦,新旧业务的调用接口修改等问题。

蘑菇街的组件化方案只实现了跨业务页面调用的需求,本质上只实现了我在view层架构的文章中跨业务页面调用的内容,这还没有到成为组件化方案的程度,且蘑菇街的组件化方案距离真正的App组件化的要求还是差了一段距离的,且存在设计逻辑缺陷,希望蘑菇街能够加紧重构,打造真正的组件化方案。

没想到limboy如此迅速地发文回应了。文章地址在这里:蘑菇街 App 的组件化之路 续。然后我花了一些时间重新看了limboy的第一篇文章。我觉得在本文开头我对蘑菇街的组件化方案描述过于简略了,而且我还忽略了原来是有ModuleManager的,所以在这里我重新描述一番。

蘑菇街是以两种方式来做跨组件操作的

第一种是通过MGJRouterregisterURLPattern:toHandler:进行注册,将URL和block绑定。这个方法前面一个参数传递的是URL,例如mgj://detail?id=:id这种,后面的toHandler:传递的是一个^(NSDictionary *routerParameters){// 此处可以做任何事}的block。

当组件执行[MGJRouter openURL:@"mgj://detail?id=404"]时,根据之前registerURLPattern:toHandler:的信息,找到之前通过toHandler:收集的block,然后将URL中带的GET参数,此处是id=404,传入block中执行。如果在block中执行NSLog(routerParameters)的话,就会看到@{@"id":@"404"},因此block中的业务就能够得到执行。

然后为了业务方能够不生写URL,蘑菇街列出了一系列宏或者字符串常量(具体是宏还是字符串我就不是很确定,没看过源码,但limboy文章中有提到通过一个后台系统生成一个装满URL的源码文件)来表征URL。在openURL时,无论是远程应用调用还是本地组件间调用,只要传递的参数不复杂,就都会采用openURL的方式去唤起页面,因为复杂的参数和非常规参数这种调用方式就无法支持了。

缺陷在于:这种注册的方式其实是不必要的,而且还白白使用URLblock占用了内存。另外还有一个问题就是,即便是简单参数的传递,如果参数比较多,业务工程师不看原始URL字符串是无法知道要传递哪些参数的。

蘑菇街之所以采用id=:id的方式,我猜是为了怕业务工程师传递多个参数顺序不同会导致问题,而使用的占位符。这种做法在持久层生成sql字符串时比较常见。不过这个功能我没在limboy的文章中看到有写,不知道实现了没有。

在本文提供的组件化方案中,因为没有注册,所以就没有内存的问题。因为通过category提供接口调用,就没有参数的问题。对于蘑菇街来说,这种做法其实并没有做到拆分远程应用调用和本地组件间调用的目的,而不拆分会导致的问题我在文章中已经论述过了,这里就不多说了。


由于前面openURL的方式不能够传递非常规参数,因此有了第二种注册方式:新开了一个对象叫做ModuleManager,提供了一个registerClass:forProtocol:的方法,在应用启动时,各组件都会有一个专门的ModuleEntry被唤起,然后ModuleEntry@protocolClass进行配对。因此ModuleManager中就有了一个字典来记录这个配对。

当有涉及非常规参数的调用时,业务方就不会去使用[MGJRouter openURL:@"mgj://detail?id=404"]的方案了,转而采用ModuleManagerclassForProtocol:方法。业务传入一个@protocolModuleManager,然后ModuleManager通过之前注册过的字典查找到对应的Class返回给业务方,然后业务方再自己执行allocinit方法得到一个符合刚才传入@protocol的对象,然后再执行相应的逻辑。

这里的ModuleManager其实跟之前的MGJRouter一样,是没有任何必要去注册协议和类名的。而且无论是服务提供者调用registerClass:forProtocol:也好,服务的调用者调用classForProtocol:,都必须依赖于同一个protocol。蘑菇街把所有的protocol放入了一个publicProtocol.h的文件中,因此调用方和响应方都必须依赖于同一个文件。这个我在文章中也论述过:响应方在提供服务的时候,是不需要依赖任何人的。


所以针对蘑菇街的这篇文章我是这么回应的:

  • 蘑菇街所谓分开了远程应用调用和本地组件调用是不成立的,蘑菇街分开的只是普通参数调用非常规参数调用。不去区分远程应用调用和本地组件间调用的缺陷我在文中已经论述过了,这里不多说。
  • 蘑菇街确实不只有openURL方式,还提供了ModuleManager方式,然而所谓的我们其实是分为「组件间调用」和「页面间跳转」两个维度,只要 app 响应某个 URL,无论是 app 内还是 app 外都可以,而「组件间」调用走的完全是另一条路,所以也不会有安全上的问题。其实也是不成立的,因为openURL方式也出现在了本地组件间调用中,这在他第一篇文章里的组件间通信小节中就已经说了采用openURL方式调用了,这是有可能产生安全问题的。而且这段话也承认了openURL方式被用于本地组件间调用,又印证了我刚才说的第一点。
  • 根据上面两点,蘑菇街在openURL场景下,还是出现了以远程调用的方式为本地间调用提供服务的问题,这个问题我也已经在文中论述过了。
  • 蘑菇街在本地间调用同时采用了openURL方案和protocol - class方案,所以其实之前我指出蘑菇街本地间调用不能传递非常规参数和复杂参数是不对的,应该是蘑菇街在本地间调用时如果是普通参数,那就采用openURL,如果是非常规参数,那就采用protocol - class了,这个做法对于本地间调用的管理和维护,显而易见是不利的。。。
  • limboy说必须要在 app 启动时注册 URL 响应者这步不可避免,但没有说原因。我的demo已经证实了注册是不必要的,所以我想听听limboy如何解释原因。
  • 你的架构图画错了

mgj

按照你的方案来看,红圈的地方是不可能没有依赖的。。。

另外,limboy也对本文方案提出了一些看法:

认为category在某种意义上也是一个注册过程。

蘑菇街的注册和我这里的category其实是两回事,而且我无论如何也无法理解把category和注册URL等价联系的逻辑😂

一个很简单的事实就可以证明两者完全不等价了:我的方案如果没有category,照样可以跑,就是业务方调用丑陋一点。蘑菇街如果不注册URL,整个流程就跑不起来了~

认为openURL的好处是可以更少地关心业务逻辑,本文方案的好处是可以很方便地完成参数传递。

我没觉得本文方案关心的业务逻辑比openURL更多,因为两者比较起来,都是传参数发调用请求,在关心业务逻辑的条件下,两者完全一样。唯一的不同就是,我能传非常规参数而openURL不能。本文方案的整个过程中,在调用者这一方是完全没有涉及到任何属于响应者的业务逻辑的。

认为protocol/URL注册将target-action抽象出调用接口是等价的

这其实只是效果等价了,两者真正的区别在于:protocol对业务产生了侵入,且不符合黑盒模型。

  • 我来解释一下protocol侵入业务的原因

由于业务中的某个对象需要被调用,因此必须要符合某个可被调用的protocol,然而这个protocol又不存在于当前业务领域,于是当前业务就不得不依赖publicProtocol。这对于将来的业务迁移是有非常大的影响的。

  • 另外再解释一下为什么不符合黑盒模型

蘑菇街的protocol方式使对象要在调用者处使用,由于调用者并不包含对象原本所处的业务领域,当完成任务需要多个这样的对象的时候,就需要多次通过protocol获得class来实例化多个对象,最终才能完成需求。

但是target-action模式保证了在执行组件间调用的响应时,执行的上下文处于响应者环境中,这跟蘑菇街的protocol方案相比就是最大的差别。因为从黑盒理论上讲,调用者只管发起请求,请求的执行应该由响应者来负责,因此执行逻辑必须存在于响应者的上下文内,而不能存在于调用者的上下文内。

举个具体一点的例子就是,当你发起了一个网页请求,后端取好数据渲染好页面,无论获取数据涉及多少渠道,获取数据的逻辑都在服务端完成,然后再返回给浏览器展示。这个是正确的做法,target-action模式也是这么做的。

但是蘑菇街的方案就变成了这样:你发起了一个网络请求,后端返回的不是数据,返回的竟然是一个数据获取对象(DAO),然后你再通过DAO去取数据,去渲染页面,如果渲染页面的过程涉及多个DAO,那么你还要再发起更多请求,拿到的还是DAO,然后再拿这个DAO去获取数据,然后渲染页面。这是一种非常诡异的做法。。。

如果说这么做是为了应对执行业务的过程中,需要根据中间阶段的返回值来决定接下来的逻辑走向的话,那也应该是多次调用获得数据,然后决定接下来的业务走向,而不是每次拿到的都是DAO啊。。。使用target-action方式来应对这种场景其实也很自然啊~

所以综上所述,蘑菇街的方案是存在很大问题的,希望蘑菇街继续改正

ios本地通知记录

From: https://developer.aliyun.com/article/760903

简介: 简单介绍iOS的本地通知推送的基本使用步骤

iOS10以前本地通知(UILocalNotification)

使用步骤:

  1. 创建一个UILocalNotification对象
  2. 设置触发时间及标题、内容
  3. 注册并安排通知

    // 1. 创建一个UILocalNotification对象
    let localNotification = UILocalNotification()

    // 2. 设置触发时间及标题、内容
    localNotification.fireDate = Date(timeIntervalSinceNow: 3)
    localNotification.alertTitle = “Title”
    localNotification.alertBody = “alertBodyalertBodyalertBodyalertBody”

    // 0. 注册通知(一般在程序刚启动时注册通知)
    UIApplication.shared.registerUserNotificationSettings(UIUserNotificationSettings(types: [.badge, .alert, .sound], categories: nil))

    // 3. 安排通知
    UIApplication.shared.scheduleLocalNotification(localNotification)

  • UILocalNotification的其他属性

    • applicationIconBadgeNumber :应用程序图标上的数字标记
    • repeatInterval :重复间隔(按照年、月、日、时、分重复)
    • soundName :发出通知时的提示音,使用UILocalNotificationDefaultSoundName或者指定的音频文件名
    • userInfo :与通知相关的额外的字典,用户在通知上看不到此数据

应用程序处理收到的通知

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // ......

    // 点击通知启动程序(程序不在前台也不在后台,即程序退出时),在此可获取被点击的通知并处理
    if let localNotification = launchOptions?[.localNotification] {
        print(localNotification)
    }

    return true
}

// 应用程序收到通知时,在此方法中处理收到的通知
func application(_ application: UIApplication, didReceive notification: UILocalNotification) {
    print(notification)
}

iOS10+使用通知请求(UNNotificationRequest)创建本地通知

使用步骤

  1. 请求授权
  2. 创建通知内容
  3. 创建通知触发时间
  4. 使用唯一标识字符串、内容、触发器创建通知请求
  5. 将通知请求加到通知中心

    // 1. 创建通知内容
    let content = UNMutableNotificationContent()
    // 标题
    content.title = NSString.localizedUserNotificationString(forKey: “Hello!”, arguments: nil)
    // 内容
    content.body = NSString.localizedUserNotificationString(forKey: “Hello_message_body”, arguments: nil)
    // 通知提示音
    content.sound = .default

    // 2. 创建通知触发器
    // Deliver the notification in five seconds.
    let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 5, repeats: false)

// 3. 使用唯一标识字符串、内容、触发器创建通知请求
let uuidString = UUID().uuidString
let request = UNNotificationRequest(identifier: uuidString, content: content, trigger: trigger)


// 获取当前程序的通知中心
let notificationCenter = UNUserNotificationCenter.current()
// 设置代理,用来处理收到的通知
notificationCenter.delegate = self
// 0. 请求授权(一般在程序刚启动时请求通知授权)
notificationCenter.requestAuthorization(options: [.alert, .badge, .sound]) { (granted, error) in

}

// 4. 将通知请求加到通知中心
notificationCenter.add(request) { (error) in
    if error != nil {
       // Handle any errors.
    }
}
  • UNMutableNotificationContent 的其他常用属性

    • subtitle :子标题
    • badge :应用程序图标上的数字标记
    • userInfo :与通知相关的额外的字典,用户在通知上看不到此数据
  • UNNotificationTrigger 常见的通知触发器

    • UNTimeIntervalNotificationTrigger : 几秒后触发,如果要设置可重复触发需要大于60
    • UNCalendarNotificationTrigger :某年某月某日某天某时某分某秒触发
    • UNLocationNotificationTrigger :在某个位置触发
  • 处理接收到的通知(使用UNUserNotificationCenterDelegate中的两个方法)

    // Asks the delegate to process the user’s response to a delivered notification.
    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {

    // 处理代码
    ......
    completionHandler()
    

    }

    // 应用程序运行在前台时,此方法处理收到的通知
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {

    // 处理代码
    ......
    completionHandler(.sound)
    

    }

iOS 13使用LaunchScreen.storyboard适配各尺寸启动图

From: https://www.lizenghai.com/archives/43641.html

目录

  • 背景
  • 方案一:图片直接拉伸
  • 方案二:autolayout 排版内容
  • 方案三:还是使用多图片适配
    • 添加图片
    • 设置启动图
  • 最后

背景

苹果在2019年WWDC中提出,到2020年4月开始,使用 iOS 13 SDK 的 App 必须使用 LaunchScreen,相应的LuanchImage 也要退出历史的舞台。但是现在苹果手机尺寸也越来越丰富了,很难找到一种适配各种尺寸启动图的方式。下面我就根据网上的例子与自己研究给出几种方案。

方案一:图片直接拉伸

1、直接在 LaunchScreen.storyboard 中添加 UIImageView,autolayout 设置边距都为0。

2、 设置图片的的 Content Mode 为 Aspect Fill。

3、然后直接添加默认尺寸的图片就可以了。

这种做法简单粗暴,直接对图片进行拉伸适配了,对于启动图来说如果四边空白位置比较多也是没有问题。或者根据自己的图片通过设置 stretching 设置各个方向的拉伸的位置大小,具体方式大家自行研究了。

方案二:autolayout 排版内容

这种方式就不要在把启动页面看成是一张图片了,直接把它当成一个页面,放进我们需要显示的控件,使用 autolayout 正常布局适配。

这是个人最为推荐的一种方式,能够更加灵活的适配各尺寸。

方案三:还是使用多图片适配

这种方式本质上跟使用 LaunchImage 好像没什么区别,但是我们在嫌麻烦的或者不得不使用多启动图的时候,这也不失为一种解决方式。

添加图片

1、在 Assets.xcassets 中 New Image Set

2、把所有尺寸图片添加进图片文件夹下

3、进入图片文件夹中,编辑 Contents.json,把里面的内容改成如下内容:

{
  "images" : [
    {
      "idiom" : "iphone",
      "scale" : "1x"
    },
    {
      "idiom" : "iphone",
      "filename" : "640x960.png",
      "scale" : "2x"
    },
    {
      "idiom" : "iphone",
      "scale" : "3x"
    },
    {
      "idiom" : "iphone",
      "subtype" : "retina4",
      "scale" : "1x"
    },
    {
      "idiom" : "iphone",
      "filename" : "640x1136.png",
      "subtype" : "retina4",
      "scale" : "2x"
    },
    {
      "idiom" : "iphone",
      "subtype" : "retina4",
      "scale" : "3x"
    },
    {
      "idiom" : "iphone",
      "filename" : "1242x2208.png",
      "subtype" : "736h",
      "scale" : "3x"
    },
    {
      "idiom" : "iphone",
      "filename" : "750x1334.png",
      "subtype" : "667h",
      "scale" : "2x"
    },
    {
      "idiom" : "iphone",
      "filename" : "1125x2436.png",
      "subtype" : "2436h",
      "scale" : "3x"
    },
    {
      "idiom" : "iphone",
      "filename" : "1242x2688.png",
      "subtype" : "2688h",
      "scale" : "3x"
    },
    {
      "idiom" : "iphone",
      "filename" : "828x1792.png",
      "subtype" : "1792h",
      "scale" : "2x"
    }
  ],
  "info" : {
    "version" : 1,
    "author" : "xcode"
  }
}

保存之后变成如下所示:

设置启动图

1、在 LaunchScreen.storyboard 中直接添加 UIImageView,autolayout 设置填满整个屏幕。

2、图片资源直接引用刚才添加的图片

3、设置后你会发现上面显示的是3.5寸的图片,这不用理会它,直接运行起来,你会发现能够不同尺寸加载不同启动图。

以上这个方式是参考了NinJaLife同学的这篇文章

这方式需要注意一个问题:如果你的项目有多个 target 需要配置多套启动图,就需要建立多个 LaunchScreen.storyboard,并注意导入的多套启动图名称不能相同,会导致各个 target 的启动图都加载不出来。

最后

以上就是我总结的几种可行的方式,用哪种方式就仁者见仁智者见智了,如果大家还有什么更好的方案,欢迎一起讨论。

https://juejin.im/post/5e1463d4f265da5d716e572d

mac 环境变量设置

From: https://blog.csdn.net/qq_18505715/article/details/83276208

打开终端

cd /Users/用户名

是否存在.bash_profile

open .bash_profile

不存在即创建

touch .bash_profile

打开编辑后,让.bash_profile配置的全局变量理解生效

source ~/.bash_profile

可能不起作用,原因是 mac下采用zsh代替bash,而zsh加载的是 ~/.zshrc文件,而 ‘.zshrc’ 文件中并没有定义任务环境变量

解决办法

在~/.zshrc文件最后,增加一行:

source ~/.bash_profile 

扩展
Zsh是一个Linux用户很少使用的shell,这是由于大多数Linux产品安装,以及默认使用bash shell。几乎每一款Linux产品都包含有zsh,通常可以用apt-get、urpmi或yum等包管理器进行安装,mac自带zsh

切换到zsh

chsh -s /bin/zsh

默认的zsh配置不太友好,我们下载一个github上别人配置好的文件

git clone git://github.com/robbyrussell/oh-my-zsh.git ~/.oh-my-zsh 

替换~/.zshrc

#备份
cp ~/.zshrc ~/.zshrc.orig
#替换
cp ~/.oh-my-zsh/templates/zshrc.zsh-template ~/.zshrc

注意 :采用zsh替代bash后,以前/.bashrc之类的配置应该写入/.zshrc中

更换zsh主题
~/.oh-my-zsh/themes目录中,有大量的主题可供选择,根据不同的爱好,可以自行选择,然后在~/.zshrc中配置即可。command+s,重启终端。

wift/Objective-C-使用Cocoapods创建/管理私有库(高级用法)

Swift/Objective-C-使用Cocoapods创建/管理私有库(高级用法)
From: https://www.jianshu.com/p/abb4ffe60e6a

接着上篇文章”Swift/Objective-C-使用Cocoapods创建/管理私有库(初中级用法)”的探索之路。

另外两篇文章:
Swift/Objective-C-使用Cocoapods创建/管理公共库
Swift/Objective-C-使用Cocoapods创建/管理私有库(初中级用法)

  • 高级
  • 通过subspec创建子模块(pod时可根据需要引入私有库的某个或者某几个模块到项目中,用不到的模块不引入);
  • 私有库中ARC和MRC文件的配置;
  • 依赖自己其他的私有库到当前创建的私有库中;
  • 添加第三方公共库到私有库中(两种情况:含/不含动态(.framework)、静态(.a)文件);
  • 私有库中Swift和Objective-C混编;
一、通过subspec创建子模块,及子模块之间的引用

Subspecs是一种分解Podspec功能的方法,它允许人们安装库的一个子集。也就是说,一个私有库中,包含很多模块,有时我们只需要pod这个私有库中的某个或者某些模块,选择性的使用,并不是pod全部,这时我们就需要在私有库中添加subspec,将各个模块开放分解出来,成为一个单独的子模块,以实现这样的需求。或者私有库中的某个模块和某个模块之间有引用关联,为了不影响其他模块的使用和冗余,也需要将这部分和其他模块区分开,也需要添加subspec。
在添加subspec时需要我们在编码的过程中尽量减少模块之间的依赖,使各个模块儿可以独立运行。

  • .podspec文件中添加子模块(subspec),并设置子模块键,配置如下:

    Pod::Spec.new do |s|
    s.name = ‘JYPrivateLibTest0’
    s.version = ‘1.0.1’
    s.summary = ‘这是一个私有测试库!’

    s.homepage = ‘https://git.asd.net/pod/JYPrivateLibTest0
    s.license = { :type => ‘MIT’, :file => ‘LICENSE’ }
    s.author = { ‘JYanshao’ => ‘654565181@qq.com’ }
    s.source = { :git => ‘https://git.asd.net/pod/JYPrivateLibTest0.git‘, :tag => s.version.to_s }

    s.ios.deployment_target = ‘8.0’
    s.requires_arc = true
    s.swift_version = ‘4.0’

    s.source_files = ‘JYPrivateLibTest0/Classes/*/

    —— 创建子模块 ——

    s.subspec ‘TextField’ do |tf| # tf为子模块键

    tf.source_files = 'JYPrivateLibTest0/Classes/TextField/*'
    tf.dependency 'JYPrivateLibTest0/Constant'  # TextField模块中文件使用了Constant模块的东西,所以需要引入依赖。如果不写依赖,当你只引入TextField模块的时候,就会报错,由于找不到Constant模块中你使用的东西。这里不可以直接引入主spec,即tf.dependency 'JYPrivateLibTest0',因为cocospods不允许(podspec文件:[!子规范不能要求它的父规范。),且在实际项目中引入时引入不成功,错误移步下边。
    

    end

    s.subspec ‘Constant’ do |c| # c为子模块键

    c.source_files = 'JYPrivateLibTest0/Classes/Constant/*'
    

    end

    end

在上面的例子中,一个工程中,有多个模块,并把各模块分解为子模块,使用 pod 'JYPrivateLibTest0' 的Podfile会包含整个库,而 pod 'JYPrivateLibTest0/Constant' 会只包含Constant模块。只引入你的项目中需要的模块,其他不需要的可以不引入。

  • 问题:tf.dependency 'JYPrivateLibTest0/Constant' TextField模块中文件使用了Contants模块的东西,子模块的依赖不能直接依赖父spec,如tf.dependency 'JYPrivateLibTest0', 这样cocospods是不允许,你pod的时候也不会pod成功,错误如下:

    Analyzing dependencies
    Fetching podspec for JYPrivateLibTest0 from ../
    [!] Failed to load ‘JYPrivateLibTest0’ podspec:
    [!] Invalid JYPrivateLibTest0.podspec file: [!] A subspec can’t require one of its parents specifications.

    from /Users/123456/Desktop/PrivateRepository/JYPrivateLibTest0/JYPrivateLibTest0.podspec:49

    ——————————————-

    tf.source_files = ‘JYPrivateLibTest0/Classes/TextField/*’

    tf.dependency 'JYPrivateLibTest0'
    

    end

    ——————————————-

解释:[!]加载“ JYPrivateLibTest0”podspec失败:
[! JYPrivateLibTest0]无效”。podspec文件:[!子规范不能要求它的父规范。
也就是说:同一工程中一子模块使用了另一子模块的东西,需要引入对另一子模块的依赖,且另一子模块也必须为subspec,不能直接依赖父spec。

二、私有库中ARC(自动内存管理)和MRC(手动内存管理)文件的配置

我这里找到一个使用Objective-C写的Base64加密的封装(Base64.h/Base64.m),且为MRC(手动内存管理)。下面让我们看一下如何来区别对待MRC和ARC文件吧,即如何让Cocoapods自动给MRC的文件添加-fno-objc-arc标识,使MRC和ARC共存。

  • 方法一:在.podspec文件中,先将整个子模块设置为MRC,即ocb.requires_arc = false,再通过requires_arc设置ARC文件,具体如下:([官方推荐][3])
[3]: https://links.jianshu.com/go?to=https%3A%2F%2Fguides.cocoapods.org%2Fsyntax%2Fpodspec.html

Pod::Spec.new do |s|

// 此处省略一些不便的配置

    s.subspec 'OCBase64' do |ocb|
        ocb.requires_arc = false
        ocb.requires_arc = ['JYPrivateLibTest0/Classes/OCBase64/*.{h,m}']
    end
end
  • 方法二:在.podspec文件中,先将子整个模块设置为ARC,即ocb.requires_arc = true,然后再通过exclude_filessubspec设置MRC文件,具体如下:

    Pod::Spec.new do |s|

    ...
    // 此处省略一些不便的配置
    ...
    
    s.subspec 'OCBase64' do |ocb|
        ocb.requires_arc = true # 如果其他地方已经写了这句,可省略;或者直接省略也可
        ocb.source_files = 'JYPrivateLibTest0/Classes/OCBase64/*'
    
        non_arc_files = 'JYPrivateLibTest0/Classes/OCBase64/Base64/*.{h,m}'
        ocb.exclude_files = non_arc_files # 排除MRC文件
        ocb.subspec 'no-arc' do |dd|
            dd.source_files = non_arc_files
            dd.requires_arc = false
        end
    end
    

    end

这两种配置方法的区别就是:
方法一,在项目中拉取私有库的时候,不会自动创建一个包含MRC文件的文件夹,看起来代码比较整齐。
方法二,在项目中拉取私有库的时候,会自动创建一个包含MRC文件的文件夹。

三、私有库中依赖其他私有库
  • 私有库中依赖其他的私有库
    这里以TESTRely私有库为例,该私有库中除了自己的封装还依赖了公共库AFNetworking。
  1. 将自己使用了TESTRely私有库的源代码封装文件,添加到Classes文件夹目录下,步骤参考上边的步骤;

  2. 配置.podspec文件,参数如下:(这里都是以子模块的形式添加的)

    Pod::Spec.new do |s|

    // 这里省略没改变的配置

    s.subspec ‘TestController’ do |tc|

    tc.source_files = 'JYPrivateLibTest0/Classes/TestController/*'
    tc.dependency 'TESTRely', '~>0.0.6'  # 依赖TESTRely私有库
    

    end

    end

  1. 配置demo工程的Podfile文件,通过source添加TESTRely私有库对应的索引库地址和TESTRely依赖的公共库AFNetworking对应的索引库地址,如下:

    source ‘https://git.asd.net/pod/JYPrivateRepoTest0.git‘ # JYPrivateRepoTest0对应的私有索引库地址
    source ‘https://git.asd.net/CocoaPods/TESTRelyLibrary.git‘ # TESTRely对应的私有索引库地址
    source ‘https://github.com/CocoaPods/Specs.git‘ # 官方公共库对应的公共索引库地址

注意:在项目中使用私有库,这个私有库又中的封装又依赖其他私有库或公共库,则项目的Podfile文件中,需要将该私有库,以及其依赖的其他私有库或公共库的索引库地址都添加到Podfile文件中,否则会报错,找不到依赖的其他私有库或公共库。
pod install时错误如下:

Analyzing dependencies
[!] Unable to find a specification for `TESTRely (~> 0.0.6)` depended upon by `JYPrivateLibTest0/TestController`

You have either:
 * out-of-date source repos which you can update with `pod repo update` or with `pod install --repo-update`.
 * mistyped the name or version.
 * not added the source repo that hosts the Podspec to your Podfile.

Note: as of CocoaPods 1.0, `pod repo update` does not happen on `pod install` by default.
  1. 验证本地的.podspec文件,命令如下:

    $ pod lib lint –sources=https://git.asd.net/pod/TESTRelyLibrary.git,https://github.com/CocoaPods/Specs.git –allow-warnings

    Cloning spec repo asd-pod-testrelylibrary from https://git.asd.net/pod/TESTRelyLibrary.git
    -> JYPrivateLibTest0 (1.0.2)

    • NOTE | [JYPrivateLibTest0/TextField, JYPrivateLibTest0/Constant, JYPrivateLibTest0/NetworkService, and more…] xcodebuild: note: Using new build system
    • NOTE | [JYPrivateLibTest0/TextField, JYPrivateLibTest0/Constant, JYPrivateLibTest0/NetworkService, and more…] xcodebuild: note: Planning build
    • NOTE | [JYPrivateLibTest0/TextField, JYPrivateLibTest0/Constant, JYPrivateLibTest0/NetworkService, and more…] xcodebuild: note: Constructing build description
    • NOTE | [JYPrivateLibTest0/TextField, JYPrivateLibTest0/Constant, JYPrivateLibTest0/NetworkService, and more…] xcodebuild: warning: Skipping code signing because the target does not have an Info.plist file. (in target ‘App’)

    JYPrivateLibTest0 passed validation.

注意:这里验证命令不能直接使用$ pod lib lint,这样会报找不到该私有库中依赖的其他私有库,导致验证通不过,后边必须通过--sources命令添加其他私有库和公共库的索引库地址,且用“,”隔开,如:$ pod lib lint --sources=https://git.asd.net/pod/TESTRelyLibrary.git,https://github.com/CocoaPods/Specs.git。运行pod sepc lintpod repo push [REPO] [NAME.podspec]命令时,也需要通过--sources命令添加其他私有库和公共库的索引库地址,否则也会验证通不过。

验证不通过,错误信息如下:

$ pod lib lint

 -> JYPrivateLibTest0 (1.0.2)
    - ERROR | [iOS] unknown: Encountered an unknown error (Unable to find a specification for `TESTRely (~> 0.0.6)` depended upon by `JYPrivateLibTest0/TestController`

You have either:
 * out-of-date source repos which you can update with `pod repo update` or with `pod install --repo-update`.
 * mistyped the name or version.
 * not added the source repo that hosts the Podspec to your Podfile.

Note: as of CocoaPods 1.0, `pod repo update` does not happen on `pod install` by default.
) during validation.

[!] JYPrivateLibTest0 did not pass validation, due to 1 error.
You can use the `--no-clean` option to inspect any issue.
  1. 运行git命令,提交修改到远程端私有代码库;

  2. 验证远程端.podspec文件,命令:

    $ pod spec lint –sources=https://git.asd.net/pod/TESTRelyLibrary.git,https://github.com/CocoaPods/Specs.git –allow-warnings

  1. 将.podspec文件提交到远程端私有索引库,命令:

    $ pod repo push JYPrivateRepoTest0 JYPrivateLibTest0.podspec –sources=https://git.asd.net/pod/TESTRelyLibrary.git,https://github.com/CocoaPods/Specs.git –allow-warnings

  1. 项目中验证,注意别忘了通过source引入私有库对应的私有索引库地址。
四、私有库中依赖第三方公共库

私有库中引入Cocoapods中的公共库,包含两种情况。
1. 引入的公共库中不包含动态文件(.framework)、静态文件(.a)
在制作我自己私有Pod时,我们往往需要用到第三方提供的工具包,比如说网络请求Alamofire、图片加载SDWebImage等,对于这些只有源代码文件的框架的使用是很简单的,我们在制作Pod的时候,只需要在.podspec文件中直接通过s.dependency引入即可,如:s.dependency 'Alamofire', '~> 4.8.1'。添加、验证、上传步骤请参考上边。

具体配置信息如下:

Pod::Spec.new do |s|
   ...
   // 这里省略没改变的配置
   ...

   s.subspec 'NetworkService' do |ns|
      ns.source_files = 'JYPrivateLibTest0/Classes/NetworkService/*'
      ns.dependency 'Alamofire', '~> 4.8.1'
   end
end

当执行pod install命令时,Pod会自己检测并且install我所引用到的第三方仓库。

2. 引入的公共库中包含动态文件(.framework)、静态文件(.a)
有时呢,我们也会用到诸如ShareSDK分享、高德地图等制作成动态库(framework)、静态库(library/.a)方式的第三方库,其中有些库是给你提供了Pod方式导入的,有些没有提供。下面让我们都试试吧。
这里以ShareSDK为例。
1). 提供了Pod方式导入的,可能你会想到直接通过dependency依赖,如下:

Pod::Spec.new do |s|
  ...
  // 这里省略没改变的配置
  ...

  s.static_framework = true # 是否包含静态库框架(注意:不能写在subspec子模块中)

  s.subspec 'ShareService' do |ss|
        ss.dependency 'mob_sharesdk', '~>4.2.3'
        ss.dependency 'mob_sharesdk/ShareSDKPlatforms/QQ', '~>4.2.3'
        ss.dependency 'mob_sharesdk/ShareSDKPlatforms/SinaWeibo', '~>4.2.3'
        ss.dependency 'mob_sharesdk/ShareSDKPlatforms/WeChat', '~>4.2.3'
        ss.dependency 'mob_sharesdk/ShareSDKUI', '~>4.2.3'
        ss.dependency 'mob_sharesdk/ShareSDKExtension', '~>4.2.3'
  end
end

这样引入是没问题的,可以通过pod lib lintpod sepc lint命令的验证,提交到远程端的私有代码库。在项目中测试,是没有问题的,可以引入和使用,如下图:

第三方库头文件引入.png

遇到的错误:
如果你在pod lib lint验证时,遇到如下错误,导致验证不通过,说明你没有配置s.static_framework = true,因为你使用了动态库(.framework)、静态库(.a),如果use_frameworks!指定时,pod应包含静态库框架,所以需要允许静态库的使用。错误信息如下:

$ pod lib lint JYPrivateLibTest0.podspec

 -> JYPrivateLibTest0 (1.0.4)
    - ERROR | [iOS] unknown: Encountered an unknown error (The 'Pods-App' target has transitive dependencies that include static binaries: (/private/var/folders/qt/d9rd9h7n2m3cb9014qph7xkm0000gn/T/CocoaPods-Lint-20190320-5532-92gozf-JYPrivateLibTest0/Pods/mob_sharesdk/ShareSDK/ShareSDK.framework, /private/var/folders/qt/d9rd9h7n2m3cb9014qph7xkm0000gn/T/CocoaPods-Lint-20190320-5532-92gozf-JYPrivateLibTest0/Pods/mob_sharesdk/ShareSDK/Support/Required/ShareSDKConnector.framework, /private/var/folders/qt/d9rd9h7n2m3cb9014qph7xkm0000gn/T/CocoaPods-Lint-20190320-5532-92gozf-JYPrivateLibTest0/Pods/mob_sharesdk/ShareSDK/Support/Optional/ShareSDKExtension.framework, /private/var/folders/qt/d9rd9h7n2m3cb9014qph7xkm0000gn/T/CocoaPods-Lint-20190320-5532-92gozf-JYPrivateLibTest0/Pods/mob_sharesdk/ShareSDK/Support/PlatformSDK/QQSDK/TencentOpenAPI.framework, /private/var/folders/qt/d9rd9h7n2m3cb9014qph7xkm0000gn/T/CocoaPods-Lint-20190320-5532-92gozf-JYPrivateLibTest0/Pods/mob_sharesdk/ShareSDK/Support/PlatformConnector/QQConnector.framework, /private/var/folders/qt/d9rd9h7n2m3cb9014qph7xkm0000gn/T/CocoaPods-Lint-20190320-5532-92gozf-JYPrivateLibTest0/Pods/mob_sharesdk/ShareSDK/Support/PlatformSDK/SinaWeiboSDK/libWeiboSDK.a, /private/var/folders/qt/d9rd9h7n2m3cb9014qph7xkm0000gn/T/CocoaPods-Lint-20190320-5532-92gozf-JYPrivateLibTest0/Pods/mob_sharesdk/ShareSDK/Support/PlatformConnector/SinaWeiboConnector.framework, /private/var/folders/qt/d9rd9h7n2m3cb9014qph7xkm0000gn/T/CocoaPods-Lint-20190320-5532-92gozf-JYPrivateLibTest0/Pods/mob_sharesdk/ShareSDK/Support/PlatformSDK/WeChatSDK/libWeChatSDK.a, /private/var/folders/qt/d9rd9h7n2m3cb9014qph7xkm0000gn/T/CocoaPods-Lint-20190320-5532-92gozf-JYPrivateLibTest0/Pods/mob_sharesdk/ShareSDK/Support/PlatformConnector/WechatConnector.framework, and /private/var/folders/qt/d9rd9h7n2m3cb9014qph7xkm0000gn/T/CocoaPods-Lint-20190320-5532-92gozf-JYPrivateLibTest0/Pods/mob_sharesdk/ShareSDK/Support/Optional/ShareSDKUI.framework)) during validation.

[!] JYPrivateLibTest0 did not pass validation, due to 1 error.
You can use the `--no-clean` option to inspect any issue.

以上错误信息解读:在验证过程中,遇到一个未知的错误(‘ podcast - app ‘目标有传递依赖关系,包括动静态二进制文件。

2). 这种不管能不能pod导入,都要下载你需要的SDK包,手动导入。以ShareSDK为例,首先在ShareSDK官网下载你所需要的ShareSDK包,然后将下载的ShareSDK包,导入到你的私有库的Classes文件夹下,再然后配置你的.podspec文件。(这里仅仅是导入第三方库,不包含其他文件)
.podspec文件配置如下:

Pod::Spec.new do |s|
  ...
  // 这里省略没改变的配置
  ...

  # 是否包含静态库框架(注意:不能写在subspec子模块中)
  s.static_framework = true  

  s.subspec 'ShareService' do |ss|
          # 文件的路径和公开头文件路径
          #ss.source_files = 'JYPrivateLibTest0/Classes/ShareService/JYMobShareSDK/**/*.framework/Headers/*.h'
          #ss.public_header_files = 'JYPrivateLibTest0/Classes/ShareService/JYMobShareSDK/**/*.framework/Headers/*.h'

          # ShareSDK的所有动态库路径(也可以写具体的路径,ShareSDK的framework太多了,偷懒一下)
          ss.vendored_frameworks = 'JYPrivateLibTest0/Classes/ShareService/JYMobShareSDK/**/*.framework'
          # 第三方的静态文件路径
          ss.vendored_libraries = 'JYPrivateLibTest0/Classes/ShareService/JYMobShareSDK/ShareSDK/Support/PlatformSDK/**/*.a'
          # 第三方的资源文件
          ss.resources = 'JYPrivateLibTest0/Classes/ShareService/JYMobShareSDK/ShareSDK/Support/**/*.bundle'

          # 第三方用到的系统动态库
          ss.frameworks = 'UIKit', 'JavaScriptCore', 'ImageIO'
          # 第三方用到的系统静态文件(前面的lib要去掉,否则会报错)
          ss.libraries = 'icucore', 'z', 'c++', 'sqlite3'

          # Build Settings里边的设置
          ss.pod_target_xcconfig = {
              #'FRAMEWORK_SEARCH_PATHS' => '${PODS_ROOT}/JYPrivateLibTest0/Classes/ShareService/JYMobShareSDK',
              'HEADER_SEARCH_PATHS' => '$(PODS_ROOT)/JYPrivateLibTest0/Classes/ShareService/JYMobShareSDK/**/*.framework/Headers',
              'LD_RUNPATH_SEARCH_PATHS' => '$(PODS_ROOT)/JYPrivateLibTest0/Classes/ShareService/JYMobShareSDK/',
              'OTHER_LDFLAGS' => ['-ObjC']
          }
  end
end

将修改提交到远程端私有代码库,通过pod lib lintpod sepc lint命令验证,并提交到远程端的私有代码库。
注意:s.static_framework = true 必须和s.name同级,不能写在s.subspec子模块中,否则回报错误。

遇到的问题:
demo工程pod install后,编译运行,以及pod lib lint验证时都会报以下错误,错误信息如下:

编译运行demo报错.png

$ pod lib lint --sources=https://git.asd.net/pod/TESTRelyLibrary.git,https://github.com/CocoaPods/Specs.git --allow-warnings

 -> JYPrivateLibTest0 (1.0.5)
    - ERROR | [iOS] xcodebuild: Returned an unsuccessful exit code. You can use `--verbose` for more information.
    - NOTE  | [JYPrivateLibTest0/Constant, JYPrivateLibTest0/TextField, JYPrivateLibTest0/NetworkService, and more...] xcodebuild:  note: Using new build system
    - NOTE  | [JYPrivateLibTest0/Constant, JYPrivateLibTest0/TextField, JYPrivateLibTest0/NetworkService, and more...] xcodebuild:  note: Planning build
    - NOTE  | [JYPrivateLibTest0/Constant, JYPrivateLibTest0/TextField, JYPrivateLibTest0/NetworkService, and more...] xcodebuild:  note: Constructing build description
    - NOTE  | [JYPrivateLibTest0/Constant, JYPrivateLibTest0/TextField, JYPrivateLibTest0/NetworkService, and more...] xcodebuild:  warning: Skipping code signing because the target does not have an Info.plist file. (in target 'App')
    - NOTE  | xcodebuild:  <module-includes>:1:9: note: in file included from <module-includes>:1:
    - NOTE  | [iOS] xcodebuild:  //privateTarget Support Files/JYPrivateLibTest0/JYPrivateLibTest0-umbrella.h:45:9: note: in file included from //privateTarget Support Files/JYPrivateLibTest0/JYPrivateLibTest0-umbrella.h:45:
    - ERROR | [iOS] xcodebuild:  /Users/123/Desktop/PrivateRepository/JYPrivateLibTest0/JYPrivateLibTest0/Classes/ShareService/JYMobShareSDK/Required/MOBFoundation.framework/Headers/MOBFOAuthService.h:9:9: error: include of non-modular header inside framework module 'JYPrivateLibTest0.MOBFOAuthService': '/Users/123/Desktop/PrivateRepository/JYPrivateLibTest0/JYPrivateLibTest0/Classes/ShareService/JYMobShareSDK/Required/MOBFoundation.framework/Headers/MOBFoundation.h'
    - NOTE  | [iOS] xcodebuild:  //privateTarget Support Files/JYPrivateLibTest0/JYPrivateLibTest0-umbrella.h:64:9: note: in file included from //privateTarget Support Files/JYPrivateLibTest0/JYPrivateLibTest0-umbrella.h:64:
    - ERROR | [iOS] xcodebuild:  /Users/123/Desktop/PrivateRepository/JYPrivateLibTest0/JYPrivateLibTest0/Classes/ShareService/JYMobShareSDK/ShareSDK/ShareSDK.framework/Headers/ShareSDK+Base.h:9:9: error: include of non-modular header inside framework module 'JYPrivateLibTest0.ShareSDK_Base': '/Users/123/Desktop/PrivateRepository/JYPrivateLibTest0/JYPrivateLibTest0/Classes/ShareService/JYMobShareSDK/ShareSDK/ShareSDK.framework/Headers/ShareSDK.h'
    - NOTE  | xcodebuild:  //privateTarget Support Files/JYPrivateLibTest0/JYPrivateLibTest0-umbrella.h:84:9: note: in file included from //privateTarget Support Files/JYPrivateLibTest0/JYPrivateLibTest0-umbrella.h:84:
    - ERROR | [iOS] xcodebuild:  /Users/123/Desktop/PrivateRepository/JYPrivateLibTest0/JYPrivateLibTest0/Classes/ShareService/JYMobShareSDK/ShareSDK/Support/Optional/ShareSDKUI.framework/Headers/ShareSDKUI.h:13:9: error: include of non-modular header inside framework module 'JYPrivateLibTest0.ShareSDKUI': '/Users/123/Desktop/PrivateRepository/JYPrivateLibTest0/JYPrivateLibTest0/Classes/ShareService/JYMobShareSDK/ShareSDK/Support/Optional/ShareSDKUI.framework/Headers/SSUIEditorConfiguration.h'
    - ERROR | [iOS] xcodebuild:  /Users/123/Desktop/PrivateRepository/JYPrivateLibTest0/JYPrivateLibTest0/Classes/ShareService/JYMobShareSDK/ShareSDK/Support/Optional/ShareSDKUI.framework/Headers/ShareSDKUI.h:14:9: error: include of non-modular header inside framework module 'JYPrivateLibTest0.ShareSDKUI': '/Users/asd/Desktop/PrivateRepository/JYPrivateLibTest0/JYPrivateLibTest0/Classes/ShareService/JYMobShareSDK/ShareSDK/Support/Optional/ShareSDKUI.framework/Headers/SSUIShareSheetConfiguration.h'
    - ERROR | [iOS] xcodebuild:  /Users/asd/Desktop/PrivateRepository/JYPrivateLibTest0/JYPrivateLibTest0/Classes/ShareService/JYMobShareSDK/ShareSDK/Support/Optional/ShareSDKUI.framework/Headers/ShareSDKUI.h:15:9: error: include of non-modular header inside framework module 'JYPrivateLibTest0.ShareSDKUI': '/Users/asd/Desktop/PrivateRepository/JYPrivateLibTest0/JYPrivateLibTest0/Classes/ShareService/JYMobShareSDK/ShareSDK/Support/Optional/ShareSDKUI.framework/Headers/SSUIPlatformItem.h'
    - NOTE  | [iOS] xcodebuild:  <unknown>:0: error: could not build Objective-C module 'JYPrivateLibTest0'
    - WARN  | [JYPrivateLibTest0/TestController] xcodebuild:  /Users/asd/Desktop/PrivateRepository/JYPrivateLibTest0/JYPrivateLibTest0/Classes/TestController/JYFather.swift:9:8: warning: file 'JYFather.swift' is part of module 'JYPrivateLibTest0'; ignoring import
    - WARN  | [JYPrivateLibTest0/TestController, JYPrivateLibTest0/OCBase64, JYPrivateLibTest0/OCBase64/Base64, and more...] xcodebuild:  /Users/asd/Desktop/PrivateRepository/JYPrivateLibTest0/JYPrivateLibTest0/Classes/OCBase64/Base64/Base64.m:63:63: warning: implicit conversion loses integer precision: 'long long' to 'NSUInteger' (aka 'unsigned int') [-Wshorten-64-to-32]
    - WARN  | [JYPrivateLibTest0/TestController, JYPrivateLibTest0/OCBase64, JYPrivateLibTest0/OCBase64/Base64, and more...] xcodebuild:  /Users/asd/Desktop/PrivateRepository/JYPrivateLibTest0/JYPrivateLibTest0/Classes/OCBase64/Base64/Base64.m:91:25: warning: implicit conversion loses integer precision: 'long long' to 'NSUInteger' (aka 'unsigned int') [-Wshorten-64-to-32]
    - WARN  | [JYPrivateLibTest0/TestController, JYPrivateLibTest0/OCBase64, JYPrivateLibTest0/OCBase64/Base64, and more...] xcodebuild:  /Users/asd/Desktop/PrivateRepository/JYPrivateLibTest0/JYPrivateLibTest0/Classes/OCBase64/Base64/Base64.m:107:58: warning: implicit conversion loses integer precision: 'long long' to 'unsigned long' [-Wshorten-64-to-32]
    - WARN  | [JYPrivateLibTest0/TestController, JYPrivateLibTest0/OCBase64, JYPrivateLibTest0/OCBase64/Base64, and more...] xcodebuild:  /Users/asd/Desktop/PrivateRepository/JYPrivateLibTest0/JYPrivateLibTest0/Classes/OCBase64/Base64/Base64.m:147:44: warning: implicit conversion loses integer precision: 'long long' to 'unsigned long' [-Wshorten-64-to-32]
    - WARN  | [JYPrivateLibTest0/TestController, JYPrivateLibTest0/OCBase64, JYPrivateLibTest0/OCBase64/Base64, and more...] xcodebuild:  /Users/asd/Desktop/PrivateRepository/JYPrivateLibTest0/JYPrivateLibTest0/Classes/OCBase64/Base64/Base64.m:149:54: warning: implicit conversion loses integer precision: 'long long' to 'NSUInteger' (aka 'unsigned int') [-Wshorten-64-to-32]

[!] JYPrivateLibTest0 did not pass validation, due to 6 errors.
You can use the `--no-clean` option to inspect any issue.

上面信息的摘取:

error: include of non-modular header inside framework module 'JYPrivateLibTest0.ShareSDK_Base': '/Users/asd/Desktop/PrivateRepository/JYPrivateLibTest0/JYPrivateLibTest0/Classes/ShareService/JYMobShareSDK/ShareSDK/ShareSDK.framework/Headers/ShareSDK.h'

要解决这个错误其实很简单,只需要注释掉或删除掉ShareService子模块中的ss.source_files和ss.public_header_files这两个参数及配置即可。

那报这个错误是什么原因造成的呢?
在创建Pod,且引入Objective-C语言开发的第三方公共库供Swift使用时,我们并不需要创建xxx-Bridge-Header.h桥文件去引入Objective-C的头文件, 这个工作是交由xxx-umbrella.h文件完成,这个文件的其中一个作用:其实和xxx-Bridge-Header.h桥文件的作用基本相同,向外界暴露Objective-C的头文件供Swift使用,实现Swift和Objective-C的混编。当你再次通过ss.source_files和ss.public_header_files暴露第三方公共的头文件时会重复定义,所以需要注释掉或删除掉ss.source_files和ss.public_header_files这两个参数及配置。让我们试试能否成功吧,结果是肯定的。

  • 优化(仅限手动导入的第三方)
    通过以上的第三方公库的导入已经是可以使用了。但是呢,你可能会想到如果你的项目主要是采用Swift语言来写的,那么你就必须要创建xxx-Bridge-Heder.h桥文件来引入Objective-C的头文件,那有没有一种方式是可以不需要创建xxx-Bridge-Header.h桥文件的呢?

为了使你更好的了解,在这里我新创建了一个私有代码库JYPLibTest1,且使用百度地图Lib来演示,然后通过subspec在.podsepc中创建百度地图的子模块JYBaiDuMapKit(子模块文件夹名字,可随意取)。目录如下:

百度地图Lib目录结构.png

解决方案:让我们来优化一下吧,这里我将创建一个.modulemap文件来解决这个问题,让我们修改.podspec,为所引用到的framework创建Module

注意:如果我们手动创建一个. modulemap文件,然后直接将该文件拖到相应的目录下,这样在 pod install 时可能会导致丢失该文件,那么我们应该怎么办呢?解决办法是,我们需要使用prepare_command属性,来帮助我们自动创建.modulemap文件。

prepare_command属性的解释、使用场景及禁用条件:
prepare_command属性是下载Pod后将执行的bash脚本。此命令可用于创建、删除和修改下载的任何文件,并将在收集规范的其他文件属性的任何路径之前运行。
此命令在清理Pod和创建Pods项目之前执行。工作目录是Pod的根目录。
prepare_command属性必须在主模块中使用。
如果pod安装了:path选项,则不会执行此命令。

具体配置如下:

Pod::Spec.new do |s|
  s.name             = 'JYPLibTest1'
  s.version          = '1.0.0'
  s.summary          = '另一个测试私有库'
  s.homepage         = 'https://git.artron.net/CocoaPods/JYPLibTest1'
  s.license          = { :type => 'MIT', :file => 'LICENSE' }
  s.author           = { 'JYanshao' => '654565181@qq.com' }
  s.source           = { :git => 'https://git.123.net/CocoaPods/JYPLibTest1.git', :tag => s.version.to_s }
  s.ios.deployment_target = '8.0'
  s.source_files = 'JYPLibTest1/Classes/**/*.swift'
  s.swift_version = '4.2'

  s.subspec 'JYBaiDuMapKit' do |mk|

      mk.vendored_frameworks =  'JYPLibTest1/Classes/JYBaiDuMapKit/*.framework'
      mk.vendored_libraries = 'JYPLibTest1/Classes/JYBaiDuMapKit/thirdlibs/*.a'

      mk.resources = 'JYPLibTest1/Classes/JYBaiDuMapKit/BaiduMapAPI_Map.framework/mapapi.bundle'

      mk.frameworks   =  'CoreLocation', 'QuartzCore', 'OpenGLES', 'SystemConfiguration', 'CoreGraphics', 'Security', 'CoreTelephony'
      mk.libraries    = 'sqlite3', 'c++'

      mk.preserve_paths = 'JYPLibTest1/Classes/JYBaiDuMapKit/*.framework', 'JYPLibTest1/Classes/JYBaiDuMapKit/thirdlibs/*.a'

      mk.pod_target_xcconfig = {
          'HEADER_SEARCH_PATHS' => '$(PODS_ROOT)/JYPLibTest1/Classes/JYBaiDuMapKit/*.framework/Headers',
          'LD_RUNPATH_SEARCH_PATHS' => '$(PODS_ROOT)/JYPLibTest1/Classes/JYBaiDuMapKit/',
          'OTHER_LDFLAGS' => '-ObjC'
      }
  end

  # prepare_command属性必须在主模块中使用
  s.prepare_command = <<-EOF

      # 创建BaiduMapAPI_Base Module
      rm -rf JYPLibTest1/Classes/JYBaiDuMapKit/BaiduMapAPI_Base.framework/Modules
      mkdir JYPLibTest1/Classes/JYBaiDuMapKit/BaiduMapAPI_Base.framework/Modules
      touch JYPLibTest1/Classes/JYBaiDuMapKit/BaiduMapAPI_Base.framework/Modules/module.modulemap
      cat <<-EOF > JYPLibTest1/Classes/JYBaiDuMapKit/BaiduMapAPI_Base.framework/Modules/module.modulemap
      framework module BaiduMapAPI_Base {
          umbrella header "BMKBaseComponent.h"
          export *
          link "sqlite3"
          link "c++"
      }
      \EOF

      # 创建BaiduMapAPI_Map Module
      rm -rf JYPLibTest1/Classes/JYBaiDuMapKit/BaiduMapAPI_Map.framework/Modules
      mkdir JYPLibTest1/Classes/JYBaiDuMapKit/BaiduMapAPI_Map.framework/Modules
      touch JYPLibTest1/Classes/JYBaiDuMapKit/BaiduMapAPI_Map.framework/Modules/module.modulemap
      cat <<-EOF > JYPLibTest1/Classes/JYBaiDuMapKit/BaiduMapAPI_Map.framework/Modules/module.modulemap
      framework module BaiduMapAPI_Map {
          umbrella header "BMKMapComponent.h"
          export *
          link "sqlite3"
          link "c++"
      }
      \EOF

  EOF

end

配置完成,让我们更新一下demo工程,测试一下是否可以使用。在测试项目中的使用和在自己私有库中的使用,结果如下图:

测试项目中的使用.png

私有库中的使用.png

百度地图运行结果.png

相同的方法在ShareSDK相应的framework目录下创建Module却出现了错误,暂时未解决,有兴趣的朋友可以试试,分享一下。

五、私有库中Swift和Objective-C混编(两种情况)

这里我门先看一下,混编的两种情况的文件目录结构,然后再一步一步的往下走,目录结构如下:

├── JYPLibTest1
│   ├── Assets
│   ├── Classes
│   │   ├── OCClass                      # Objective-C类的文件夹(OCClass子模块)
│   │   │   ├── JYFamily.h                    # 家庭类(NSObject)
│   │   │   ├── JYFamily.m
│   │   │   ├── JYFather.swift             # 父亲类(NSObject)
│   │   │   ├── JYKid.h                           # 孩子类(NSObject)
│   │   │   ├── JYKid.m
│   │   │   ├── JYMother.swift                # 母亲类(NSObject)
│   │   │   ├── JYPLibTest1.h                  # 头文件(项目要求要有的)
│   │   │   └── JYPersonProtocol.h       # 协议类
│   │   ├── SwiftClass                      # Swift类的文件夹(SwiftClass子模块)
│   │   │   ├── JYFather2.swift               # 父亲类2(NSObject)
│   │   │   └── JYMother2.swift             # 母亲类2(NSObject)

1. 同一个模块(OCClass)内的Swift和Objective-C混编
一个层面问题的解决,又伴随着另一层面的思考,那就是如何在私有库中实现Swfit和Objective-C的混编及混合打包(我们这一步只验证Swift与Objective-C混合打包)。

同一个模块(OCClass)内的Swift和Objective-C混编,可以是单向调用,即Swift调用Objective-C,或者Objective-C调用Swift,也可以是双向调用,即Swift调用Objective-C,Objective-C又调用Swift。

在这里我们继续使用JYPLibTest1私有库,且以双向调用为例。这个问题让我们通过一个案例来看,能更好的理解。
案例:一个周末的早上,家中的父母做好早餐,叫孩子起床,喂他/她吃饭。(一个家庭中,包括父母和孩子,父母中又包括孩子,父母、孩子都有吃的动作,孩子吃饭是由父母喂的)。

  • 首先让我们创建一个JYKid(孩子)类和一个JYPersonProtocol协议类,这两个类都是Objective-C类。接下来实现这两个类,代码如下:

    /// JYKid.h
    #import
    #import “JYPersonProtocol.h”

    @interface JYKid : NSObject // 遵循协议

    @property (nonatomic, strong) NSString *name;

    @end

/// JYKid.m
#import "JYKid.h"

@implementation JYKid

/// 实现JYPersonProtocol协议方法
- (void)eat {
    NSLog(@"%@ is eating", self.name != nil ? self.name : @"Kid");
}

@end


/// JYPersonProtocol.h
#import <Foundation/Foundation.h>

@protocol JYPersonProtocol <NSObject>

@optional
- (void)eat;

@end

完成后,我们的这两个类肯定是需要给外部文件(Objective-C/Swift)使用的,那么该如何处理呢?其实解决办法很简单,只需将Objective-C类的.h文件设置为public即可。以JYKid.h为例,具体设置如下图:

将OC的.h文件设置为public.png

相同的方法,将JYPersonProtocol.h也设置为public。有的文件创建完默认就是public,那就可以略过此步了。

设置完成,修改.podspec文件,内容如下:

Pod::Spec.new do |s|
  s.name             = 'JYPLibTest1'
  s.version          = '1.0.1'
  s.summary          = '另一个测试私有库'
  s.homepage         = 'https://git.artron.net/CocoaPods/JYPLibTest1'
  s.license          = { :type => 'MIT', :file => 'LICENSE' }
  s.author           = { 'JYanshao' => '654565181@qq.com' }
  s.source           = { :git => 'https://git.artron.net/CocoaPods/JYPLibTest1.git', :tag => s.version.to_s }
  s.ios.deployment_target = '8.0'
  s.source_files = 'JYPLibTest1/Classes/**/*.swift'

  s.subspec 'OCClass' do |cc|
      cc.source_files = 'JYPLibTest1/Classes/OCClass/*.{h,m}'
  end  
end

在终端执行下pod install命令,更新下demo工程,编译运行没有错误。然后打开demo中的xxx-umbrella.h文件,你会发现JYKid和JYPersonProtocol两个文件的头文件已经被自动引入进来了。如下图:

xxx-umbrella.h文件可以被看成是你项目中桥接文件xxx-Bridge-Header.h,也可以看成是一个Objective-C的头文件。

查看JYPLibTest1-umbrella.h文件.png

让我们继续,创建两个Swift类JYFather和JYMother,然后实现Swift对Objective-C类的引用,使其有一个小孩JYKid并且实现JYPersonProtocol协议,然后喂这个小孩吃饭。
注意:Swift类与Objective-C类有些区别,Swift类不需要像Objective-C类一样设置他们的头文件为public,Swift类只需要定义其文件的访问权限即可。
JYFather和JYMother文件中的代码一样,以JYFather为例,代码如下:

/// JYFather.swift / JYMother.swift 这两个文件中的代码一样,copy一下
/// 父母这两个文件都继承JYPersonProtocol协议,并实现其方法
import UIKit

open class JYFather: NSObject, JYPersonProtocol {
    var name: String = ""
    var kid: JYKid?

    public init(name: String, kid: JYKid) {
        super.init()
        self.name = name
        self.kid = kid
    }

    @objc public func feed(_ food: String) {
        print("\(name) is feeding \(kid?.name ?? "kid") eat \(food)")
    }

    /// 实现协议方法
    public func eat() {
        print("\(name) is eating")
    }
}

修改.podspec文件,配置如下:

Pod::Spec.new do |s|
    ...
    # 没有变化的部分省略了
    ...

    s.subspec 'OCClass' do |cc|
          cc.source_files = 'JYPLibTest1/Classes/OCClass/*.{h,m,swift}'
    end
end

执行pod install,更新demo,编译运行,结果报错了,错误如下:

错误1.png

错误11.png

提示我们没有找到JYPLibTest1.h文件,因为项目中根本就没有这个文件,所以才找不到,既然它需要这么一个文件,那就按它的意思创建一个JYPLibTest1.h头文件,并 #import 引入已经创建的Objective-C类头文件。
注意:JYPLibTest1头文件,是一个Objective-C类的头文件。用来导入Objective-C类的头文件用的,相当于xxx-Bridging-Header.h文件。

JYPLibTest1.h头文件.png

更新demo,并编译运行发现没有问题了。

到这里,我们创建了父母、孩子、以及吃的协议类,接下来让我们创建一个Objective-C的家庭类JYFamily,把他们组合成一个家庭吧,这样才圆满,并实现Objective-C类调用Swift类。
JYFamily类的代码如下:

/// JYFamily.h
#import <Foundation/Foundation.h>
#import "JYKid.h"
@class JYFather, JYMother;   // 这里有个问题需注意,后面会讲到

@interface JYFamily : NSObject

@property (nonatomic, strong) JYKid *kid;
@property (nonatomic, strong) JYFather *father;
@property (nonatomic, strong) JYMother *mother;

// 打印父母各自的喂食
- (void)feed:(NSString *)fFood mFood:(NSString *)mFood;

@end


/// JYFamily.m
#import "JYFamily.h"
#import <JYPLibTest1/JYPLibTest1-Swift.h>

@implementation JYFamily

- (void)feed:(NSString *)fFood mFood:(NSString *)mFood {
    [self.father feed:fFood]; // 父亲喂孩子吃食物
    [self.mother feed:mFood]; // 母亲喂孩子吃食物
}

@end

更新demo,并编译运行,发现没有错误。然后在测试项目中引入,并测试是否可用。我的测试结果如下:

Swift和OC混合开发运行结果.png

本地和远程验证也是可以通过的,上传也是成功的。

这里有个问题需要注意一下
如果你在JYFamily.h文件中直接引入#import "JYPLibTest1-Swift.h"头文件的话,编译运行demo可能会报如下错误。

导入JYPLibTest1-Swift.h错误.png

原因有两点:1). 在你的ProjectName-Swift.h中是引用了JYFamily.h的,如果你这时候在定义JYFamily.h中又引用ProjectName-Swift.h造成了有点像循环引用的概念,所以在JYFamily.h中只需要用@Class声明一下用到的文件即可,在JYFamily.m中在真正引用需要用到的文件;
(下面这张图片是借用的,原文章在下边的参考文章部分有链接)

image

2). Swift只支持动态库,但并非完全意义的动态库,而我们的代码在Pod之后实际上是一个动态的Framework,Swift是有命名空间的一个概念,这时候你需要做的是在引用时需要写明命名空间。

基于上述两点原因,我们只能在Objective-C类的JYFamily.m文件中引用(#import)并且加上命名空间,而JYFamily.h文件中则用 @Class 声明一下引用的类。最终结果如下图:

循环关联和命名空间2解决方案.png

循环关联和命名空间1解决方案.png

解决完这个问题,记得更新demo,编译运行。

2.不同模块内的Swift和Objective-C的混编

不同模块内的Swift和Objective-C混编,可以是单向调用,即Swift调用Objective-C,或者Objective-C调用Swift,也可以是双向调用,但有些限制,不是很灵活。

紧接着第一种情况,我们来探索一下第二种情况(还是以双向引用为例)。
我们还使用上面OCClass子模块中的Objective-C类,然后在SwiftClass文件夹下创建Swift类JYFather2和JYMother2两个文件,这两个文件中的代码是copy的JYFather和JYMother两个文件中的代码。

创建并编辑完成后,将JYFamily文件中的JYFather和JYMother替换为JYFather2和JYMother2。如下图所示:

修改后的JYFamily.h文件.png

完成以上步骤,修改.podspec文件的配置,因为是两个不同的子模块之间的混编,所以需要通过dependency来引入另一个模块供本模块使用,具体配置如下:

Pod::Spec.new do |s|
  ...
  # 不变的配置省略
  ...

  s.subspec 'OCClass' do |cc|
      cc.source_files = 'JYPLibTest1/Classes/OCClass/*.{h,m,swift}'
      #cc.dependency 'JYPLibTest1/SwiftClass' # 这里不能再依赖SwiftClass子模块了,否则会造成循环依赖的问题
  end

  s.subspec 'SwiftClass' do |sc|
      sc.source_files = 'JYPLibTest1/Classes/SwiftClass/*.swift'
      sc.dependency 'JYPLibTest1/OCClass'
  end
end 

终端下更新demo工程,编译运行,没有问题。
但是在执行pod lib lint验证时却出现了错误,具体错误如下:

- ERROR | [JYPLibTest1/OCClass,JYPLibTest1/SwiftClass] xcodebuild:  /Users/artron/Desktop/PrivateRepository/JYPLibTest1/JYPLibTest1/Classes/OCClass/JYFamily.m:15:6: error: receiver type 'JYFather2' for instance message is a forward declaration
- ERROR | [JYPLibTest1/OCClass,JYPLibTest1/SwiftClass] xcodebuild:  /Users/artron/Desktop/PrivateRepository/JYPLibTest1/JYPLibTest1/Classes/OCClass/JYFamily.m:16:6: error: receiver type 'JYMother2' for instance message is a forward declaration

这个问题造成的原因是你的JYFamily.h文件中用了@Class声明了你的JYFather2和JYMother2,但是在JYFamily.m中使用的时候没有检测到你用#import引入这两个文件,所以验证失败。所以子模块之间的混编会有些局限。

子模块之间还要注意循环依赖的问题
如上.podspec中,cc.dependency 'JYPLibTest1/SwiftClass'sc.dependency 'JYPLibTest1/OCClass'两个子模块都互相依赖了对方,这时执行pod install,会报JYPLibTest1/SwiftClassJYPLibTest1/OCClass之间存在循环依赖关系的问题,解决方法就是断开一方的依赖即可。
具体错误如下:

两者之间循环依赖问题.png

以上若有不妥请指正。

参考文章:
Build with CocoaPods
使用私有Cocoapods仓库 中高级用法
pod库包含MRC的文件
组件化开发之-如何解决Swift/OC-Framenwork/Library混合创建pod问题


CocoaPods私有库可能遇到的坑

From: https://www.jianshu.com/p/bdb1db6c9db4

1.pod lib lint 和 pod spec lint 命令的区别

pod lib lint 是只从本地验证你的pod能否通过验证。
pod spec lint 是从本地和远程验证你的pod能否通过验证。
一般可以直接使用pod spec lint去验证pod有没有问题。

2.私有pod的验证

使用pod spec lint去验证私有库能否通过验证时应该,应该要添加–sources选项,不然会出现找不到repo的错误。

pod spec lint --sources='私有仓库repo地址,https://github.com/CocoaPods/Specs'

3.subspec

为了让自己的Pod被导入时显示出良好的文件层划分,subspec是必须的。
若subspec要依赖其它的subspec,则subspec的dependency后面接的不是目录路径,而是specA/specB这种spec关系。

4.私有库引用私有库的问题

在私有库引用了私有库的情况下,在验证和推送私有库的情况下都要加上所有的资源地址,不然pod会默认从官方repo查询。

pod spec lint --sources='私有仓库repo地址,https://github.com/CocoaPods/Specs'
pod repo push 本地repo名 podspec名 --sources='私有仓库repo地址,https://github.com/CocoaPods/Specs'

引用自己或第三方的framework或.a文件时,在podsepc中应该这样写:

s.ios.vendored_frameworks = "xxx/**/*.framework"
s.ios.vendored_libraries = "xxx/**/*.a”

5.便捷地开发本地私有库

Cocoapods就提供了一个开发模式,其实操作起来也是非常简单的事情,就是将所谓的引用路径修改成本地路径即可。就是讲Podfile中的pod ‘库名’, :path => ‘本地路径’即可。这样在通常的修改代码中是不需要执行pod update的,但是对于如果修改了目录结构(添加、删除或者移动文件文件)或者是修改了Podspec文件的配置的话,最好是运行一下pod update的命令。普通修改代码的情况下就不需要运行pod update命令和打tag了。
pod 'iOS-Test', :path => '../iOS-Test’

6.私有库中添加资源(图片、音视频等)

方法共有三种:

  • 第一种

    spec.resources = [“Images/.png”, “Sounds/“]

  • 第二种

    spec.resource = “Resources/MYLibrary.bundle”

把资源都放在bundle中,然后打包时候这个bundle会直接拷贝进app的mainBundle中。使用的时候在mainBundle中查找这个bundle然后再搜索具体资源。

NSURL *bundleURL = [[NSBundle mainBundle] URLForResource:@"JZShare" withExtension:@"bundle"];
NSBundle *bundle = [NSBundle bundleWithURL:bundleURL];
UIImage *img = [UIImage imageNamed:icon inBundle:bundle compatibleWithTraitCollection:nil];
  • 第三种

    spec.resource_bundles = {
    ‘MyLibrary’ => [‘Resources/.png’],
    ‘OtherResources’ => [‘OtherResources/
    .png’]
    }

这种方法利用 framework 的命名空间,有效防止了资源冲突。
使用方法是先拿到最外面的 bundle,然后再去找下面指定名字 的 bundle 对象,再搜索具体资源。

NSBundle *bundle = [NSBundle bundleForClass:[MYSomeClass class]];
NSURL *bundleURL = [bundle URLForResource:@"MyLibrary" withExtension:@"bundle"];
NSBundle *resourceBundle = [NSBundle bundleWithURL: bundleURL];
UIImage *img = [UIImage imageNamed:icon inBundle:bundle compatibleWithTraitCollection:nil];

7.如果私有库添加了静态库或者dependency用了静态库

那么执行pod lib lint还有pod spec lint时候需要加上—user-libraries选项
否则会出现’The ‘Pods’ target has transitive dependencies错误。

8.如果私有库只引用其他库的subspec

只需要依赖想依赖的subspec,不用管主spec(因为依赖subspec必然要依赖主spec)。

9.私有库已经通过验证并传到私有repo也能通过pod search,但是就是pod install失败

这时候只要执行pod update 然后去喝杯水就好了。。。(前提是你把官方源换成国内的,不然从github上更新官方repo的速度你懂的。 更换官方源

flutter 环境搭建问题汇总


building flutter tool… 卡住

在命令行下,进入用户目录:执行命令 cd $HOME

打开.bash_profile文件:执行命令 open -e .bash_profile

文件打开后,开始编辑bash文件,在其中添加以下内容:

export PUB_HOSTED_URL=https://pub.flutter-io.cn
export FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
同时添加flutter相关工具到path中,也就是添加 Flutter SDK 安装的路径

export PATH=${PATH}:/Users/wujian/Flutter/flutter/bin:$PATH
关闭bash文件,配置完成后,更新配置的环境变量,执行命令: source .bash_profile
摘录于:https://zhuanlan.zhihu.com/p/48866181


集成文档
按照开发文档集成到当前项目时 方案1 pod install遇到

1
[!] No podspec found for `Flutter` in `../flutterModule/cbit_flutter/.ios/Flutter/engine`

先执行:flutter build ios 后再pod install


运行flutter的时候显示警告
Waiting for another flutter command to release the startup lock
复制代码当项目异常关闭,或者android studio用任务管理器强制关闭,下次启动就会出现上面的一行话,
此时需要打开 flutter/bin/cache/lockfile,删除就行了
或者直接用下面的命令:rm ./flutter/bin/cache/lockfile

作者:GabrielPanda
链接:https://juejin.im/post/6844903855751217160
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


mac自签证书

From: https://www.cnblogs.com/shuiche/p/13557475.html

前言

现在https大行其道, ssl又是必不可少的环节. 今天就教大家用开源工具openssl自己生成ssl证书的文件和私钥

环境

MAC电脑
openssl工具自行搜索安装

正文

  1. 终端执行命令

    //生成rsa私钥,des3算法,1024位强度,ssl.key是秘钥文件名。
    openssl genrsa -des3 -out ssl.key 1024

  1. 输入密码,这里会输入两次. 填写一样即可. 随意填写一个. 下一步就会删除这个密码
  1. 删除密码

    //终端执行删除密码命令
    //这里目录和生成私钥的目录一致
    openssl rsa -in ssl.key -out ssl.key

  1. 生成CSR(证书签名请求)
    我们根据根据刚刚生成的key文件来生成证书请求文件,终端执行如下命令:

    openssl req -new -key ssl.key -out ssl.csr

说明:执行以上命令后,需要依次输入国家、地区、城市、组织、组织单位、Common Name、Email和密码。其中Common Name应该与域名保持一致。密码我们已经删掉了,直接回车即可
温馨提示Common Name就是证书对应的域名地址. 我们开发微信小程序时必须要让我们的外链的https的域名和证书统一才行

  1. 生成自签名证书
    根据以上2个文件生成crt证书文件,终端执行下面命令:

    //这里3650是证书有效期(单位:天)。这个大家随意。最后使用到的文件是key和crt文件。
    openssl x509 -req -days 3650 -in ssl.csr -signkey ssl.key -out ssl.crt

到这里我们的证书就已经创建成功了.(ssl.key 和 ssl.crt) 可以直接用到https的server中了.

提示

需要注意的是,在使用自签名的证书时,浏览器会提示证书的颁发机构是未知的

navigationController系统的侧滑pop小记

navigationController系统的侧滑pop小记

系统的侧滑返回一般 我们会做自定义的base nav 做一些全局处理

1
2
3
4
5
6
7
8
9
navigationBar.backIndicatorImage = UIImage(named: "nav_back")
navigationBar.backIndicatorTransitionMaskImage = UIImage(named: "nav_back")
navigationBar.isTranslucent = true
navigationBar.barTintColor = UIColor.white
let attributes = [NSAttributedString.Key.foregroundColor:UIColor.black, NSAttributedString.Key.font:UIFont.systemFont(ofSize: 18)]
navigationBar.titleTextAttributes = attributes
// navigationBar.tintColor = UIColor.white
// navigationBar.setBackgroundImage(UIImage.imageFrom(color: UIColor.white) , for: UIBarMetrics.default)
navigationBar.shadowImage = UIImage()//隐藏线条

重写push

1
2
3
4
5
6
7
if viewControllers.count > 0 {
viewController.hidesBottomBarWhenPushed = true
}
//去掉backBarButtonItem的文字
let item = UIBarButtonItem(title: "", style: .plain, target: nil, action: nil)
self.topViewController?.navigationItem.backBarButtonItem = item
super.pushViewController(viewController, animated: animated)

然后一般 有些页面我们会重写leftBarButtonItem 会发现 系统自带的侧滑返回失效了
这个时候我们需要做一下处理恢复整个功能
在当前VC 中 (在使用中发现当前vc 销毁后 这个代理会影响原本默认的侧滑,所以放到自定义navVc 中, 去使用了, 且如果不需要侧滑 可以根据最上册的vc 是谁去做特别判断)

1
self.navigationController?.interactivePopGestureRecognizer?.delegate = self

并实现代理

1
2
3
4
5
///UIGestureRecognizerDelegate
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
//当导航控制器的子控制器个数 大于1 手势才有效
return true
}

即时通讯下数据粘包、断包处理实例(基于CocoaAsyncSocket)

From: https://www.jianshu.com/p/2e16572c9ddc

前言

本文旨以实例的方式,使用CocoaAsyncSocket这个框架进行数据封包和拆包。来解决频繁的数据发送下,导致的数据粘包、以及较大数据(例如图片、录音等等)的发送,导致的数据断包。

本文实例Github地址:即时通讯的数据粘包、断包处理实例

注:文章内容属于应用的范畴,内容相对简单易懂。给大家对数据包的处理提供了一个思路, 希望能抛砖引玉。
它是楼主CocoaAsyncSocket系列Read篇解析的一个前置插曲,至于详细的实现原理,作者会在后续的文章中写出。

正文
一、什么是粘包?

经常我们发现,如果用客户端同一时间发送几条数据,而服务端只能收到一大条数据,类似下图:

如图,由于传输的过程为数据流,经过TCP传输后,三条数据被合并成了一条,这就是数据粘包了。

那么为什么会造成粘包呢?

原来这是因为TCP使用了优化方法(Nagle算法)。
它将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。
这么做优点也很明显,就是为了减少广域网的小分组数目,从而减小网络拥塞的出现。

具体的内容感兴趣的可以看看这两篇文章:
TCP之Nagle算法&&延迟ACK
TCP NAGLE算法和实现

而UDP就不会有这种情况,它不会使用块的合并优化算法。
这里说到了就顺便提一下,由于它支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息)。

当然除了优化算法,TCP和UDP都会因为下面两种情况造成粘包:

  • 发送端需要等缓冲区满才发送出去,造成粘包
  • 接收方不及时接收缓冲区的包,造成多个包接收。
二、什么是断包?

断包应该还是比较好理解的,比如我们发送一条很大的数据包,类似图片和录音等等,很显然一次发送或者读取数据的缓冲区大小是有限的,所以我们会分段去发送或者读取数据。
类似下图:

无论是粘包还是断包,如果我们要正确解析数据,那么必须要使用一种合理的机制去解包。这个机制的思路其实很简单:

  • 我们在封包的时候给每个数据包加一个长度或者一个开始结束标记。
  • 然后我们拆包的时候就能区分每个数据包了,再按照长度或者分解符去分拆成各个数据包。

Talk is cheap. Show me the code

三、实例:基于CocoaAsyncSocket的封包,拆包处理。

开始动手之前,我们需要去理解下面这几个方法

//读取数据,有数据就会触发代理
- (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag;
//直到读到这个长度的数据,才会触发代理
- (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag;
//直到读到data这个边界,才会触发代理
- (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag;

还记得我们之前讲:[iOS即时通讯,从入门到“放弃”?][4]中提到过,这个框架每次读取数据,必须手动的去调用上述这些read方法,而我们之前的实现思路是,第一次连接成功的代理触发后调用:

[4]: https://www.jianshu.com/p/2dbb360886a8
  • (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag;

之后每次收到消息之后,都在去调用一次这个方法,超时为-1,即不超时。这样我们每次收到消息,都会即时触发我们读取消息的代理:

- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag

然而这么做显然没有考虑数据的拆包,如果我们一条一条的发送文字信息,自然没什么问题。如果我们一次发送数条,或者发送大图片。那么问题就出来了,我们解析出来的数据显然是不对的。

这时候我们就需要另外两个read方法了,一个是读取到指定长度,另一个是读取到指定边界。
我们通过自己定义的数据边界,去调用这两个方法,而触发的读取代理,得到的数据才是正确的一个包的数据。

所以我们的核心思路有了:
  1. 封包的时候给每个包的数据加一个标记,来标明数据的长度和类型(类型显然是需要的,我们需要知道它是文本、图片、还是录音等等,来用正确的方式处理这个数据)。
  2. 拆包的时候,先获取到我们给每个包的标记,然后根据标记的数据长度,去获取数据。最后再根据标记的类型去处理数据。(文字输出、图片展示、录音播放等等)。

接着我们可以开始动手了:
这里我们首先需要一个服务端,一个客户端。为了简单,我们都用OC来实现。

其中我们客户端用手机,服务端我们用Xcode模拟器。(由于Xcode只能同一时间运行一个模拟器…)

这里我们用客户端封包发送数据,然后服务端拆包解析数据。

我们先来看看客户端的代码:

static  NSString * Khost = @"10.10.100.48";
static const uint16_t Kport = 6969;
//建立连接
- (BOOL)connect
{
    return  [gcdSocket connectToHost:Khost onPort:Kport error:nil];
}

初始化略过了,大家可以看看github中的代码,这里需要说的是,为了连接上本机的服务端,我们这里的host为服务端的IP地址:

端口为6969(只需和服务端accpet端口一致即可)。

注意:如果大家要运行github上的demo,只需修改这个host地址即可,把它改成你电脑(服务端)的IP地址。

接着我们来看看write方法,我们在该方法中进行封包:

//发送消息
- (void)sendMsg
{
    NSData *data  = [@"你好" dataUsingEncoding:NSUTF8StringEncoding];
    NSData *data1  = [@"猪头" dataUsingEncoding:NSUTF8StringEncoding];
    NSData *data2  = [@"先生" dataUsingEncoding:NSUTF8StringEncoding];


    NSData *data3  = [@"今天天气好" dataUsingEncoding:NSUTF8StringEncoding];
    NSData *data4  = [@"吃饭了吗" dataUsingEncoding:NSUTF8StringEncoding];

    [self sendData:data :@"txt"];
    [self sendData:data1 :@"txt"];
    [self sendData:data2 :@"txt"];
    [self sendData:data3 :@"txt"];
    [self sendData:data4 :@"txt"];

    NSString *filePath = [[NSBundle mainBundle]pathForResource:@"test1" ofType:@"jpg"];

    NSData *data5 = [NSData dataWithContentsOfFile:filePath];

    [self sendData:data5 :@"img"];
}

- (void)sendData:(NSData *)data :(NSString *)type
{
    NSUInteger size = data.length;

    NSMutableDictionary *headDic = [NSMutableDictionary dictionary];
    [headDic setObject:type forKey:@"type"];
    [headDic setObject:[NSString stringWithFormat:@"%ld",size] forKey:@"size"];
    NSString *jsonStr = [self dictionaryToJson:headDic];


    NSData *lengthData = [jsonStr dataUsingEncoding:NSUTF8StringEncoding];


    NSMutableData *mData = [NSMutableData dataWithData:lengthData];
    //分界
    [mData appendData:[GCDAsyncSocket CRLFData]];

    [mData appendData:data];


    //第二个参数,请求超时时间
    [gcdSocket writeData:mData withTimeout:-1 tag:110];

}
- (NSString *)dictionaryToJson:(NSDictionary *)dic
{
    NSError *error = nil;
    NSData *jsonData = [NSJSONSerialization dataWithJSONObject:dic options:NSJSONWritingPrettyPrinted error:&error];
    return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
}

总共上述两个方法,也很简单,我们发送了6条数据,前5条为文本形式,最后一条是一个20多M的图片。当我们点击发送的时候会触发这个方法,这6条数据会被同时发出。

这里我们来看看我们是如何封包的:

  • 我们定义了一个headDic,这个是我们数据包的头部,里面装了这个数据包的大小和类型信息(当然,你可以装更多的其他标识信息。)然后我们把它转成了json,最后转成data
  • 然后我们把这个head拼在最前面,接着拼了一个:

    [GCDAsyncSocket CRLFData]

这个是什么呢?其实它就是一个\r\n。我们用它来做头部的边界。(又或者我们可以规定一个固定的头部长度,来作为边界,这里仅仅是提供给大家一个思路)。

  • 最后我们把真正的数据包给拼接上。

注:如果你想的更远的话,甚至可以在结尾,再拼一个包结束的标识符,后面我们会讲到为什么可以这么做。这里暂时先这样。

就这样,我们完成了数据的封包和发送。

客户端有了,接着我们来看看服务端是如何来拆包的:

首先我们需要监听本机6969端口。(完整代码可以见github)

static const uint16_t Kport = 6969;

//等待连接
- (BOOL)accept
{
    NSError *error = nil;

    BOOL isSuccess =   [gcdSocket acceptOnPort:Kport error:&error];
    if (isSuccess) {
        NSLog(@"监听成功6969端口成功,等待连接");
        return YES;
    }else{
        NSLog(@"监听失败,原因:%@",error);
        return NO;
    }
}

当客户端连接上来后,调用成功接收到客户端连接的代理方法:

- (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket
{
    NSLog(@"接受到socket连接");

    [_sockets addObject:newSocket];
    [newSocket readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:110];
}

这里需要注意的是,成功接收到连接后,调用代理我们必须把新生成的这个newSocket保存起来,如果它被销毁了,那么连接就断开了,这里我们把它放到了一个数组中去了。
这里需要注意的是,成功连接后,我们就调用了:

[newSocket readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:110];

还记得我们封包的时候,数据包头部之后拼了这么一个分解符data。这样,当有数据包传输过来我们就能获取到这个数据包的头部(后面的信息先不读取)。

接着我们来看看服务端的read代理方法是如何拆包的:

- (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag
{
    //先读取到当前数据包头部信息
    if (!currentPacketHead) {
        currentPacketHead = [NSJSONSerialization
                             JSONObjectWithData:data
                             options:NSJSONReadingMutableContainers
                             error:nil];
        if (!currentPacketHead) {
            NSLog(@"error:当前数据包的头为空");

            //断开这个socket连接或者丢弃这个包的数据进行下一个包的读取

            //....
            return;
        }        

        NSUInteger packetLength = [currentPacketHead[@"size"] integerValue];

        //读到数据包的大小
        [sock readDataToLength:packetLength withTimeout:-1 tag:110];

        return;
    }


    //正式的包处理
    NSUInteger packetLength = [currentPacketHead[@"size"] integerValue];
    //说明数据有问题
    if (packetLength <= 0 || data.length != packetLength) {
        NSLog(@"error:当前数据包数据大小不正确");
        return;
    }

    NSString *type = currentPacketHead[@"type"];

    if ([type isEqualToString:@"img"]) {
        NSLog(@"图片设置成功");
        self.recvImg.image = [UIImage imageWithData:data];
    }else{

        NSString *msg = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
        NSLog(@"收到消息:%@",msg);
    }

    currentPacketHead = nil;

    [sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:110];
}

这个方法也很简单,我们判断,如果currentPacketHead(当前数据包的头部)为空,则说明这次读取,是一个头部信息,我们去获取到该数据包的头部信息。并且调用下一次读取,读取长度为从头部信息中取出来的数据包长度:

[sock readDataToLength:packetLength withTimeout:-1 tag:110];

这样当GCDAsyncSocket中数据缓冲区长度达到我们需要读取的length就能触发代理方法的第二次回调。(具体原理实现会在楼主的GCDAsyncSocket解析的后续系列Read篇中去讲,敬请期待)。
这时候因为currentPacketHead不为空,所以我们就知道是去获取一个数据包,我们从头部信息中拿到数据包的类型,如果是文本或者图片,则分别输出或展示到屏幕上。读取完成后我们再次调用:

[sock readDataToData:[GCDAsyncSocket CRLFData] withTimeout:-1 tag:110];

这样就开始了下一个数据包的头部信息读取。
就这样,整个数据拆包的处理就完成了

接着我们来讲讲我们之前所说的为什么可以在数据包之后加一个结束标识符。我们数据很可能在传输的过程中,丢失了一部分,或者头部信息不可读,导致我们无法正常读取这个数据包。
可能我们会有一个应用场景,当出现错误包的时候,我们就直接抛弃掉它,直接开始下一个数据包的读取(当然现实中,我们往往是需要重新发送,这里仅仅是举一个应用场景)。这样这个结束标识符就起作用了,我们可以直接把数据读取到这个错误包的结束标识处,不做任何处理,这样相当于丢弃掉这个错误包了。

最后我们来看看运行效果:

我们客户端手机连接上服务器后,点击发送,发出我们上述客户端写的6条数据,在我们服务端,按照顺序接受到数据如图:

写在结尾:

本来不打算写应用篇的,但是很多朋友在问数据包相关的内容,而且正好之后的Read篇会涉及到这些,所以就当为了后面的内容做一个铺垫吧。

关于IM的路还有很长,路漫漫其修远兮,吾将上下而求索。