iOS动态库开发中遇到的问题小结

关键词:1. XIB在framework中加载失败;2. imageNamed在framework中加载失败;3. 第三方库冲突;4. 然后手工添加Pods库;5. 一些意想不到的事情(感觉这里是本文最干的干货,前面没意思的,可以直接拉到最后,(__) 嘻嘻……)

如何创建一个iOS动态framework的事情就不在本文赘述了,网上有很多的相关文章介绍。需要注意一点的是,网上有些文章会比较旧(iOS8以前的时代),会讲一些过时了的方法,注意选择正确的文章即可。


为了更加方便读者了解framework,放一个我认为写得用心的连接iOS 开发中的『库』


本人主要讲述实际做一个动态framework作为SDK提供给第三方使用时候遇到的实际问题以及我的一些解决办法。
情况是这样的:项目原先已经开发了一个APP了,现在需要把其中的一些功能部件包装成SDK给第三方使用。
为了方便同步开发SDK和APP,我选择在原来的APP工程中添加一个target的方式来产生SDK。

XIB在framework中加载失败

UIViewController的init方法默认使用mainBundle加载与类同名的XIB文件。而动态库在APP里面是一个独立的bundle。这样子的话,在动态库中用到这种方式加载的视图都会失败了。我解决的办法是使用runtime修改了一下init的实现:

@implementation UIViewController(InitFromFramewok)

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method originalMethod = class_getInstanceMethod(self, @selector(init));
        Method swizzleMethod = class_getInstanceMethod(self, @selector(dwsdk_init));
        method_exchangeImplementations(originalMethod, swizzleMethod);
    });
}

- (instancetype)dwsdk_init{
    NSBundle *bundle = [NSBundle bundleForClass:[self class]];

    return [self initWithNibName:nil bundle:bundle];
}

@end

imageNamed方法失败了

失败的的原因和1的问题差不多,因为imageNamed默认也是从mainBundle加载,因此动态库里的图片都加载不到了。

一开始,我使用了和1类似的方法,想通过runtime修改imageNamed的默认实现,改成从[self class]的bundle中加载,结果却是不成功。

不成功的原因是,imageNamed是类方法,它的[self class]是UIImage,而UIImage类所在的bundle是UIKit这个系统动态库中。
于是,更换另外一个实现方式如下:

+ (nullable UIImage *)dwsdk_imageNamed:(NSString *)name {
    NSBundle *bundle = [NSBundle bundleForClass:[FrameworkNibSwizzle class]];
    UIImage *image = [UIImage imageNamed:name
                                inBundle:bundle
           compatibleWithTraitCollection:nil];
    NSLog(@"<DEBUG>load image:%@ from bundle:%@, result:%@", name, bundle, (image?@"YES":@"NO"));
    return image;
}

其中的FrameworkNibSwizzle是一个确定在动态库中的类。
这下子虽然解决了SDK里加载图片的问题了,然而,这个方案会使得SDK外面的imageNamed也被一起替换了,结果就是变成SDK外面的imageNamed找不到图片了。
于是,引入了一套更加复杂一些的配套操作:

@implementation FrameworkNibSwizzle

static IMP orgImageNamedImp = nil;

+ (void)initialize {
    orgImageNamedImp = [self currentImageNamedIMP];
}

+ (IMP)orgImageNamedIMP {
    return orgImageNamedImp;
}

+ (IMP)currentImageNamedIMP {
    Method currentImageNamed = class_getClassMethod([UIImage class], @selector(imageNamed:));
    return method_getImplementation(currentImageNamed);
}

+ (IMP)sdkImageNamedIMP {
    Method sdkImageNamed = class_getClassMethod(self, @selector(jcsdk_imageNamed:));
    return method_getImplementation(sdkImageNamed);
}

+ (void)changeImageNamed{
    IMP currIMP = [self currentImageNamedIMP];
    IMP sdkIMP = [self sdkImageNamedIMP];
    if (currIMP != sdkIMP) {
        Method currentImageNamed = class_getClassMethod([UIImage class], @selector(imageNamed:));
        method_setImplementation(currentImageNamed, sdkIMP);
    }
}

+ (void)restoreImageNamed{
    Method currentImageNamed = class_getClassMethod([UIImage class], @selector(imageNamed:));
    method_setImplementation(currentImageNamed, orgImageNamedImp);
}

+ (nullable UIImage *)dwsdk_imageNamed:(NSString *)name {
    NSBundle *bundle = [NSBundle bundleForClass:[FrameworkNibSwizzle class]];
    UIImage *image = [UIImage imageNamed:name
                                inBundle:bundle
           compatibleWithTraitCollection:nil];
    NSLog(@"<DEBUG>load image:%@ from bundle:%@, result:%@", name, bundle, (image?@"YES":@"NO"));
    return image;
}
@end

这套处理方法的基本过程是:SDK加载的时候先记录一下原始UIImage的imageNamed方法的实现函数,然后在需要使用SDK的时候,替换实现方法,使用完SDK之后,还原实现方法。

这个方案对于使用SDK的过程有明确界限的情况还勉强可以,如果是使用SDK的同时还要使用其他代码的情况,还是不能解决问题。

这里请教读者,基于runtime有没有可能完美解决此问题?

最后,为了完全解决imageNamed的问题,把项目的全部UIImage imageNamed方法统一改了一遍,改成调用[DWImageLoader imageNamed:]:

@implementation DWImageLoader

+ (UIImage *)imageNamed:(NSString *)name {
    NSBundle *bundle = [NSBundle bundleForClass:[self class]];
    return [UIImage imageNamed:name
                      inBundle:bundle compatibleWithTraitCollection:nil];
}
@end

哈哈,也就是说在工程里面明确的定义一个类和imageNamed方法,使用这个类的bundle来加载,完全解决了。

第三方库冲突

这个问题比较普遍,比如,我们自己的SDK使用的AFN这个开源库。使用我们SDK的人同时也使用AFN这个开源库,这样,在运行的时候会有告警,大致的意思是:现在runtime找到两个AFN的类,我警告你,只有其中一个类会被加载,至于是加载SDK里的AFN还是别的地方的AFN,我也保证不了:(

这个问题没有解决,目前我的情况是,确保AFN这些第三方库是相同的版本的话,就可以忽略这个告警

补充说明一下,动态库方式的framework和静态库方式framework的库冲突不同:静态库如果有库冲突,编译的时候就会编译错误,错误是说符号重复了,必须要重新调整framework把其中重复的代码去掉才行。动态库在编译的时候不会报错,执行的时候告警。

手工添加Pods

因为framework是自己手动添加的target,其中使用到一些原APP已经引入的Pods库。需要手工添加需要使用的Pods库

有读者知道这种情况下还可以用Pod install给我新加的target关联使用Pods吗?

具体需要做如下一些事情(中间过程磕磕碰碰,不一定能把完整的步骤复原出来,如果有遗漏,还请提问)
a. 在Setting里首先添加一个自定义变量PODS_ROOT,后面的挺多配置都需要这个环境变量的


b. 设置Pods头文件引用路径

c. 添加Pods的库引用(由于历史原因,我的Pods还是静态库,没改成动态framework)

最后,还有一些意想不到的事情。

a. 有个客户说使用我们的SDK之后,它的tabbar的样式总觉得不正常了。查看了代码才知道,原来我们APP里是通过这样的方式修改了全局的tarbar:)

+ (void)load {
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        Class class = [self class];
        
        SwizzleInstanceMethod(class,
                            @selector(viewDidLoad),
                            @selector(xx_viewDidLoad));
        SwizzleInstanceMethod(class,
                            @selector(setSelectedViewController:),
                            @selector(xx_setSelectedViewController:));
        SwizzleInstanceMethod(class,
                            @selector(setSelectedIndex:),
                            @selector(xx_setSelectedIndex:));
    });
}

在判断SDK用不到这个功能的情况下,加了一个编译选项关闭了这个动作

b. 又有客户说,用了我们SDK之后,他的navigation bar的样式总觉得不正常了。查看了代码才知道,原来我们的APP里是通过[UINavigationBar appearance]修改了全局的样式。好吧,改成使用到SDK的界面才修改就好了。

c. 又有客户说,用了我们的SDK之后,每次进入某个界面再出来就crashed了。我那个去,还有这么神奇的事情?仔细观察界面,发现界面里有一个UITextView。有了a问题的经验,全局搜索“+(void)load”,有所发现了。我们APP用到了一个开源库:UITextView+Placeholder。这是一个给UITextView添加placeholder属性的category。其中有一个这样的实现

+ (void)load {
    // is this the best solution?
    method_exchangeImplementations(class_getInstanceMethod(self.class, NSSelectorFromString(@"dealloc")),
                                   class_getInstanceMethod(self.class, @selector(swizzledDealloc)));
}
- (void)swizzledDealloc {
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    UILabel *label = objc_getAssociatedObject(self, @selector(placeholderLabel));
    if (label) {
        for (NSString *key in self.class.observingKeys) {
            @try {
                [self removeObserver:self forKeyPath:key];
            }
            @catch (NSException *exception) {
                // Do nothing
            }
        }
    }
    [self swizzledDealloc];
}

它需要在dealloc里面remove一些它自己add进去的KVO。

巧的是,我们的客户也用到这个库。我们知道,一个类的多个category的+ (void)load都是会一一加载并执行的,于是SDK里的这个category执行一次load,客户的代码执行一次load。这样dealloc和swizzledDealloc就被调换了两次,相当于就是没有调换了。也就是说在UITextView的dealloc的时候,并没有调用到期望的swizzledDealloc方法,于是,UITextView释放的时候,注册了的KVO没有被释放,于是crash!

解决的办法?好吧,我当时只是简单的把这个库里注册KVO的代码注释了(项目时间紧,客户急,压力大呀)。

这个category里的KVO其实是为了实现在设置了placeholder之后,修改UITextView的字体,大小等属性的时候,placeholder能够相应的响应这些变化表现出一样的字体和大小来。


周末终于有一些自己的时间了,想了一个自己觉得能完美解决的方案,暂时提交到我自己的Github分支上UITextView+Placeholder。同时PR了一份给原作者。不过,作者已经写完这个库有好几个月了,不知道还会不会再看一眼这个项目了:)

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

推荐阅读更多精彩内容