特别感谢团队小伙伴(李龙龙、崇庆旭)
越小的团队求变化,越大的团队求稳定。作为一个小团队我们又开始折腾了。不用再赘述为什么要转swift。唯一能阻止我们的问题是:
target has transitive dependencies that include static binaries
这个问题来自于使用cocoapods组件化开发。使用swift时必须use_frameworks!
,然后我们不得不依赖一些第三方库,这些第三方库是static library(哪些库就不点名了,大家心里都应该有数)。pod install
时就能看到上面的内容了。
本来想先分析下问题的原因的,但是怕各位看官没有心思看。所以先讲解决,再分析吧。分别有上中下策任君选择。
上策
等cocopods官方的1.4.x正式版,会提供static_framework
CocoaPods version 1.3.1 and earlier do not support static framework dependencies.CocoaPods 1.4.0 adds the
static_framework
option in #6811 that enables you to specify building a pod as a static_framework, which unlike dynamic frameworks, can have static framework dependencies.
上策的好处是官方的,改动最小。
中策
如果你等不起官方,可以使用这个方案。这个方案写的非常非常之详尽,你一定能额外收获不少知识。
这个问题的思路简单讲就是用一个动态库吸附一个静态库,然后因为动态库的隔离性巧妙地解决了这个问题。
亲测,可以。但是在做的时候遇到了小问题,分享一下。
第一个问题是出现了warning:
Missing sub module
可能是我们理解能力有问题,一开始没有解决。特此分享一下,也许大家一看就明白怎么做了。
比如包装了一个XXX.framework。里面的Headers,有一个XXX.h。需要在XXX.h中导入其他头文件。
XXX.framework
├── Headers
│ ├── XXX.h
│ ├── WXApi.h
│ ├── WXApiObject.h
│ └── WechatAuthSDK.h
├── Info.plist
├── XXX
├── Modules
│ └── module.modulemap
└── _CodeSignature
└── CodeResources
XXX.h需要这样
#import <XXX/WXApi.h>
#import <XXX/WXApiObject.h>
#import <XXX/WechatAuthSDK.h>
第二个问题是:
直接发cocopods库发出来,千万不要直接放入到一个组件项目中直接用。这样会找不到头文件。
中策的好处是还可以用组件化开发的方式依赖包装过的第三方库。改动也很小,但是需要做很多体力活包库发库。
下策
我们发现在主项目的podfile依赖第三方库(static library),用use_framework!不会有问题,原因后面分析。所以我们想把这些第三方库都放在主项目(App)中。要解决的是其他组件如何使用这些第三方库。
其实思路很简单,就是组件化开发中的业务组件相互隔离通过中间件通讯。我们用的是协议的方式。用协议的好处是比较优雅,协议方法等照抄第三库就可以了。
首先。我们创建了一个ModuleManager类取名叫做NBUtilProtocolManager,这个manager主要负责收集协议和下发协议。这个manager就提供两个方法在NBUtilProtocolManager.h中
#import <Foundation/Foundation.h>
@interface NBUtilProtocolManager : NSObject
+ (void)registServiceProvide:(_Nonnull id)provide forProtocol:(nonnull Protocol *)protocol;
+ (_Nonnull id)serviceProvideForProtocol:(nonnull Protocol *)protocol;
@end
在NBUtilProtocolManager.m中
#import "NBUtilProtocolManager.h"
@interface NBUtilProtocolManager ()
@property (nonatomic, strong) NSMutableDictionary *serviceProvideSource;
@end
@implementation NBUtilProtocolManager
+ (NBUtilProtocolManager *)sharedInstance
{
static NBUtilProtocolManager * instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
- (instancetype)init
{
self = [super init];
if (self) {
_serviceProvideSource = [[NSMutableDictionary alloc] init];
}
return self;
}
+ (void)registServiceProvide:(id)provide forProtocol:(Protocol *)protocol
{
if (provide == nil || protocol == nil)
return;
[[self sharedInstance].serviceProvideSource setObject:provide forKey:NSStringFromProtocol(protocol)];
}
+ (id)serviceProvideForProtocol:(Protocol *)protocol
{
return [[self sharedInstance].serviceProvideSource objectForKey:NSStringFromProtocol(protocol)];
}
@end
manager创建完成后发布,作为基础组件给个个业务组件使用。
有个这个manager以后,我们就开始定义一个协议NBShareSDKProtocol.h这个协议里面有一个分享方法,协议里面的方法无论是返回值还是参数都是我们自己组件能识别的 和shareSDK没有关系.代码如下
- (nonnull RACSignal *)shareContentWithInfo:(nonnull NSDictionary *)info;
协议定义完成后发布,作为基础组件给个个业务组件使用。
这个时候我们就开始定义一个NBShareSDKObject的类来实现协议中的方法。代码如下
@interface NBShareSDKObject () <NBShareSDKProtocol>
@end
@implementation NBShareSDKObject
@synthesize defaultShareImageUrl;
+ (void)load
{
[NBUtilProtocolManager registServiceProvide:[[self alloc] init] forProtocol:@protocol(NBShareSDKProtocol)];
}
- (nonnull RACSignal *)shareContentWithInfo:(nonnull NSDictionary *)info {
return ....;
}
注意在这个类中的load方法中使用NBUtilProtocolManager将这个协议的实现者给加入进去。load 方法会在加载类的时候就被调用,也就是 ios 应用启动的时候,就会加载所有的类,就会调用每个类的 + load 方法。在这个时机注入协议的实现者是比较合适的时机。这样的话在项目启动的时候manager里面就已经包含了这个协议的实现者。协议的实现都在主项目里面完成.这样所有的准备工作都已经完成。接下来就是如何使用的问题。
在业务组件的podfile中我们要依赖NBUtilProtocolManager和NBShareSDKProtocol这个自己发布的库。然后在代码中导入头文件后按照如下代码:
id <NBShareSDKProtocol> obj = [NBUtilProtocolManager serviceProvideForProtocol:@protocol(NBShareSDKProtocol)];
[obj shareContentWithInfo:@{@"url" : @"www.baidu.com"}];
下策是保底方案。修改多,体力活多。
这个下策也是这个issue的下策解决方案
Pod lint fails when containing dynamic-frameworks without simulator architectures
这个问题的上策是等官方PR最终发出来。
中策是用skip-import-validation
。
下策的方案本质是隔离。不经感叹组件化开发的本质也是隔离。代码隔离,逻辑隔离,业务隔离,人员隔离,开发隔离,发布隔离。对内自洽,对外隔离。
分析
target has transitive dependencies that include static binaries
这个错误,owner早在2014.11.24在这个issue下进行了说明。
Static libraries and frameworks are only linked to the user target by classic CocoaPods. That approach will not work if we are building frameworks and have a vendored static library as a
because the linker expects all symbols to be resolved in this case.
Using -undefined dynamic_lookup or linking to both the dynamic framework and the user target could be solutions to pursue for the future, but as a first step, we should reject these cases.
To reject vendored_libraries we can simply use the file extension, for vendored_frameworks we have to check if the included binary is statically or dynamically linked.
owner鲜明的指出了"我们应当拒绝这种情况"。最后一段讲了:对于vendored_libraries(.a),我们可以直接判断文件后缀名,对于framework,需要检查是statically or dynamically。我们如果需要自己检查的话可以使用file xxx.framework/xxx
第一段的问题用图来讲解
还有个问题是, umbrella header中有传递头文件。
I guess, that happens because public headers are imported by the umbrella header and will have implicit clang module exports. That also includes transitive imported headers. If the imported header is not within the same target, it can't probably be exported as nested module, if it isn't already part of a clang module itself.
为什么在podfile依赖中可以,在pod spec中不可以呢?因为pod spec中写dependency是transitive dependencies。
图不一定很准确,大概是这个意思。
那为什么动态库依赖动态库就可以呢?具体理论可以参考这篇博文(也就是中策)中说的动态库的隔离性,和动态库在链接时不copy而是在启动时交给dyld。
为什么中策可以呢?来个图。
最后
苹果官方在XCode9支持了static swift library。这说明swift版本已趋于稳定了。附上链接在Building and Linking章节。也许也是因为这一点cocoapods官方也开始想要提供static_framework
这个配置吧。
没有什么点可以阻止我们了,让我们开始写swift吧。