iOS插件化

前言

framework是Cocoa/Cocoa Touch程序中使用的一种资源打包方式,可以将将代码文件、头文件、资源文件、说明文档等集中在一起,方便开发者使用,作为一名Cocoa/Cocoa Touch程序员每天都要跟各种各样的Framework打交道。

Cocoa/Cocoa Touch开发框架本身提供了大量的Framework,比如Foundation.framework/UIKit.framework/AppKit.framework等。需要注意的是,这些framework无一例外都是动态库。

但残忍的是,Cocoa Touch上并不允许我们使用自己创建的framework。不过由于framework是一种优秀的资源打包方式,拥有无穷智慧的程序员们便想出了以framework的形式打包静态库的招数,因此我们平时看到的第三方发布的framework无一例外都是静态库,真正的动态库是上不了AppStore的。

WWDC2014给我的一个很大感触是苹果对iOS的开放态度:允许使用动态库、允许第三方键盘、App Extension等等,这些在之前是想都不敢想的事。

iOS上动态库可以做什么?

和静态库在编译时和app代码链接并打进同一个二进制包中不同,动态库可以在运行时手动加载,这样就可以做很多事情,比如:

  • 应用插件化

目前很多应用功能越做越多,软件显得越来越臃肿。因此插件化就成了很多软件发展的必经之路,比如支付宝这种平台级别的软件:

首页上密密麻麻的功能,而且还在增多,照这个趋势发展下去,软件包的大小将会不可想象。目前常用的解决方案是使用web页面,但用户体验和Native界面是没法比的。

设想,如果每一个功能点都是一个动态库,在用户想使用某个功能的时候让其从网络下载,然后手动加载动态库,实现功能的的插件化,就再也不用担心功能点的无限增多了,这该是件多么美好的事!

  • 软件版本实时模块升级

还在忍受苹果动辄一周,甚至更长的审核周期吗?有了动态库妈妈就再也不用担心你的软件升级了!

如果软件中的某个功能点出现了严重的bug,或者想在其中新增功能,你的这个功能点又是通过动态库实现的,这时候你只需要在适当的时候从服务器上将新版本的动态库文件下载到本地,然后在用户重启应用的时候即可实现新功能的展现。

  • 共享可执行文件

在其它大部分平台上,动态库都可以用于不同应用间共享,这就大大节省了内存。从目前来看,iOS仍然不允许进程间共享动态库,即iOS上的动态库只能是私有的,因为我们仍然不能将动态库文件放置在除了自身沙盒以外的其它任何地方。

不过iOS8上开放了App Extension功能,可以为一个应用创建插件,这样主app和插件之间共享动态库还是可行的。

PS: 上述关于动态库在iOS平台的使用,在技术上都是可行的,但本人并没有真正尝试过做出一个上线AppStore的应用,因此并不保证按照上述方式使用动态库一定能通过苹果审核!

  • 创建动态库

这边是PiaoJinDylib,创建你测试类PiaoJin

头文件部分

 #import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface PiaoJin : NSObject

- (void)love;

@end

NS_ASSUME_NONNULL_END

实现部分

#import "PiaoJin.h"

#import <UIKit/UIKit.h>

@implementation PiaoJin

- (void)love {
NSLog(@"love you more than I can say!");

UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"love you more than I can say by 飘金!" message:**nil** delegate:**nil** cancelButtonTitle:**nil** otherButtonTitles:@"确定", **nil**];

 [alertView show];

}

@end

在PiaoJinDylib中引入

#import <UIKit/UIKit.h>

//! Project version number for PiaoJinDylib.

FOUNDATION_EXPORT double PiaoJinDylibVersionNumber;

//! Project version string for PiaoJinDylib.

FOUNDATION_EXPORT const unsigned  char PiaoJinDylibVersionString[];

*// In this header, you should import all the public headers of your framework using statements like #import <PiaoJinDylib/PublicHeader.h>*

#import "PiaoJin.h"

设置开放的头文件

一个库里面可以后很多的代码,但是我们需要设置能够提供给外界使用的接口,可以通过Target—>Build Phases—>Headers来设置,如下图所示:

我们只需将希望开放的头文件放到Public列表中即可,比如我开放了PiaoJinDylib.h和PiaoJin.h两个头文件,在生成的framework的Header目录下就可以看到这两个头文件.一切完成,Run以后就能成功生成framework文件了。

前面只是我们只是创建了一个动态库文件,但是和静态库类似,该动态库并同时不支持真机和模拟器,可以通过以下步骤创建通用动态库:

创建Aggregate Target(PiaoJinDylib工程下)

给Aggregate的Target的命名是CommonDylib。

设置Target Dependencies

按以下路径设置CommonDylib对应的Target Dependencies:

TARGETS-->CommonDylib-->Build Phases-->Target Dependencies 

将真正的动态库PiaoJinDylib Target添加到其中。

添加Run Script

按以下路径为CommonDylib添加Run Script:

TARGETS-->CommonDylib-->Build Phases-->Run Script 

添加的脚本为:

if  [ "${ACTION}" = "build" ]

then

INSTALL_DIR=${SRCROOT}/Products/${PROJECT_NAME}.framework

DEVICE_DIR=${BUILD_ROOT}/${CONFIGURATION}-iphoneos/${PROJECT_NAME}.framework

SIMULATOR_DIR=${BUILD_ROOT}/${CONFIGURATION}-iphonesimulator/${PROJECT_NAME}.framework

if  [ -d "${INSTALL_DIR}" ]

then

rm -rf "${INSTALL_DIR}"

fi

mkdir -p "${INSTALL_DIR}"

cp -R "${DEVICE_DIR}/" "${INSTALL_DIR}/"

#ditto "${DEVICE_DIR}/Headers" "${INSTALL_DIR}/Headers"*

lipo -create "${DEVICE_DIR}/${PROJECT_NAME}" "${SIMULATOR_DIR}/${PROJECT_NAME}" -output "${INSTALL_DIR}/${PROJECT_NAME}"

open "${DEVICE_DIR}"

open "${SRCROOT}/Products"

fi

添加以后的效果:

脚本的主要功能是:

1.分别编译生成真机和模拟器使用的framework;

2.使用lipo命令将其合并成一个通用framework;

3.最后将生成的通用framework放置在工程根目录下新建的Products目录下。

如果一切顺利,对CommonDylib target执行run操作以后就能生成一个如图所示的通用framework文件了:

使用动态库

实际过程中动态库是需要从服务器下载并且保存到app的沙盒中的,这边直接模拟已经下载好了动态库并且保存到沙盒中:

image

使用动态库

  • 使用NSBundle加载动态库
/**

 使用NSBundle加载动态库

 */
- (IBAction)loadFrameworkForNSBundle:(UIButton *)sender {

//从服务器去下载并且存入Documents下(只要知道存哪里即可),事先要知道framework名字,然后去加载

NSString *destinationPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask, **YES**) lastObject];

NSString *frameworkPath = [destinationPath stringByAppendingPathComponent:@"PiaoJinDylib.framework"];

NSError *err = **nil**;

NSBundle *bundle = [NSBundle bundleWithPath:frameworkPath];

NSString *str = @"加载动态库失败!";

if ([bundle loadAndReturnError:&err]) {

  self.bundle = bundle;

  NSLog(@"bundle load framework success.");

  str = @"加载动态库成功!";

} else {

  NSLog(@"bundle load framework err:%@",err);
}

UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:str message:**nil**delegate:**nil** cancelButtonTitle:**nil** otherButtonTitles:@"确定", **nil**];

[alertView show];

}
  • 使用dlopen加载动态库

以PiaoJinDylib.framework为例,动态库中真正的可执行代码为PiaoJinDylib.framework/PiaoJinDylib文件,因此使用dlopen时指定加载动态库的路径为PiaoJinDylib.framework/PiaoJinDylib。

/**

 使用dlopen加载动态库

 */

- (IBAction)loadFrameworkForDlopen:(UIButton *)sender {

//从服务器去下载并且存入Documents下(只要知道存哪里即可),事先要知道framework名字,然后去加载(注意需要加上PiaoJinDylib)*

NSString *destinationPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask, **YES**) lastObject];

NSString *frameworkPath = [destinationPath stringByAppendingPathComponent:@"PiaoJinDylib.framework/PiaoJinDylib"];

libHandle = NULL;

  libHandle = dlopen([frameworkPath cStringUsingEncoding:NSUTF8StringEncoding], RTLD_NOW);

NSString *str = @"加载动态库失败!";

if(libHandle == NULL) {

char *error = dlerror();

NSLog(@"dlopen error: %s", error);

} else {

NSLog(@"dlopen load framework success.");

str = @"加载动态库成功!";

}

UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:str message:**nil**delegate:**nil** cancelButtonTitle:**nil** otherButtonTitles:@"确定", **nil**];

[alertView show];

}
  • 调用动态库中的方法
/**

 利用运行时调用动态库方法

 */
- (IBAction)callFrameworkMethod:(UIButton *)sender {

Class piaoJinClass = NSClassFromString(@"PiaoJin");

if (piaoJinClass) {

//事先要知道有什么方法在这个FrameWork中*

id object = [[piaoJinClass alloc] init];

//由于没有引入相关头文件故通过performSelector调用*

[object performSelector:**@selector**(love)];

} else {

UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"调用方法失败!"message:**nil** delegate:**nil** cancelButtonTitle:**nil** otherButtonTitles:@"确定", **nil**];

[alertView show];
}
}
  • 监测动态库的加载和移除

我们可以通过下述方式,为动态库的加载和移除添加监听回调:

+ (void)load 
{ 
  _dyld_register_func_for_add_image(&image_added); 
  _dyld_register_func_for_remove_image(&image_removed); 
} 

github上有一个完整的示例代码

后记

这边只是一件最简单的例子,实际项目中肯定需要与动态库所代表的模块进行交互,数据的共享等等,这些都是需要去根据实际项目场景再去设计解决的!

如果是真机调试可以通过运行需打开iTunes导入到PiaoJinFrameWorkDemo应用中。

Demo 已经上传GitHub,有需要的码友可以去看看

参考文档:

WWDC2014之iOS使用动态库

更多文章

CocoaPods开源库的搭建
CocoaPods搭建私有库
CocoaPods搭建私有库遇到问题
CocoaPods私有库的升级维护
SKStoreReviewController之程序内评价
App应用程序图标的动态更换
开源框架 MGJRouter_Swift
iOS的MVP设计模式
iOS插件化
iOS FMDB的使用
Swift之ReactiveSwift
OC之ReactiveCocoa
OC之ReactiveCocoa进阶
iOS 性能考虑

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容

  • framework是Cocoa/Cocoa Touch程序中使用的一种资源打包方式,可以将将代码文件、头文件、资源...
    飘金阅读 8,620评论 14 15
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,596评论 4 59
  • 是因为农村孩子多吗 为啥总能遇上农村孩子呢 我还自作多情人家对我挺感兴趣 我喜欢肿么样的男生呢 the best ...
    角落蜷缩阅读 134评论 0 0
  • 1918年,国内一些小地方的学堂经费不足,但每年仍花六十块钱去请中学堂学生兼职教英文唱歌,然后花二十块钱买手风琴。...
    萤火虫少女阅读 301评论 0 1
  • 韦小宝就是金庸先生《鹿鼎记》中主人公。他是一名教育专家。 为什么这么说,有如下证据。 证据一:韦小宝从小懂得很多市...
    徐鸿图阅读 216评论 0 1