iOS开发分享架构自己看的文章

【iOS】使用workspace搭建SDK开发框架

2018-11-08  本文已影响117人  子天々君

前言

SDK开发和APP并不一样,APP开发简单点直接开个项目撸就是了,但是SDK需要打包成库,然后才能拿这个库去用。所以,SDK开发一般都需要创建3个项目:SDK项目、测试项目(自己单元测试用的)、demo项目(给用户看的,要有完整的使用代码)。
如果每个项目都是独立的,那么就需要打包好库,然后放到测试项目里去测试,测试好了再搞到demo里,这样子太麻烦了,还很low。
其实苹果对于这种情况早就有解决方案了,那就是workspace,即工作空间。

workspace允许你把多个项目放到一个工程里,他们既是独立的,也能有所联系。正是这种特性使得我们能快速开发而不需要过多的考虑其它。



接下来我会给大家演示怎么用workspace来搭建开发SDK的架构

库的选用 -- 为什么用Framework

综上所述,如果是.a的话,资源和头文件与库就会很零散,被弄乱了都不知道;而framework可以很好的把一个所需的文件库集合在一起,故而选framework更为友好。

开始搭建框架

创建workspace


先在桌面创建一个文件夹,我把它命名为iOS(因为创建workspace是不会像创建项目一样自动帮你生成文件夹)。
打开Xcode,选择File-New-Workspac,名字也命名为iOS。然后选择我们刚才创建的iOS文件夹,点击保存。一个空的workspace创建好了。

创建workspace.png

创建测试项目


选择File-New-Project-Single View App,名字命名为SDKTest。然后选择我们刚才创建的iOS工作空间,点击创建。一个空的测试项目创建好了。

创建SDKTest.png

创建demo项目


选择File-New-Project-Single View App,名字命名为SDKDemo。然后选择我们刚才创建的iOS工作空间,点击创建。一个空的demo项目创建好了。

创建SDKDemo.png

创建SDK项目


选择File-New-Project-Cocoa Touch Framework,命名为SDK。然后选择我们刚才创建的iOS工作空间,点击创建。一个空的SDK项目创建好了。

创建SDK.png

项目结构一览

项目结构.png

创建脚本target


单单只是有上面的项目还是不行的,无法实现联调,现在我的需求是,当运行SDKDemo或者SDKTest时,Xcode自动帮我编译好SDK。为了实现这个需求,要用到脚本。

我们选中SDK项目,点击File-New-Target,选中Cross-platform,然后选择Aggregate,命名为SDKBuildScript,点击完成。

创建SDKBuildScript.png

点击File-New-File,选择Shell Script,命名为SDKBuild,点击创建(不要把它添加到Targets,不然会被编译到Framework里的)。

创建SDKBuild.png

把这段脚本复制到SDKBuild中,其中TARGET_NAME默认是当前项目名,如果你的target名字和项目名不一样,请修改TARGET_NAME为正确的target名字;OUTPUT_FOLDER、TEST_FOLDER、DEMO_FOLDER也是根据你实际情况来赋值的。其它的不需要修改,能直接用。
完成脚本编写后,把Xcode里的SDKBuild文件删了(但不要移除到废纸篓)。

# 要build的target名
TARGET_NAME="${PROJECT_NAME}"

# 存放Framework的路径路径
OUTPUT_FOLDER="${SRCROOT}/${PROJECT_NAME}/"
TEST_FOLDER="${SRCROOT}/../SDKTest/SDKTest"
DEMO_FOLDER="${SRCROOT}/../SDKDemo/SDKDemo"

# 编译路径
IPHONE_DIR="build/Release-iphoneos/${TARGET_NAME}.framework"
SIMULATOR_DIR="build/Release-iphonesimulator/${TARGET_NAME}.framework"

# 删除之前的Framework文件
rm -rf "${OUTPUT_FOLDER}/${TARGET_NAME}.framework"
rm -rf "${TEST_FOLDER}/${TARGET_NAME}.framework"
rm -rf "${DEMO_FOLDER}/${TARGET_NAME}.framework"

# 分别编译模拟器和真机的Framework
xcodebuild -configuration "Release" -target "${TARGET_NAME}" -sdk iphoneos clean build
xcodebuild -configuration "Release" -target "${TARGET_NAME}" -sdk iphonesimulator clean build

# 拷贝Framework到目录
cp -R "${IPHONE_DIR}" "${OUTPUT_FOLDER}"

# 合并Framework
lipo -create -output "${IPHONE_DIR}/${TARGET_NAME}" "${SIMULATOR_DIR}/${TARGET_NAME}" "${OUTPUT_FOLDER}/${TARGET_NAME}.framework/${TARGET_NAME}"

# 拷贝Framework到别的工程
cp -R "${OUTPUT_FOLDER}/${TARGET_NAME}.framework" "${TEST_FOLDER}"
cp -R "${OUTPUT_FOLDER}/${TARGET_NAME}.framework" "${DEMO_FOLDER}"

# 删除编译之后生成的无关的配置文件
dir_path="${OUTPUT_FOLDER}/${TARGET_NAME}.framework/"
for file $(ls "${dir_path}"|tr " " "?") # 解决名字带空格的问题
do
if [[ "${file}" =~ ".xcconfig" ]]
then
rm -f "${dir_path}/${file}"
fi
done

# 判断build文件夹是否存在,存在则删除
if [ -d "${SRCROOT}/build" ]
then
rm -rf "${SRCROOT}/build"
fi  

点击SDK项目,然后在TARGETS里选中SDKBuildScript,上面选中Build Phases,点击左上角的“+”号,选择New Run Script Phase

添加Script Phase.png

在黑框里输入./SDKBuild.sh

黑框.png

当然你也可以直接把脚本写在黑框里,这样子就不需要创建脚本文件了。
如果使用脚本文件的话,会报没权限的错误,所以需要使用命令行来打开权限。(因为使用文件方便管理和编写代码,所以这里我选择了使用文件的方式
打开命令行,cd到SDKBuild.sh所在的目录,然后执行sudo chmod +x SDKBuild.sh即可。

使用Bundle管理资源


当我们的库需要用到一些资源时,如果资源分散乱放,对使用者而言是件很痛苦的事,所以我们需要把资源集中到一起,这个时候bundle就很有用了。

如何创建Bundle我就不说了,网上大把的资料,我主要说的是使用Bundle的一些细节。
目前普遍使用Bundle的方式都是把Bundle作为一个独立体的存在,即打包好Bundle,然后把Bundle和库一起给开发者去使用,这样子开发者就能使用到我们打包好的资源了。但是这里有个问题,那就是资源可能会被替换,从而引发未知问题。我们使用framework就是为了资源和代码成为一个整体,也就是说,开发者只需要导入framework就能使用到Bundle的资源了。
基于此,所以这里对“Bundle作为一个独立体的存在”这种情况不讨论了,网上也一大把资料。
我曾经使用很多方法尝试了获取framework里Bundle的资源,但都失败了,虽然Bundle确实存在于framework里,但是怎么都读取不出来。
然后我读了这篇文章:iOS:NSBundle的一些理解
我终于知道了,静态库是拿不到该Bundle的资源的,所以只能使用动态库(iOS8开始,动态库也能上架了)。
经验证,在动态库里确实是能读取到资源的,以读取图片为例,有3种方式:

NSBundle *resourceBundle = [NSBundle bundleForClass:[SDKData class]]; // 获取类所在的bundle
NSString *bundlePath = [resourceBundle pathForResource:@"SDK" ofType:@"bundle"]; // 获取资源bundle路径

    // 方式1 直接拼路径
//    NSString *imagePath = [bundlePath stringByAppendingPathComponent:@"user.jpg"];
//    UIImage *image = [UIImage imageWithContentsOfFile:imagePath];
    
    // 方式2 通过获取bundle来操作
//    NSBundle *bundle = [NSBundle bundleWithPath:bundlePath];
//    NSString *imagePath = [bundle pathForResource:@"user.jpg" ofType:nil];
//    UIImage *image = [UIImage imageWithContentsOfFile:imagePath];

    // 方式3 通过传入bundle来获取数据
NSBundle *bundle = [NSBundle bundleWithPath:bundlePath];
UIImage *image = [UIImage imageNamed:@"user.jpg" inBundle:bundle compatibleWithTraitCollection:nil];

NSLog(@"%s  image = %@", __func__, image);
  • 如果动态库编译报错,在SDK的General - Linked Framework and Librarles里点击“+”号,搜索libSystem并添加进去。



一个简单的workspace工程就完成了,接下来说说SDK的一些配置、库的使用和workspace联调的一些注意事项

项目配置

SDK项目配置


可选配置



这些配置不是SDK必须的,但可以根据实际需求进行配置。

Level of Debug Symbols有3个值,分别是:

  • used:只引用符号
  • full:所有符号
  • default:使用编译器默认值

Strip Style表示的是我们需要去除的符号的类型的选项,其分为三个选择项:

  • All Symbols:去除所有符号,一般是在主工程中开启。
  • Non-Global Symbols:去除一些非全局的Symbol(保留全局符号,Debug Symbols同样会被去除),链接时会被重定向的那些符号不会被去除,此选项是静态库/动态库的建议选项。
  • Debug Symbols:去除调试符号,去除之后将无法断点调试。

iOS的调试符号是DWARF格式,相关概念如下:

  • Mach-O: 可执行文件,源文件编译链接的结果。包含映射调试信息(对象文件)具体存储位置的Debug Map。
  • DWARF:一种通用的调试文件格式,支持源码级别的调试,调试信息存在于对象文件中,一般都比较大。Xcode调试模式下一般都是使用DWARF来进行符号化的。
  • dSYM:独立的符号表文件,主要用来做发布产品的崩溃符号化。dSYM 是一个压缩包,里面包含了DWARF文件。使用Xcode编译打包的时候会先通过可执行文件的Debug Map获取到所有对象文件的位置,然后使用dsymutil来将对象文件中的DWARF提取出来生成dSYM文件。

通用配置


以上的配置只是针对SDK的配置,这里的配置是针对所有项目的配置(即SDK和APP项目)。

附上我pch文件的一些定义。

#ifndef PrefixHeader_SDK_pch
#define PrefixHeader_SDK_pch

#ifdef __OBJC__ // 防止非OC文件包含OC的头文件而引发的编译报错
// OC相关的应该在这里包含
// #import "Tools.h"  // OC的工具类

// frame相关
#define kScreenHeight [[UIScreen mainScreen] bounds].size.height // 物理屏幕高度
#define kScreenWidth [[UIScreen mainScreen] bounds].size.width   // 物理屏幕宽度
#define kIsFullScreen ((([[[UIDevice currentDevice] systemVersion] floatValue] >= 11.0f) && ([[[[UIApplication sharedApplication] delegate] window] safeAreaInsets].bottom > 0.0))? YES : NO) // 判断是否全面屏
#define kIsiPhoneX CGSizeEqualToSize(CGSizeMake(1125, 2436), [[UIScreen mainScreen] currentMode].size) // 判断是否是iPhone X
#define kStatusBarHeight (kIsFullScreen ? 44.f : 20.f)       // 状态栏高度
#define kNavigationBarHeight (kIsFullScreen ? 88.f : 64.f)   // 导航栏高度
#define kTabBarHeight (kIsFullScreen? (49.f + 34.f) : 49.f)  // tabBar高度
#define kHomeIndicatorHeight (kIsFullScreen ? 34.f : 0.f)    // home指示器高度

// 颜色相关
#define kRGBA255(R, G, B, A) [UIColor colorWithRed:((R) / 255.0f) green:((G) / 255.0f) blue:((B) / 255.0f) alpha:(A)]
#define kRGBA(R, G, B, A) [UIColor colorWithRed:(R) green:(G) blue:(B) alpha:(A)]
#define kRGB255(R, G, B) [UIColor colorWithRed:((R) / 255.0f) green:((G) / 255.0f) blue:((B) / 255.0f) alpha:1.0f]
#define kRGB(R, G, B) [UIColor colorWithRed:(R) green:(G) blue:(B) alpha:1.0f]
#define kUIColorFromRGB(rgbValue) [UIColor colorWithRed:((float)((rgbValue & 0xFF0000) >> 16))/255.0 green:((float)((rgbValue & 0xFF00) >> 8))/255.0 blue:((float)(rgbValue & 0xFF))/255.0 alpha:1.0] // 0xf8ff格式(16进制格式)
#define UIColorFronHSB(h, s, b) [UIColor colorWithHue:h saturation:s brightness:b alpha:1.0f]
#define UIColorFronHSBA(h, s, b, a) [UIColor colorWithHue:h saturation:s brightness:b alpha:a]

// 定义通用颜色
#define kBlackColor         [UIColor blackColor]
#define kDarkGrayColor      [UIColor darkGrayColor]
#define kLightGrayColor     [UIColor lightGrayColor]
#define kWhiteColor         [UIColor whiteColor]
#define kGrayColor          [UIColor grayColor]
#define kRedColor           [UIColor redColor]
#define kGreenColor         [UIColor greenColor]
#define kBlueColor          [UIColor blueColor]
#define kCyanColor          [UIColor cyanColor]
#define kYellowColor        [UIColor yellowColor]
#define kMagentaColor       [UIColor magentaColor]
#define kOrangeColor        [UIColor orangeColor]
#define kPurpleColor        [UIColor purpleColor]
#define kClearColor         [UIColor clearColor]

// 路径相关
#define kDocumentPath [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject] // 获取沙盒Document路径
#define kTempPath NSTemporaryDirectory() // 获取沙盒temp路径
#define kCachePath [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject] // 获取沙盒Cache路径

#define kWeakSelf(x) __weak typeof(self) x = self     // 弱引用
#define kStrongSelf(x) __strong typeof(self) x = self // 强引用

#ifdef DEBUG
#define kLog(...) NSLog(__VA_ARGS__)
#else
#define kLog(...)
#endif

#endif

#endif /* PrefixHeader_pch */

可选配置



这些配置不是必须的,可以根据实际需求来进行配置(SDK和APP项目)。

注意事项

打包出来的framework库使用方法:

  • 点击项目名称,选中你想配置的TARGETS,选择General
  • 如果你的framework是静态库,到Linked Framework and Librarles里,点击“+”号,然后点击Add Other..,找到库文件添加进去就OK了。
  • 如果你的framework是动态库,到Embedded Binarles里,点击“+”号,然后点击Add Other..,找到库文件添加进去就OK了。

当你的SDK项目使用了分类,别人在用你的库时需要在Build Settings - Other Linker Flags里面加入-ObjC参数,以下是这些参数的意义:

  • -ObjC:加了这个参数后,链接器就会把静态库中所有的Objective-C类和分类都加载到最后的可执行文件中,如果使用了分类就要加这个参数。
  • -all_load:会让链接器把所有找到的目标文件都加载到可执行文件中,但是千万不要随便使用这个参数!假如你使用了不止一个静态库文件,然后又使用了这个参数,那么你很有可能会遇到ld: duplicate symbol错误,因为不同的库文件里面可能会有相同的目标文件,所以建议在遇到-ObjC失效的情况下使用-force_load参数。
  • -force_load:所做的事情跟-all_load其实是一样的,但是-force_load需要指定要进行全部加载的库文件的路径,这样的话,你就只是完全加载了一个库文件,不影响其余库文件的按需加载。



项目配置也完事了,接下来介绍下SDK开发时的一些代码风格

SDK编写的代码风格

别人刚使用你的SDK时,如果看到乱乱的代码,就会心生畏怯,就会觉得你的SDK可能很复杂很难用,所以,怎么样才写好一个SDK是很重要的。现在来介绍编写SDK需要注意的地方:




这里只介绍SDK独占的一些注意事项,别的请期待我之后编写的代码风格文章,里面详细介绍了APP编写代码的风格

使用git子模块来管理Workspace

每个项目都有一个git来管理,他们是独立的,因为各个项目都有了git,所以在workspace里的git对workspace的各个项目是不起作用的。但是,我们既想每个项目的git都独立,而在workspace又能统一去管理,那怎么办?
事实上git已经给我们提供了该功能,那就是子模块(Submodule)。

之前我们并没有为workspace创建git,所以我们首先要创建git。
添加子模块的命令为git submodule add <url> <path>,其中url可以是远程地址和本地地址,本地地址要用绝对对路径,path则是该子模块存储的目录路径(使用相对路径)。
操作如下:
打开终端cd到workspace所在的文件夹。

$ git ini
$ git submodule add /Users/cer/Desktop/iOS/SDK ./SDK
$ git submodule add /Users/cer/Desktop/iOS/SDKTest ./SDKTest
$ git submodule add /Users/cer/Desktop/iOS/SDKDemo ./SDKDemo  

如此就把3个项目成功添加为workspace的子模块了。

想git对子模块了解更多,可以参考以下文章:
Git Submodule管理项目子模块
【Git】子模块:一个仓库包含另一个仓库
git中submodule子模块的添加、使用和删除


demo地址在这里:iOS_SDK


关于SDK开发的相关知识就到这了,各位有啥有不懂可以到我的QQ群(139322447)找我

上一篇下一篇

猜你喜欢

热点阅读