加入百度知道团队也有一段时间了,能跟@我就叫Sunny怎么了、@sinojerk等小伙伴一起工作生活是一种极赞的体验。在完成日常业务开发之余,我们也会进行一些技术研究项目,并将研究结果以开源的方式公布出来,自然我也成为了forkingdog
开源小组的一员。
近期我们的研究项目是FDStackView
,现如今已经完成了Alpha
版本的开发工作,并将其开源在了Github
上,项目地址。虽然现在已经完成所有的基本功能,但是仍需要在真实的环境中测试试用,欢迎大家将试用之后的问题反馈给我们,提issue
给我们,使我们更好的修复和完善FDStackView
,以便于更好的方便开发者们使用。
Introduce
FDStackView
究竟是什么呢?在介绍FDStackView
之前,首先你需要知道UIStackView
是什么?UIStackView
是苹果在WWDC上发布iOS9
的时候新推出的一个UIKit
的视图,现在网上可以搜索到很多关于它的资料,关于介绍,如何使用等。简单来说就是可以使用它来做一些流式布局,开发者只需要将需要的视图丢到UIStackView
中,然后设置它的一些属性来展现所需要的布局,因此无需自己再去添加各种约束,所有约束不在由开发者自己去管理,这对于一些还不会使用AutoLayout
的开发者来说是一个福音。复杂来说,因为UIStackView
是可以嵌套使用的,那么再结合上一些简单的约束,那么就可以完成任何复杂的界面了。想想之前需要各种管理约束,而现在有了它只需要将视图丢给它,改几个属性然后界面就做好了,是不是爽到爆,开发效率又提升一个档次啊。下面提供几个介绍UIStackView
的文章,使还不太了解的同学可以了解一下,传送门在此:
iOS 9: Getting Started with UIStackView
介绍完UIStackView
的优势想必大家都已经跃跃欲试了,我自身对于这个控件都是十分的期待,因为在开发中你可以不用去写大段的创建constraints
的代码了,如果你使用xib
或者storyboard
的话,那么在IB
中你也不需要去连接各种约束了,这是多么棒的一种体验,而且在Xcode7
的IB
中右下角往常用来增加约束,修正视图的位置又新增加了一个stack
按钮,可以快速的将所选视图加入到UIStackView
中,可见苹果也是推荐开发者使用UIStackView
的。但是UIStackView
是在iOS9
才推出的,最低支持的系统也是iOS9
,这就蛋疼了,现在能有几个APP
是从iOS9
开始支持的,如此一来这个控件就成了鸡肋般的存在,再低版本下根本无法使用。自己在业务开发中经常会想这个需求用UIStackView
简直就是妙解,而我却还在这里痛苦的连约束……鉴于这个强烈的需求,FDStackView
出现了,它就是为了解决UIStackView
在低于iOS9
的系统下无法使用的问题。在FDStackView
之前也已经有了一些类似的开源项目,比如OAStackView
和TZStackView
,然而他们都不能满足我们的需求,局限性还是比较大的,比如不支持IB
,某些功能还没有实现,类名需要使用非UIStackView
,在我们看来这些对开发者来说都是不友好的,开发者需要的是一款功能完善,支持IB
,使用时完全无感,在Xcode7
上直接使用UIStackView
即可,接下来的事情交给FDStackView
就好,它负责将UIStackView
在低于iOS9
的系统上运行。需要注意的是如果使用IB
的话,那么IB
的Builds for
属性需要设置为iOS 9.0 and later
。如图所示:
Research
这个技术项目有一大部分的时间,我们都是在做调研工作,首先我们需要把UIStackView
玩的很熟练,它的各种属性,各种状态以及他们的组合关系分别是什么样的,其次我们需要解决的问题有:
- 使用低系统版本的
API
和控件创建一个和UIStackView
一模一样的控件FDStackView
; - 在低系统版本运行
UIStackView
的时候使用我们的FDStackView
; - 使
FDStackView
获得Interface Builder
的支持。
解决了以上三个问题后,那么这个项目基本上也就算是完成了,第一个是工作量最大的工程,它又可以拆分为以下几个技术点:
alignment
和distribution
的约束如何添加和管理;spacing
和distribution
的关系及约束的创建;- 子视图的隐藏显示如何处理;
- 子视图的
intrinsicContentSize
发生变化时如何处理。
首先我们假设在第一个难点已经解决的前提下去攻克其他的难点,毕竟有其他开源方案的存在,说明这个不是不可行的。
至于第二个难点,UIStackView
在低系统版本编译时会报找不到符号的error
,那么解决的思路就是在低系统版本将UIStackView
的符号写进去,然后在runtime
将符号与我们的FDStackView
做关联,从而使低系统版本也能够运行UIStackView
,而实际上在起作用的是我们的FDStackView
。这里使用到的黑魔法
就是汇编语言,网上已经有大神给出了类似的解决方案,对其进行优化和修改之后应该就能满足我们的需求。
最后一个难点就是使FDStackView
获得Interface Builder
的支持,因为我们是IB
的重度使用者,一个不能在IB
上使用的控件一定不是一个好控件。所以一定要让FDStackView
能够在IB
上使用,有一个方案就是直接使用UIView
然后把他的Class
指定为FDStackView
,将Axis
、Alignmen
和Distribution
等属性通过IBInspectable
使其可以在IB
中编辑和设置,但是这样一个是IBInspectable
在IB
中的显示效果很烂,说实话就是不好用,再一个就是用了UIView
没有办法像UIStackView
那样在IB
中可以直接预览布局效果,这就是很差的一种体验了。最好的方案就是在IB
中仍然使用UIStackView
,使其在IB
中有最佳的体验,然后借助上一难点的解决方案,在低系统版本中使用FDStackView
代替UIStackView
。这样就会带来两个其他问题:
IB
的构建版本是根据Project
的部署版本来的,如果项目不是支持iOS9
的话那么会报这样一个error
:”UIStackView before iOS 9.0”
;- 如何使
IB
构建出来的FDStackView
获得在IB
中给UIStackView
所设置的各种属性。 这两个问题,第一个只需要将IB
的构建版本设置为iOS9
及以后即可,目前来看是没有问题的,但是还不知道其他的控件被IB
搞成iOS9
的版本,在低系统版本上会不会有问题,这个还需要后续的验证。第二个问题,由于使用IB
创建的UIKit
控件都会由initWithCoder:
进行初始化,因此弄清楚NSCoder
的decode
过程就能将IB
设置的属性赋值给所创建的对象了。
解决完以上两个难点,就可以回过头来研究第一个了,就是创建一个和UIStackView
一模一样的FDStackView
。这里我们对UIStackView
进行了详细的研究,包括dump
出所有UIStackView
的相关私有类,各个类的方法,实例变量等。还需要添加符号断点来跟踪各个方法的调用顺序及各个实例变量的值得变化情况。同时还需要分析各个状态下UIStackView
的约束constraints
的情况,包括约束的个数,连接的方式,及约束所添加到的视图等。经过以上的各种分析之后,我们又通过在IB
中借助UIView
手动连接约束的方式,连出每一个UIStackView
所对应的状态。经过这一番调查与研究我们已经大概摸清的UIStackView
的工作原理与实现方式。
与此同时我们还发现了两个UIStackView
的bug
,本以为在Xcode7
正式发布之后会得到修复,可是遗憾的是从我们开始研究的时候的beta5
到后来的beta6
、GM
和正式版这两个bug
依然存在,后面我会介绍一下这两个bug
。
Implementation
下面介绍一下具体的实现细节,同样还是从第二个点说起,最终起关键作用的代码是这些:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
|
首先说一下__asm
:
1 2 3 |
|
意思就是说在你的C
或C++
源代码中放入汇编代码用来替换任何C++
的符号。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
先来说这一个部分,大神的解决方案给出了英文注释,尝试着直译了一下:
1 2 3 |
|
发现还不如不译,就直接说一下大概的意思吧。
第一行是取得符号所在的区间,之后区分64
和32
位系统,将_OBJC_CLASS_$_UIStackView
这个符号与自定的符号做一个weak
类型的关联。
接下来就是__attribute__((constructor))
这个黑魔法,这个标识的方法会在所有的类load
之后,main
函数调用之前调用。所以此时FDStackView
已经被load
了。再之后就是判断runtime
是否存在UIStackView
,不存在的话就根据不同的系统平台将指向_OBJC_CLASS_$_UIStackView
这个符号的指针存储在stackViewClassLocation
中,接下来通过runtime
创建UIStackView
这个类并作为FDStackView
的子类,并注册进runtime
,最后将UIStackView
作为stackViewClassLocation
这个指针的值。如此一来在低系统版本中UIStackView
就能作为FDStackView
的子类使用了。它没有重载任何方法,因此就跟使用直接FDStackView
一模一样。
接下来的问题是IB
加载出来的UIStackView
如何将属性值设置到我们的FDStackView
上,这个在前面研究是已经有结论,首先需要将IB
的build for
做下修改,然后IB
创建的UIKit
控件都会由initWithCoder:
进行初始化,所以所有的信息都在NSCoder
这个对象中,NSCoder
提供了一系列的decode
方法,由于key
是字符串,所以可以在汇编代码处直接看到,所以通过加符号断点的方式找到这几个key
。
如此一来就可以直接在FDStackView
的initWithCoder:
方法中取到值,再将这几个值赋值即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
最后就是通过系统的API
创建constraints
来实现FDStackView
了,这里涉及的内容比较多,包括几个辅助的私有类,及Alignment
和Distribution
方向上的约束创建,子视图隐藏,intrinsicContentSize
改变如何处理等。这里我们都尽可能的与猜测到的UIStackView
的实现保持一致。这些内容将会在后续的另一篇文章中介绍。
UIStackView Bugs
现在来说一下我们在调研UIStackView
时发现的两个bug
,测试的Demo
已经放在Github
上。
这个测试Demo
会借助我们的FDStackView
来演示对比出UIStackView
的bug
,上面是系统原生的UIStackView
,下面是我们的FDStackView
,两者的参数设置是完全相同的。
先来看第一个,当Distribution
设置为UIStackViewDistributionFillProportionally
时,并且存在spacing
时就会出现问题,如图所示:
UIStackViewDistributionFillProportionally
这个属性的意思是子视图的宽度会根据他们内容的宽度比例而在UIStackView
中占据对应的宽度,即他们的实际的宽度比应该是他们的内容固有宽度(intrinsicContentSize
)的比例,Demo
中三个Label
的固有宽度即汉字的宽度是4:1:2
,那么在UIStackView
中他们所占据的宽度也应该是4:1:2
,这在spacing
为0
的情况下是ok的。
如果存在spacing
的话,那么UIStackView
应该先减去子视图之间的spacing
,然后再去按比例分布子视图的宽度。这里可以看到UIStackView
的布局是烂的了,而FDStackView
的布局是ok的。
这里我们通过分析UIStackView
身上的constraints
大概得出UIStackView
出现这个bug
的原因是,他们的算法出了问题,他们这一部分的约束是这样添加的,每一个子视图的宽度等于UIStackView
的宽度乘上一个比例系数,即AutoLayout
计算公式y = m * x + c
中的m
系数,c
的值一直为0
。他们在计算m
的时候出了问题,忽略了spacing
的存在,也就是在计算中没有计算上spacing
的值。
具体拿Demo
来看的话,UIStackView
的最左边的Label
的宽度应该是这样计算的label.width = 4 * UIStackView.width / (4 + 1 + 2)
,这是spacing
为0
时,m
的值就是4 / (4 + 1 + 2)
,这没有问题 ,但是如果有spacing
的话,他们把spacing
也作为了分母的一部分,认为spacing
也是可以按比例显示宽度的,所以m
的值就成为了4 / (4 + 1 + 2 + spacing)
(这里的spacing
不是UIStackView
设置spacing
的值,而应该是实际UIStackView
中出现的所有spacing
的和)。因为spacing
被当作分母计算了进去,那么在布局的时候spacing
也应该按照计算出的系数乘上UIStackView
的宽度来显示,但实际上他们没有这么做,而是把spacing
按固定值来显示了,这样就会因为分母加入了spacing
导致所有子视图计算出的m
偏小,进而显示出来也就会偏小,到了最后一个视图时,由于约束优先级的缘故导致这个宽度的约束不再起作用,从而导致被拉长,出现了上图的效果。
所以这里UIStackView
是算法出了问题而显示时又按正确的样式来显示,所以布局就烂了,其实在有spacing
的状态下就不应该忽略c
的值了,而且spacing
也不应该参与到分母中去计算,正确的约束应该是这个样子的label.width = 4 * UIStackView.width / (4 + 1 + 2) - 4 * spacing / (4 + 1 + 2)
,这时c
就有值了,不再是0
而是-4 * spacing / (4 + 1 + 2)
。
整体来说UIStackView
在处理UIStackViewDistributionFillProportionally
这个属性的时候采取的约束添加方式不是最好的,处理起来是比较复杂的,这样处理会出现很多非整数情况,一个是计算复杂,在一个也会丢失精度。所以我们在FDStackView
中没有使用这种连接方式,而是使用了另外一种方法,后面的文章会介绍到。
另外一个bug
是当Alignment
属性设置为UIStackViewAlignmentFill
时,当一个最高的子视图隐藏掉了时,UIStackView
的高度并没有变化,这时它应该变为第二高的子视图的高度,具体如图所示:
这种情况只有在属性设置为UIStackViewAlignmentFill
时才会出现,具体的出现原因我们也有分析出来的结论,但是涉及到Alignment
方向上约束添加的问题,这个会在后一篇文章中提到,所以这里就先不做解释,之后在说。我们的FDStackView
修复了这个问题,但是在一种情况下也会失去作用就是给这个要隐藏的视图收到添加了一个高优先级的高度约束的情况下,不过一般情况下我们使用UIStackView
基本都不会再给子视图添加约束了。
第一篇文章就介绍这么多,后面我会找时间把第二篇文章(Part 2)整理出来。
————————————