这次的主题的 Runtime ,对于很多人来说,习惯了面向对象的编程语言之后再接触 C 语言一开始是拒绝的。但是当你真的用起来了,你会上瘾,因为这彻彻底底地满足了极客们的折腾心理,用代码操控一切的心理。
就拿我做大象公会的例子来说(对了,这是我在 Smartisan 的第一个项目,也是独立开发的一款App),你知道 Smartisan 一贯的软件设计风格都是拟物化的,真实模拟着现实世界的自然规律。大到一个动画小到一个按钮,无处不体现着这一设计之初就贯彻的理念。然而这对于一个 iOS 工程师来说,不得不说是一个噩耗。你也知道如今强纳肾主管的苹果设计团队已经走了一条不归路了,所有 UI 元素都拍扁了。我不是说扁平化不好,因为这和拟物化只是两种并行的设计风格,没有对错,只有喜好。但你要是一股脑地全部拍扁了,那就有问题了,该给用户明确交互反馈的地方还得拟物。我在第一次拿到产品递过来的需求文档时,就意识到了这将会是个「在平坦的路面上曲折前行的」、「公然叫板苹果设计理念」的累差事。
比如今天我要引出 Runtime 这个话题的引子,就是 —— Navigation Bar.
iOS7 之后(基本上现在这个年代,如果还有产品经理顽固地打算 iOS6 起跳的,你就可以... 不对,你要试图说服他)苹果的 Navigation Bar 虽然并没有和 Status Bar 连在一起,但是因为 Status Bar 的背景会默认与 Navigation Bar 的 barTintColor
一致,所以从视觉上看上去会觉得好像连在一起了。对于 iOS7 之后 Navigation Bar 发生的故事,你可以去 这里 看看。
如果你给 Navigation Bar 设置了一个背景图,就像上面你看到的那样,就会发现图片并不会延伸到 Status Bar 上。
代码也很简单:
[self.navigationBar setBackgroundImage:[UIImage imageNamed:@"title_bar"] forBarPosition:UIBarPositionTop barMetrics:UIBarMetricsDefault];
通常你可能会觉得这已经满足了设计要求,今天故事到这里就应该结束了。但是接下来我翻看需求文档的时候,美好的幻想就被击碎了。不信,你看:
没错,一个很简单的 presentViewController
动画就让处女座产品经理抓狂了,这里请脑补弹幕,各种工程师和产品之间的有趣对话。对话的结果就是,工程师屈服。事实上,任何人看到这多出的黑条都会觉得不舒服,不是吗?
一开始我的方向并不是 runtime,毕竟这把刀锋利是没错,但也容易伤着自己,所以开发的时候原则就是能不用就不用,除非为了设计解耦的架构或者公开API解决不了的时候。我一开始想到的自然还是这三个方法:
- (UIStatusBarStyle)preferredStatusBarStyle NS_AVAILABLE_IOS(7_0);- (BOOL)prefersStatusBarHidden NS_AVAILABLE_IOS(7_0);-(UIStatusBarAnimation)preferredStatusBarUpdateAnimation NS_AVAILABLE_IOS(7_0);
同时在 Info.plist
中增加一个 key View controller-based status bar appearance
并设置为YES
。以确保每个独立的 ViewController 可以独立地控制 Status Bar 的状态。
结果自然是吓人的。
接下来又是一轮疯狂的 Google 和 StackOverflow,结果自然是无果(如果屏幕前的你,对就是你,有自己的非 runtime 实现的workaround欢迎在评论中留言,我将不胜感激)。到这里,其实就已经满足了 runtime 的使用前提,就是公开API解决不了,何况解耦又是求之不得的,这下我才想到是时候拿出这把锋利的剑了,然后也就可以引出了今天要讨论的内容了。
runtime 的基础普及不是今天的主题,网上有很多相关的文章,比如 这篇 。还有很多,我不推荐了,自学能力强的人其实都能自己搜到。
我简单说下我的理解。翻开 <objc/runtime.h>
的头文件的第一眼,我才知道原来 NSObject
也是放在 <objc/objc.h>
的,以前一直以为是Foundation框架中的。上下滑动一看,飞流直下2000行,有种想关屏幕的冲动...简单来说,概念上的建模主要就是以下两个结构体:
struct objc_class {
Class isa
#if !__OBJC2__
Class super_class
const char *name
long version
long info
long instance_size
struct objc_ivar_list *ivars
struct objc_method_list **methodLists
struct objc_cache *cache
struct objc_protocol_list *protocols
#endif}struct objc_method {
SEL method_name
char *method_types
IMP method_imp
}
你可以理解为 class
的范围最大,其中包含这 mehod
,imp
最小,包含在 method
中。
对于 SEL
method
IMP
的区别,从上面 objc_method
的结构体我们就可以看出,一个方法Method
,其实包含一个
方法名
SEL
– 表示该方法的名称;一个
types
– 表示该方法参数的类型;一个
IMP
– 指向该方法的具体实现的函数指针,说白了IMP就是实现方法。
还有一些常用的 runtime 函数,你先认个眼熟,因为都会在下面的demo中用到。(我之所以只写出函数名而不写出有哪些参数是有考量的,如果现在就写出一大堆参数,只会让你让你感到迷惑,达不到先认个眼熟的目的,何况Xcode都有自动补齐,你记住了方法名还怕不知道有哪些参数)
添加方法
class_addMethod
@note :如果类中不存在这个方法的实现,添加成功;存在这个方法的实现,添加不成功替换方法
class_replaceMethod
@note :如果以name标识的method不存在,就会添加这个method(就好像调用了class_addMethod);如果以name标识的method存在,替换imp获取class中的某个method
class_getInstanceMethod
获取method中的某个imp
method_getImplementation
or+ (IMP)instanceMethodForSelector:(SEL)aSelector;
交换两个method中的imp
method_exchangeImplementations
关联对象
objc_setAssociatedObject
获取对象objc_getAssociatedObject
移除对象objc_removeAssociatedObjects
(文档中说,此函数的主要目的是在“初试状态”时方便返回一个对象。你不应该用这个函数来移除对象的属性,因为可能会导致其他clients对其添加的属性也被移除了。规范的方法是:调用 objc_setAssociatedObject 方法并传入一个 nil 值来清除一个关联。)(PS:这属于 Method Swizzling 技术, 我通常喜欢直白地叫方法偷换技术。Method Swizzling 也是 OC HOOK的一个方案。)
接下来我们看实际应用。打开层级调试视图:
我用粗体和编号表示了层级关系。UINavigatinBar
是最底层的父视图,其上只有一个子视图_UINavigationBarBackground
。_UINavigationBarBackground
上又有两个子视图,分别是_UIBarBackgroundTopCurtainView
和 _UIBarBackgroundCustomImageContainer
,而我们的目的是让那块黑色区域也就是 _UIBarBackgroundTopCurtainView
消失。
好了,讲到这里逻辑部分已经结束了。下面看看代码中注意的地方。
首先是 + (void)load
。load方法会在类第一次加载的时候被调用。因为load调用的时间比较靠前,适合在这个方法里做方法交换。而且国外大神们都这么推荐,没理由不做。
+ (void)load{ //方法交换应该被保证,在程序中只会执行一次
static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ //首先动态添加方法。如果类中不存在这个方法的实现,添加成功
BOOL notAdded = class_addMethod(self, @selector(layoutSubviews), [self instanceMethodForSelector:@selector(__layoutSubviews)], method_getTypeEncoding(class_getInstanceMethod(self, @selector(__layoutSubviews)))); //因为UINavigationBar已经包含了layoutSubviews的实现,所以不会被添加成功
if (notAdded) { //如果UINavigationBar没有layoutSubviews这个方法的实现,那么添加成功,将被交换方法的实现替换到这个并不存在的实现
class_replaceMethod(self, @selector(__layoutSubviews), [self instanceMethodForSelector:@selector(layoutSubviews)], method_getTypeEncoding(class_getInstanceMethod(self, @selector(layoutSubviews))));
}else{ //否则交换两个方法的实现
method_exchangeImplementations(class_getInstanceMethod(self, @selector(layoutSubviews)), class_getInstanceMethod(self, @selector(__layoutSubviews)));
}
});
}
交换方法实现 method_exchangeImplementations
—— 我的理解是相当于拦截一个方法,这执行这个方法之前或之后注入另一个方法的代码。比如在这里在系统自动调用 [self layoutSubviews] 之后,我注入了我自己的代码。类似的思想,比如你想在全局范围上,在任何一个 NSObject alloc 之后打印一次log,就可以利用交换方法实现的技术,只要写一条NSLog就行了。这就有点AOP思想了。在编程思想中,传统的做法是,改造每个业务方法,这样势必把代码弄得一团糟,而且以后再扩展还是更乱,而AOP的思想是引导你从另一个切面来看待问题,比如上面log的例子,不管加在哪,它其实都是属于日志系统这个角度的。 AOP允许你以一种统一的方式在运行时期在想要的地方插入这些逻辑。说到底。就是把不同功能的代码分离开,以便能够分离复杂度。让人在同一时间只用思考代码逻辑,或者琐碎事务。
然后我们在 Navigation Bar 调用 layoutSubviews
时注入逻辑代码。
- (void)__layoutSubviews{ //这不是递归,其实调用了[self layoutSubviews];
[self __layoutSubviews]; if (self.ky_hideStatusBarBackgroungView){
Class backgroundClass = NSClassFromString(@"_UINavigationBarBackground");
Class statusBarBackgroundClass = NSClassFromString(@"_UIBarBackgroundTopCurtainView"); for (UIView * aSubview in self.subviews){ if ([aSubview isKindOfClass:backgroundClass]) {
aSubview.backgroundColor = [UIColor clearColor]; for (UIView * aaSubview in aSubview.subviews){ if ([aaSubview isKindOfClass:statusBarBackgroundClass]) { //aaSubview.hidden = YES;
aaSubview.backgroundColor = [[UIColor blackColor]colorWithAlphaComponent:0.01];
}
}
}
}
}
}
上面的代码就解释两点:
你一定要区分名字SEL和实现IMP.[obj selector] selector只是名字,真正执行的是IMP。
[UINavigation layoutSubviews]
会调用- (void)__layoutSubviews
的实现,也就是花括号中的代码; 而[self __layoutSubviews]
会调用[UINavigation layoutSubviews]
的实现。所以不会递归。
注:_aaSubview.hidden = YES;
_ 在项目中我又发现如果直接 hidden 背景视图会带来一些副作用,比如点击状态栏 TableView 无法回滚到顶部。所以,改成 aaSubview.backgroundColor = [[UIColor blackColor]colorWithAlphaComponent:0.01]
就可以避免这一问题。因为我发现设置成clearColor
依然无法点击回到顶部,所以目前的解决方法只能是设置一个 0.01 透明度的颜色了。
事实上,除了隐藏 Status Bar 之外,你还可以改变 Status bar 的背景颜色:
if ([aaSubview isKindOfClass:statusBarBackgroundClass]) {
aaSubview.backgroundColor = [UIColor lightGrayColor];
}
最后一点,添加关联对象。我在头文件中已经声明了一个变量: @property (nonatomic,assign) BOOL ky_hideStatusBarBackgroungView;
。
//添加关联对象-(void)setKy_hideStatusBarBackgroungView:(BOOL)yesOrno{
objc_setAssociatedObject(self, @selector(ky_hideStatusBarBackgroungView), @(yesOrno), OBJC_ASSOCIATION_ASSIGN);
[self setNeedsLayout];
}//获取关联对象-(BOOL)ky_hideStatusBarBackgroungView{ return objc_getAssociatedObject(self, _cmd);
}
关于这个const void *key
,最好是常量、唯一,比如是 static char 类型的:static char kAssociatedObjectKey
;使用时用 &kAssociatedObjectKey
。当然更推荐是指针型的:static char *kAssociatedObjectKey
; 使用的时候直接就可以用kAssociatedObjectKey
。 然而可以用更简单的方式实现:用selector
。因为selector
也能保证唯一并且是常量,所以可以把方法的地址作为唯一的key,并用_cmd
代表当前调用方法的地址,就像:
- (void)addAssociatedObject:(id)object{
objc_setAssociatedObject(self, @selector(getAssociatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)getAssociatedObject{ return objc_getAssociatedObject(self, _cmd);
}
小知识:根据WWDC 2011, Session 322 (第36分钟左右)发布的内存销毁时间表,被关联的对象在生命周期内要比对象本身释放的晚很多。它们会在被
NSObject -dealloc
调用的object_dispose()
方法中释放。
使用的时候就是一行代码搞定:
self.navigationController.navigationBar.ky_hideStatusBarBackgroungView = YES;
最后的结局是美好的。
以上就是篇文章的所有内容。不过也觉得这样的导航栏设计也只有 Smartisan 才有了。
后面的故事
强迫症是没有底线的,比如导航条两端圆角之外的黑块看着实在不爽:
好在,解决起来也很容易:
if ([aaSubview isKindOfClass:navBarBackgoundImageClass]) {
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:aaSubview.bounds byRoundingCorners:UIRectCornerTopLeft | UIRectCornerTopRight cornerRadii:CGSizeMake(4.5, 4.5)];
CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
maskLayer.frame = aaSubview.bounds;
maskLayer.path = maskPath.CGPath;
aaSubview.layer.mask = maskLayer;
}
^_^
PS:写到这里,我不禁开始想一个哲学问题,就是为什么我们会默认就会想到用Category和runtime,我得出的原因是大家都这么用所以我也想到这么用。但是为什么要这么用?我们完全可以不用单独写一个category,直接在原来的类中就可以处理,还免去了使用runtime。但是为了遵循解耦和AOP的思想,我们一定要尽可能遵循科学规范的程序思想。其实真正的大牛,并不是说他知道的API比你多所以比你牛,而是他可以用更抽象、更解耦、更科学、更易维护的程序设计思想写程序。这就是软件工程师的境界了,非一朝一夕可以做到,你我在这条路上都还要躬行好多年。