搭建一个提高开发效率的iOS静态库工程

时间:2021-08-16 19:30:27

前言

探索了出了一个提高静态库工程开发的搭建方法,并在GitHub创建了对应的开源项目。
此项目模板完美解决静态库工程和demoApp工程的集成,提高开发调试效率,方便静态库的源码和demo源码的管理维护。

代码在GitHub https://github.com/zhangkn/KNCocoaTouchStaticLibrary
https://github.com/zhangkn/KNAPP

正文

步骤:
1、创建 app工程 :KNAPP
2、创建静态库工程
静态库( 创建新工程的时候选择“Cocoa Touch Static Library”)

搭建一个提高开发效率的iOS静态库工程

3、将静态库工程放到app工程

搭建一个提高开发效率的iOS静态库工程
搭建一个提高开发效率的iOS静态库工程

4、创建bundle工程

在KNCocoaTouchStaticLibrary.xcodeproj 中创建bundle工程
bundle工程和静态库工程并列。
bundle 工程用于存放资源(xib、图片等资源文件)。

Bundle :就是资源文件包。我们将许多图片、XIB、文本文件组织在一起,打包成一个Bundle文件。方便在其他项目中引用包内的资源。

Bundle文件的特点:Bundle是静态的,也就是说,我们包含到包中的资源文件作为一个资源包是不参加项目编译的。也就意味着,bundle包中不能包含可执行的文件(提交appstore 尤其注意这个问题)。它仅仅是作为资源,被解析成为特定的2进制数据。

1》创建bundle工程(KNStaticBundle) 添加到静态库工程的targets
从MacOS 的framework、library中选择bundle类型进行create
(细节: 创建之后记得将bundle工程的baseSDK 类型修改为iOS SDK,以及supported platforms 为iOS)
搭建一个提高开发效率的iOS静态库工程
搭建一个提高开发效率的iOS静态库工程

搭建一个提高开发效率的iOS静态库工程

2》设置PCH
KNStaticBundle/KNStaticBundle-Prefix.pch

//

// Prefix header

//

// The contents of this file are implicitly included at the beginning of every source file.

//




#ifdef __OBJC__

// #import <Cocoa/Cocoa.h>

#endif




#define MYBUNDLE_NAME @"KNStaticBundle.bundle"

#define MYBUNDLE_PATH [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent: MYBUNDLE_NAME]

#define MYBUNDLE [NSBundle bundleWithPath: MYBUNDLE_PATH]




#define IOS7 [[[UIDevice currentDevice] systemVersion]floatValue]>=7

3》bundle 工程可以设置版本信息。

#define MYBUNDLE_NAME @"hecardpackNFCBundle.bundle"
#define MYBUNDLE_PATH [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent: MYBUNDLE_NAME]
#define MYBUNDLE [NSBundle bundleWithPath: MYBUNDLE_PATH]
+ (NSString*)appVersionCode{
//获取的版本号
// NSDictionary *infoDictionary = [[NSBundle mainBundle] infoDictionary];
//取KNStaticBundle.bundle 的版本号 与独立版本的版本号保存一致
NSDictionary *infoDictionary = [MYBUNDLE infoDictionary];
NSString *appVersionCode = [infoDictionary objectForKey:@"CFBundleShortVersionString"];
if (appVersionCode.length > 0) {
return appVersionCode;
}else{
return @"get app version failed!";
}
}

5、配置环境变量
搭建一个提高开发效率的iOS静态库工程

6、 很重要的一步:添加静态库工程和bundle工程到KNAPP 工程中。以便编译部署。
targets—>build phases—–>link binary with libraries—–>+选择要添加的库。
搭建一个提高开发效率的iOS静态库工程

搭建一个提高开发效率的iOS静态库工程

7、Link Binary With Libraries

搭建一个提高开发效率的iOS静态库工程
搭建一个提高开发效率的iOS静态库工程

8、copy Bundle Resources

搭建一个提高开发效率的iOS静态库工程

直接从静态库工程的Products目录,直接拖拽KNStaticBundle.bundle 到copy Bundle Resources 区域
搭建一个提高开发效率的iOS静态库工程

配置静态库暴露出来的头文件所在的路径即可。

搭建一个提高开发效率的iOS静态库工程

9、灵活运用
目前的搭建基本完成之后,可以用KNAPP工程创建一个独立的app 上传app store,也可以从app抽取部分功能创建一个静态库给第三方集成。

10、配置bundle工程的infoPlist文件路径
搭建一个提高开发效率的iOS静态库工程

KNStaticBundle/KNStaticBundle-Info.plist

11、 静态库工程配置
other linker flags -ObjC -all_load

12、 静态库工程的bitcode 关闭。 app工程的bitcode 进行关闭。

13、 配置静态库暴露的头文件
搭建一个提高开发效率的iOS静态库工程

14、静态库工程的PCH文件和bundle的PCH文件都要设置,且内容要保持一致。
搭建一个提高开发效率的iOS静态库工程

  1. 将编译KNAPP工程之后,将.a 、include头文件以及bundle文件 提供给第三方集成即可:
    搭建一个提高开发效率的iOS静态库工程
    一、搭建的细节

基础知识:

把某个静态库或者Framework直接拖到工程中,需要配置搜索根源环境变量。
Header Search Paths 顾名思义就是用来存放 Project 中头文件的搜索根源,没有被add到项目里的头文件,可以通过配置Header Search Paths 来引入头文件,这样的好处可以不让project 包含的文件太多,便于管理。
1) Header Search Paths 和 User Header Search Paths 的区别
编码时候通过 #include 引入头文件的方式有两种 <> 和 “”。<> 是只从 Header Search Paths 中搜索, 而 “” 则能从 Header Search Paths 和 User Header Search Paths 中搜索。换言之 ,假如你把 路径加到 User Header Search Paths 中,那么 你用 #include

$(SRCROOT)/Libs/ASIHttprequest/ASIHTTPRequestStaticLib/Classes

$(PROJECT_DIR)/Libs/Wechat

1、搜索根源环境变量的配置
配置静态库工程的环境变量

  • Header Search Paths
  • User Header Search Paths
    为了代码的可阅读性,静态库的目录下的第一层级目录最好是实体目录,以便进行配置。

User Header Search Paths 可配置一些本地第三方开源库:SDWebImage 这样可以避免将这类第三方库加到静态库工程中,导致与集成端的类名冲突。

关于类名冲突还有更简单的解决方法: 第三方库由集成方自己添加到target,静态库工程在添加第三方库的时候,不进行add to targets 的动作。 即:

1、往静态库添加第三方库的时候,不进行add to targets 的动作
2、使用了静态库文件的demo,需要自己添加静态库
---- 关于静态库中使用的AFN 框架是否添加到静态库工程的问题
如果静态库中使用的AFN 框架没有添加到静态库工程的targets,这样能达到编译不报错的目的; 这样就要求使用.a 文件的测试app工程需要将对应的AFN框架文件添加到targets.

测试静态库的app工程,如果想直接使用静态库工程暴露的头文件,可以利用Header Search Paths 进行配置

./静态库的工程名/Business/PublicInterface

这个路径的目录最好都采用实体目录。

例如:KNAPP 工程找不到静态库工程的PublicInterface目录下的KNTestWebViewController类。
搭建一个提高开发效率的iOS静态库工程

只需在KNAPP工程的Header Search Paths 增加配置信息:

./KNCocoaTouchStaticLibrary/Business

第三方库 分类的处理

第三方库里对系统库的类加了 category

这时,就需要使用编译参数: -ObjC ,这样第三方库中对系统类作的扩展方法才能在工程中使用。
unrecognized selector sent to class

自己要制作一个库(静态库)的话,要注意两点:

1 、避免对系统类加 category, 这样,别人用你的库时,不加 ObjC 参数也可以用你的库。

2 、如果库中用到了其它的第三方的源代码,尤其是用的比较普遍的,如 Reachability, 一定一定要对

这些类重命名,最常见的作法就是给类名加个前缀。以避免别人用你的库时,产生 duplicate symbol 的问题。

-all_load Loads all members of static archive libraries.

-ObjC Loads all members of static archive libraries that implement an Objective-C class or category.

-force_load (path_to_archive) Loads all members of the specified static archive library. Note: -all_load forces all members of all archives to be loaded. This option allows you to target a specific archive.

翻译过来就是-all_load就是会加载静态库文件中的所有成员,-ObjC就是会加载静态库文件中实现一个类或者分类的所有成员,-force_load(包的路径)就是会加载指定路径的静态库文件中的所有成员。所以对于使用runtime时候的反射调用的方法应该使用这三个中的一个进行link,以保证所有的类都可以加载到内存*程序动态调用

ps: 图片格式的转换

png图片如果用了@2x@3x会自动转换成tiff格式的图片。设置不转换的方法是 在bundle的target中 Build Settings 里的 COMBINE_HIDPI_IMAGES 设置为NO

减少静态库包的大小

去掉静态库工程的调试符号:

搭建一个提高开发效率的iOS静态库工程
ps:调试符号可以帮助你调试程序,可以追踪到编译器提供的库和操作系统本身的代码。调试符号 就是 这些代码内的符号。
调试符号数据库,记录了变量,函数 这一类符号 和内存定位的关系,
从而可以用 地址相关信息追踪到变量名,函数名。方便调试

搭建过程常见的问题:

xib 引用class的问题 Unknown class MyClass in Interface Builder file.

/**



Unknown class MyClass in Interface Builder file.

由于静态框架采用静态链接,linker会剔除所有它认为无用的代码。不幸的是,linker不会检查xib文件,因此如果类是在xib中引用,而没有在O-C代码中引用,linker将从最终的可执行文件中删除类。这是linker的问题,不是框架的问题(当你编译一个静态库时也会发生这个问题)。苹果内置框架不会发生这个问题,因为他们是运行时动态加载的,存在于iOS设备固件中的动态库是不可能被删除的。

有两个解决的办法:

1.让框架的最终用户关闭linker的优化选项,通过在他们的项目的Other Linker Flags中添加-ObjC和-all_load。

2.在框架的另一个类中加一个该类的代码引用。例如,假设你有个MyTextField类,被linker剔除了。假设你还有一个MyViewController,它在xib中使用了MyTextField,MyViewController并没有被剔除。你应该这样做:

在MyTextField中:

+ (void)forceLinkerLoad_ {}

在MyViewController中:

+(void) initialize { [MyTextField forceLinkerLoad_]; }

他们仍然需要添加-ObjC到linker设置,但不需要强制all_load了。

第2种方法需要你多做一点工作,但却让最终用户避免在使用你的框架时关闭linker优化(关闭linker优化会导致object文件膨胀)。



*/


+ (void)initialize{

[KNProgress description];

}

2、退出静态库的时候,选择POP还是present。

通常,本人建议全部采用present。因为present 方式具体独立性,将自己的静态库与集成静态库的主端隔离。
如果自己的控制器需要展示主端唤起静态库时的页面当中背景,可以采用代码截图当背景即可。

跳转到静态库控制器之前进行截图的代码如下:

/**
* 返回截取到的图片
*
* @return UIImage *
*/

- (UIImage *)imageWithScreenshot
{
NSData *imageData = [self dataWithScreenshotInPNGFormat];
return [UIImage imageWithData:imageData];
}


/**
* 截取当前屏幕
*
* @return NSData *
*/

- (NSData *)dataWithScreenshotInPNGFormat
{
CGSize imageSize = CGSizeZero;
UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation;
if (UIInterfaceOrientationIsPortrait(orientation))
imageSize = [UIScreen mainScreen].bounds.size;
else
imageSize = CGSizeMake([UIScreen mainScreen].bounds.size.height, [UIScreen mainScreen].bounds.size.width);

UIGraphicsBeginImageContextWithOptions(imageSize, NO, 0);
CGContextRef context = UIGraphicsGetCurrentContext();
for (UIWindow *window in [[UIApplication sharedApplication] windows])
{
CGContextSaveGState(context);
CGContextTranslateCTM(context, window.center.x, window.center.y);
CGContextConcatCTM(context, window.transform);
CGContextTranslateCTM(context, -window.bounds.size.width * window.layer.anchorPoint.x, -window.bounds.size.height * window.layer.anchorPoint.y);
if (orientation == UIInterfaceOrientationLandscapeLeft)
{
CGContextRotateCTM(context, M_PI_2);
CGContextTranslateCTM(context, 0, -imageSize.width);
}
else if (orientation == UIInterfaceOrientationLandscapeRight)
{
CGContextRotateCTM(context, -M_PI_2);
CGContextTranslateCTM(context, -imageSize.height, 0);
} else if (orientation == UIInterfaceOrientationPortraitUpsideDown) {
CGContextRotateCTM(context, M_PI);
CGContextTranslateCTM(context, -imageSize.width, -imageSize.height);
}
if ([window respondsToSelector:@selector(drawViewHierarchyInRect:afterScreenUpdates:)])
{
//
// if ([[UIDevice currentDevice].systemVersion isEqualToString:@"8.1.3"]) {
//
//
// }else{
[window drawViewHierarchyInRect:window.bounds afterScreenUpdates:YES];

// }


}
else
{
[window.layer renderInContext:context];
}
CGContextRestoreGState(context);
}

for (UIWindow *window in self.alertViewWindows) {
CGContextSaveGState(context);
CGContextTranslateCTM(context, window.center.x, window.center.y);
CGContextConcatCTM(context, window.transform);
CGContextTranslateCTM(context, -window.bounds.size.width * window.layer.anchorPoint.x, -window.bounds.size.height * window.layer.anchorPoint.y);
if (orientation == UIInterfaceOrientationLandscapeLeft)
{
CGContextRotateCTM(context, M_PI_2);
CGContextTranslateCTM(context, 0, -imageSize.width);
}
else if (orientation == UIInterfaceOrientationLandscapeRight)
{
CGContextRotateCTM(context, -M_PI_2);
CGContextTranslateCTM(context, -imageSize.height, 0);
} else if (orientation == UIInterfaceOrientationPortraitUpsideDown) {
CGContextRotateCTM(context, M_PI);
CGContextTranslateCTM(context, -imageSize.width, -imageSize.height);
}
if ([window respondsToSelector:@selector(drawViewHierarchyInRect:afterScreenUpdates:)])
{
[window drawViewHierarchyInRect:window.bounds afterScreenUpdates:YES];
}
else
{
[window.layer renderInContext:context];
}
CGContextRestoreGState(context);
}

UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

return UIImagePNGRepresentation(image);
}

如果实在想固定采用present,可以采用以下代码进行判断

- (void)exit{

// NSLog(@"presentingViewController :%@",self.presentingViewController);

//iOS判断当前ViewController是push还是present方式显示的

if (self.presentingViewController) {

[self dismissViewControllerAnimated:YES completion:nil];

} else {

if ([self.navigationController respondsToSelector:@selector(popViewControllerAnimated:)]) {

UINavigationController *savedUinvc = self.navigationController;

UIViewController *one = nil;

one = [savedUinvc popViewControllerAnimated:NO];

}

}

}

ps:.appicon所有的尺寸:
40*40 60*60 58*58 87*87 80*80 120*120 180*180

ld: -bundle and -bitcode_bundle (Xcode setting ENABLE_BITCODE=YES) cannot be used together

http://*.com/questions/34770802/ld-bundle-and-bitcode-bundle-xcode-setting-enable-bitcode-yes-cannot-be-use

将bundle的release和debug的bitcode 进行关闭即可

测试注意事项

1、伪https 证书的安装(安装cer 证书教程)
如果测试环境的的https链接不是有效证书的时候,需要在测试安装对应的证书。

证书可通过iPhone自带的邮件安装

iPhone mail的设置:
如果是QQ邮箱的话,需要开通POP3/IMAP/SMTP/Exchange/CardDAV/CalDAV服务。 可到网页版QQ邮箱设置。 开启对应服务之后,邮箱的密码就是授权码(温馨提示:登录第三方客户端时,密码框请输入“授权码”进行验证。生成授权码)
搭建一个提高开发效率的iOS静态库工程

编译Debug、Release 版本的时候,注意静态库工程、boundle 与集成方的设置保持一致。
例如:bitcode

搭建一个提高开发效率的iOS静态库工程

提交appstore 的注意事项

封板-- 去掉bundle工程的info plist的CFBundleExecutable key(可执行文件配置信息)

关于静态库和动态库的知识补充:

库是程序代码的集合,是共享程序代码的一种方式。
根据源代码的公开情况,库可以分为2种类型:开源库(公开源代码,能看到具体实现,比如SDWebImage、AFNetworking);闭源库(不公开源代码,是经过编译后的二进制文件,看不到具体实现;主要分为:静态库、动态库)
1、 静态库和动态库

1》静态库和动态库的存在形式

静态库:.a 和 .framework

动态库:.dylib 和 .framework

2》静态库和动态库在使用上的区别

静态库:链接时,静态库会被完整地复制到可执行文件中,被多次使用就有多份冗余拷贝

动态库:链接时不复制,程序运行时由系统动态加载到内存,供程序调用,系统只加载一次,多个程序共用,节省内存(项目中如果使用了自制的动态库,不能被上传到AppStore)

3》 制作静态库的注意点
无论是 .a 静态库还是 .framework 静态库,最终需要的都是:二进制文件 + .h + 其它资源文件
.a 和 .framework 的使用区别:.a 本身是一个二进制文件,需要配上 .h 和 其它资源文件 才能使用;.framework 本身已经包含了 .h 和 其它资源文件,可以直接使用
多文件处理:如果静态库需要暴露出来的 .h 比较多,可以考虑创建一个主头文件(一般 主头文件 和 静态库 同名)在主头文件中包含所有其他需要暴露出来的 .h 文件,使用静态库时,只需要#import 主头文件
.framework为什么既是静态库又是动态库:系统的 .framework 是动态库,我们自己建立的 .framework 是静态库
静态库中包含了Category:如果静态库中包含了Category,有时候在使用静态库的工程中会报“方法找不到”的错误(unrecognized selector sent to instance)解决方案:在使用静态库的工程中配置Other Linker Flags为-ObjC
/* -ObjC这个flag告诉链接器把库中定义的Objective-C类和Category都加载进来。这样编译之后的app会变大(因为加载了其他的objc代码进来)。但是如果静态库中有类和category的话只有加入这个flag才行。
-all_load这个flag是专门处理-ObjC的一个bug的。用了-ObjC以后,如果类库中只有category没有类的时候这些category还是加载不进来。变通方法就是加入-all_load或者-force-load。-all_load会强制链接器把目标文件都加载进来,即使没有objc代码。-force_load在xcode3.2后可用。但是-force_load后面必须跟一个只想静态库的路径。
*/

4》合并真机和模拟器的.a文件
如果想让一个.a文件能同时用在真机和模拟器上,需要进行合并
在终端输入指令:lipo -create Debug-iphoneos/libMJRefresh.a Debug-iphonesimulator/libMJRefresh.a -output libMJRefresh.a
蓝色部分是固定指令: lipo -create -output
红色、紫色是真机和模拟器.a文件的路径: Debug-iphoneos/libMJRefresh.a Debug-iphonesimulator/libMJRefresh.a
绿色是所合成.a文件的路径: libMJRefresh.a
.a文件的体积(一般情况下):
真机用的.a > 模拟器用的.a
所合成.a == 真机用的.a + 模拟器用的.a
通过lipo –info libMJRefresh.a可以查看 .a 的类型(模拟器还是真机)

#!/bin/bash
#用lipo合并模拟器Framework与真机Framework
DATE=$(date +%Y%m%d-%T)
mkdir -p /Users/devzkn/Desktop/lib/lib_$DATE/lib_real_iphonesimulator
#合并文件
lipo -create /Users/devzkn/Library/Developer/Xcode/DerivedData/iPos-bzjkdluhflvvnmgjldfubxpmrdrj/Build/Products/Debug-iphoneos/libiPxxxosLib.a /Users/devzkn/Library/Developer/Xcode/DerivedData/iPos-bzjkdluhflvvnmgjldfubxpmrdrj/Build/Products/Debug-iphonesimulator/libiPxxxosLib.a -output /Users/devzkn/Desktop/lib/lib_$DATE/lib_real_iphonesimulator/libiPxxxxosLib.a
#rm -r /Users/devzkn/Desktop/lib/

》 选择需要暴露出来的.h文件,.m文件会自动编译到.a文件中

搭建一个提高开发效率的iOS静态库工程

动态库framework
修改framework 的类型,Mach-O Type。(静态库和动态库的添加到不同的位置,按照Xcode模版创建的工程,默认是动态库)
合并framework的工程名(DKFrameWork)文件,即可将真机于模拟器的framework进行合并。
framework想暴露的头文件,添加到headers的public子选项即可

桥接

Xcode7之后 添加PCH文件的方法

1.) 打开你的Xcode工程. 在Supporting Files目录下,选择 File > New > File > iOS > Other > PCH File 然后点击下一步;
2.) 给你的PCH文件起名字TestDemo-Prefix.pch. 例如你的项目工程名为TestDemo然而你的PCH 文件的名字应该为 TestDemo-Prefix.pch,然后创建;
3)pch示例
4.) 找到 Project > Build Settings > 搜索 “Prefix Header“;
5.) “Apple LLVM 7.0 -Language″ 栏目中你将会看到 Prefix Header 关键字;
6.) 输入: YourProjectName/YourProject-Prefix.pch (如 TestDemo/TestDemo-Prefix.pch );
7.),将Precompile Prefix Header为YES,预编译后的pch文件会被缓存起来,可以提高编译速度。
8.) Clean 并且 build 你的项目.
搭建一个提高开发效率的iOS静态库工程