用Static Library进行代码组织的一次实践

写在前面

还是在看喵神那篇讲Apple Watch开发的文章中演示的Demo,注意他里面用到了Framework来管理代码。虽然当时重点是Apple Watch的开发,但是由于本人热衷代码结构设计,所以当时记忆很深刻^_^,想着以后可以试一下。正巧,产品在当前版本中要再加个Today插件,由于数据的存储和获取与之前的Today插件一致,只是界面展示不同而已,但是也必须要再创建一套Target。所以我就想尝试一下用Framework来打包数据层的代码,这样以后再多加Today插件,也是很轻松就搞定的事儿。

嗯,我知道的,可以选择每个.m所属的Target,但是随着项目中的Target越来越多,你就知道这有多痛苦了。犹记得忘记给某个.m文件选上某个Target而苦苦调试了n久…。

正好这个版本的开发时间还不算紧,我决定干脆把整个项目的代码都重新梳理一下。折腾这一次,你会发现自己原来的代码存在多少不合理的耦合。但还是友情提醒一下,真的要做好心里准备,因为这个过程真的不容易,而且你不能半途而废。还有一定要跟产品和测试报备,好争取时间和保证项目没被改出问题=。=

用Static Library封装代码的好处

  • 项目结构清晰

项目里哪些是共用的代码,是在哪些Target中共用的代码,代码的分层结构,都一目了然。这样就算一个新加入的伙伴,也能比较快的了解项目,而且基本不用你给他讲=。=

  • 避免选择.m文件所在的Target,而带来的bug隐患

一种情况是,当项目中增加了一个Target时,你要到文件里一个一个去添加,难免就漏掉了几个.m没选。

一种情况是,你删除了某个类,删除的时候却没有注意它所属的Target,然后运行当前的Target还是正常的。比如我们项目有2套Target,一套是AppStore的,一套是Enterprise的。那你留给其他Target的这个bug,就不吉岛什么时候能发现了= =!

  • 有助于约束项目的代码规范

Static Libray允许你只暴露你想给别人使用的类,把不想让别人使用的类保护起来,这样就避免了淘气的Parter会随意调用不必要的类,以至于破坏了整个项目的层次结构,产生了不合理的耦合。如果即使用了Static Library,还要随意到xxLibrary中将自己想使用的类改成Public的,那真是不能一起愉快的写代码了= =

  • 降低耦合度

总体上,达到了一种单向依赖的关系,封装好的Static Library完全可以移出去用在其他的项目中。而且我在梳理代码的过程中,发现了很多环状的引用,虽然import允许这样做,但是这会拖累编译时间。所以在.h中,非必要时还是用@class。还有就是common、constant这样的类,真的是重构的重灾区。即使常量也还要按照类别放到不同的类中去。此外,如果以后项目要从Objective-C转移到Swift上,可以一个Kit一个Kit的进行重写,由于与主工程脱离开了,所以调试也会轻松。

我最终抽出的几个Kit

  • KingWeatherKit

这个Kit的目标是用在主App和Today插件中。其中封装了DB层、网络层、Model层、还有主App和Today插件会共用的一些代码,比如xxUtil类等。每往这个Kit里放代码时,要反复问自己,是必须的吗?因为我们不能把它最后给搞的很臃肿。有些类里的代码其实只有一部分是公用的,一部分只是为了适应很独特的场景,这时就要做适当的拆封了。

  • KingWeatherTodayKit

这个Kit的目标是用在Today插件中。其中封装了拿到数据后的业务逻辑处理。它会引用KingWeatherKit中的代码。

  • KingWeatherWatchKit

这个Kit的目标是用在Watch App中,主要封装了数据处理部分,和Util类。无法创建一个Framework或Static Library,即能在iOS App中用,又能在watchOS App中用,所以我创建了一个iPhoneAndWatch的Groups,来放置共用的代码,这块没办法了,只能勾选.m所在的Target,让其同时属于KingWeatherKit和KingWeahterWatchKit。这样这部分代码就始终都是属于这2个Target的了,其他地方要用,通过这2个Kit去用。

踩到的几个坑

  • 无法创建一个Kit在iOS与watchOS2上共用

这一点要注意一下,一开始不知道,我就想创建一个Kit同时在iOS和watchOS上使用,后来发现watchOS有自己的Framework和Static Library。也可以理解,从watchOS2开始watch app脱离iPhone直接在watch上运行了。watchOS1是否可以创建一个Kit同时在iOS和watchOS上运行我没有实验,所以暂时打个问号在这= =。

  • 同一个Project中的类都是可以访问到的,即使将这些类打包到了Framework或Static Library中

iOS8开始多了一个Cocoa Touch Framework,我开始重构时就直接在当前工程中创建了它,但是调试到后面时发现,即使我放在Private中的文件,也是可以被随意引用到的。原来在一个Project中的类都是可以调用到的,不管是放在Framework中还是放在Static Library中。无奈只能再弄一个Project来专门放置这些Kit了。

至于是用Static Libray还是用Framework,因为从iOS8开始苹果才允许用纯粹的Framework,所以如果项目是从iOS8开始支持的,当然是用Framework好,毕竟Framework是动态加载的。因为我们项目还是支持iOS7的,还是不太放心用Framework是否会被拒,所以最后我用了Static Library。

  • 涉及到AppGroups的地方该如何处理

AppGroups在封装librarys的project中是无法识别到的,在我们的项目中有2处涉及到了AppGroups,一处是NSUserdefaults,一处是databasePath。经过一番尝试,我最终采取的办法是,在主工程中创建了一个AppGroupUtils类来返回根据AppGroups创建好的NSUserDefaultsdatabasePath。与这两者有关的具体工作还是放到library中去做,但是在主工程中使用前进行赋值。例如NSUserDefaults在library中如下处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@implementation UserDefaultsUtils

static NSUserDefaults *_userDefaults = nil;

+ (NSUserDefaults *)userDefaults {
NSAssert(_userDefaults != nil, @"使用XXStorage之前,请先给userDefaults赋值!");
return _userDefaults;
}

+ (void)setUserDefaults:(NSUserDefaults *)userDefaults {
static dispatch_once_t predicate;
dispatch_once(&predicate, ^{
_userDefaults = userDefaults;
});
}

+ (void)saveValue:(id) value forKey:(NSString *)key {
NSUserDefaults *userDefaults = [self userDefaults];
[userDefaults setObject:value forKey:key];
[userDefaults synchronize];
}

在主工程中:

1
2
//一定要在所有的代码运行之前执行这句话,否则storage不能正常用
[UserDefaultsUtils setUserDefaults:[AppGroupUtils userDefaults]];

  • 主工程编译的时候,使封装好的Kit跟着一起编译

这一点很重要,这样就可以在改动了Kit中的代码时,不用单独编译一次Kit了,而是直接运行主工程就可以看到结果了。操作步骤如下:

Edit Scheme -> Build -> 添加编译时需要同时编译的Target

  • CocoaPods同时作用于多个Project

抽离出来的Librarys Project和主Project同时都需要通过CocoaPods来引用第三方库,只要在Podfile中处理就可以了,并不会每一个Project同时又跟一个Pods Project哦=。= 。Podfile的写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
workspace 'MyLibraryDemo.xcworkspace'
platform :ios, '7.0'

inhibit_all_warnings!

xcodeproj 'MyLibraryDemo.xcodeproj'
xcodeproj 'MyLibrarys/MyLibrarys.xcodeproj'

target :MyLibraryDemo do
xcodeproj 'MyLibraryDemo'
pod 'FMDB'
end

target :LibraryOne do
xcodeproj 'MyLibrarys/MyLibrarys.xcodeproj'
pod 'SDWebImage'
end

target :LibraryTwo do
xcodeproj 'MyLibrarys/MyLibrarys.xcodeproj'
pod 'MJRefresh'
end

至此,总结完毕,欢迎拍砖 (♥◠‿◠)

–End–

以上如果有说的不严谨的地方,希望在下面的留言中指出哦,如果对你有一些帮助,也希望能在下面的留言中给一些鼓励^_^