好几年没动这个博客了,其间收到很多陌生的小伙伴们的邮件,大体分成几类:有催更的,有问题探讨的,有鼓励的,也有指出我博客上的不足之处的。十分感谢大家,从2016年开始搭建博客,主要的出发点就是为了系统性的梳理整个技术栈的知识体系,所以内容的 是最初的一个目标,最早先是想着弄一个技术栈的Roadmap,然后在每个技术点上加上自己认为比较好的文章的链接,通过这种方式来梳理整个技术栈。但是这种方式对我个人来说就缺乏了知识提炼消化的过程,往往下一次进行回顾的时候又得从庞大的资料里面重新提取,并且很可能有些文章的链接失效了。梳理整个体系是一件十分耗费精力的事情,也不知道当时是怎么坚持过来的。当时弄的时候也没期待它能给自己带来什么附加价值。
但是期间确确实实得到了很多鼓励(当然也有批评,^V^坦诚脸),但大家都是十分善意的。

对于一些反馈比较频繁的问题给个回复以及不在的这段时间的简单总结:

  1. 最近在忙啥?

从前年开始工作内容挑战相对小了,并且当初自己从嵌入式转入互联网开发的目标就是未来能够成为一名独立开发者,所以就着手开始了独立开发的旅程,期间开发了叮咚熊和Top Tags,Aurora三款应用,因为整个设计开发都得自己来。所以基本上下班回家忙完琐事,就是设计,开发。周末除了雷打不动的陪老婆小孩的时间外基本上都花在上面了。我老婆有时候会打趣说和我在一起经常会感觉有第三者存在,是的,就是她们仨,哈哈哈哈哈哈哈哈 (在这里谢谢老婆的支持,虽然经常抱怨我,经常做这种吃力不讨好,用爱发电的事情,但是还是很感激她一路的纵容,哈哈哈哈)。

关于叮咚熊和Top Tags两款应用都已经上架到AppStore 如果大家有需要可以下载使用,目前是不收费,也没有烦人的广告,并且引用一个想要合作的热心用户的反馈就是,“别的应用iCloud 同步功能一般都收费,为啥你的应用不这样做?它的盈利点是什么?“
坦诚说谁不想靠着开发的应用来获得一份收入的,毕竟每天上班结束投入的时间成本,以及每年孝敬苹果爸爸的开发者费用都是支出的,但是它们太小了,还没长成我对他们的预期,我很希望它能够有更多的功能,能够提供更多的服务,靠自己的劳动所得赚钱不丢人,
但是还想想着提供更加完善的功能后再开启收费,包括后续引入广告后如何不影响交互,这些目前我都很难给出一个自己很满意的答案。仍然需要自己去探索,很多人会告诉你他们的答案,经验,但是这些都只是别人的,只有自己探索过才是自己的答案。这也是我个人对独立开发者的理解。
再说了我可以养它们哈,用爱发电,做很多自己觉得有意义但是不靠谱的事情做多了,就会慢慢接受这个现实,更加专注到事情的本身。哈哈哈哈,反正我老婆也会时不时得觉得我做的这些事情不靠谱,没有太多阻拦只不过知道劝我也没用,所以她对我的要求就是保持收入按计划持续就好。
其实我自己也不太清楚后续会不会后悔投入那么多时间,然后一无所成,不过唯一确定的是,不做的话肯定会后悔。不大想以后把”要是当时….”挂在嘴边,成为大家都烦的糟老头。

对了关于Aurora为什么下架,这里也做个说明,因为他有更好的替代品PPHub,它做得很完善了,当时开发Aurora也是为了学习React Native而开发的,并且需要后端服务器的成本。(对最真实的原因就是穷得付不起服务器的钱了,哈哈哈哈)

今年的计划希望叮咚熊能够增加竖屏支持,最初是想通过叮咚熊让用户戒掉使用手机的频率,让它作为桌面摆件。但是这也导致了使用时长很低。今年会增加竖屏内容。还有就是优化应用图标和ASO优化。爆光的数据太少了。
Top Tags 用户量还好,目前还有很多热心用户,数据也很稳定,前几天还收到一个热心的设计用户给了一份很详细的使用体验和从设计师维度提出很有帮助的建议。Top Tags 今年的目标就是为iPad和Mac端专门提供针对平台交互的应用。还有很多保密性功能都会一一实现。
除了叮咚熊和Top Tags 今年刚开始开发一个定格动画的应用,弄了一半,因为这阵子有其他事情,暂时搁浅了(对的就是换工作,哈哈哈哈哈哈)。七月份应该会继续每天投入时间开发。每天争取最少一行代码,或者一行注释。哈哈哈哈哈哈哈哈哈哈哈。

  1. 这个博客网站,停更了吗?

应该不会停止,其间还接触了React Native开发,这块一直想做个总结,最近还在接触Unity,后续可能还会有AR技术。不过时间真的不太够用。并且最关键的是,比起写博客我更喜欢写代码,设计界面。但是技术栈的梳理这个工作还是蛮想继续下去的。不过不敢肯定给一个明确的时间点。

  1. 交流很不方便,为什么没有留言的功能?

最早开了留言功能,但是没人理我,哈哈,所以就关闭了。但是过了几年内容丰富了,才陆续有很多人给我发邮件提起这个需求。后面有认真想了下,还是比较社恐,邮件交流这种方式,比较适合我,有时间了就回复一下大家,但是多半没有回复的,希望大家谅解下。

  1. 有些内容比较浅显,有打算往深度扩展吗?

这个博客网站的宗旨是为了全,我很能体会刚接触一个新技术栈时候的那份迷茫,2018年有幸从Android技术栈切到iOS,那时候项目赶着上架,也是两眼一抹黑,加上脸皮薄,并且组内唯一会iOS的组长,工作又很忙,所以当时真的和盲人摸象一个样。也是从那一刻
有了重新梳理iOS知识体系的念头。想着能够和我一样的朋友,能够通过我的内容让大家快速对整个体系有个认知。等到有了初步认知后,建议可以通过对应领域的大牛的博客,继续深入学习。这是最初的目的。所以暂时没有完深度扩展的计划,不过谁说得准呢?有时间就搞,我是闲不下来的。哈哈哈。只不过太多更有意义的事情需要时间精力的投入。

第二点是一个很现实的问题,这些文章一般都是接触某一个领域一两年做的这个总结,某些深度是很难做到的。这个需要大家的谅解。一般有这种邮件过来,我都会回复并表示歉意,然后给他推荐自己觉得较好的站点或者文章。

那一年一直告诫自己不要太急着找妹子,因为当时坚信那时的长相一定是处于人生的低谷,但是这些年时光一直用那把杀猪刀告诉我“哥,那时的长相才是处于巅峰时期,你却花在泡图书馆,泡实验室,眼睁睁得看着一个个妹子被带走”


那一年有种自作自受叫做“和队员们泡了三个月的实验室,一起跷课,一起网上通宵调试,上课集体睡觉,只为了这些证书,和那辆至今记忆犹新的小车”

那一天它跑起来了,它能躲避障碍物了,老师说当时的我比看到自己孩子会走路了还开心

那一天它终于上场了,我们高兴坏了,结束的那一刻我们三个人眼睛同时进沙了,我们彼此用的是同一个借口“福州的沙子太多了”


那一年我们离开了校园,所有的书本论斤买了,大伙腐败了一顿,只有这些小本本加起来还不够一本书的重量,所以就留着了
那一年知道了这些红本本的另一种用处是:本本多了,在每一届新生交流会上你可以在一群懵懂的学妹面前吹嘘你那些所谓的光荣历史
那一年听说一个高校哥们靠替师妹做C语音作业,潜规则了师妹后,当时觉得自己是多么的纯洁,纯洁到师妹留的电话号码一直不敢拨打的地步,但是现在可能会悟到那个哥们估计不会C语言也会搞到那个师妹,与会不会C语言没有半毛钱关系


那一年大家都在忙着在抓住青春的尾巴在毕业季彻底的狂欢一把的时候,总有一些人不甘这么早早地谢幕,选择了在图书馆地下室死吭,三个月后又出现在了校园的舞台,续写着青春的序曲,可是在收到录取通知书后知道某某同学在一起了,某某同学怀孕了,一切都发生在那三个月的时间,于是乎感叹,永远不要太得瑟,不要太低估青春的荷尔蒙力量,那时和四年的大学生涯依依不舍地道别,不带走一个妹子


那一年拿着三个月换来的门票来到了厦门,一切都一样,实验室,图书馆,实验室,图书馆,唯一的区别要时不时的出差调试
那一年多亏了导师的眷顾和实验室的唯一妹子一起负责一个项目,然后那个妹子成了我的初恋,成了我现在的媳妇
那一年本该是荷尔蒙迸发的时刻,本该是花前月下充满诗意的时光,可是依旧需要三点一线,然而有了痛并快乐的感受


那一年它跑起来了,我快累趴了

那一年我用这本八十多页的本本,带走了我的妹子,导师在最后的谢师宴上才知道原来我的效率是如此之高不但搞定了项目,也搞定了实验室的室花,至今想想依旧会偷偷地傻笑

那一年不管有多少个三月都不能再续写青春的剧本,拖着行李开始了另一段旅程


那一年我们回到相识的城市领证了,然后在朋友圈里发了个状态 “我们在爱情的跑道转了一圈,回到了起点重新出发”

那一年我们有了可以把酒言欢的朋友,他肥了,我依旧瘦着,身处两地依旧相互支持,相互鼓励


那一年我们的成长是肆无忌惮的一件事情,而如今有一种成长叫痛并快乐着

一. 图层操作

  1. 选择图层,隐藏显示图层,锁定,解锁,改变图层顺序,移动图层
  2. 锁定像素,新建图层,图层分组,更改图层透明度
  3. 图层筛选
  4. 图层合并,图层混合,添加图层蒙版,增加图层填充
  5. 特殊图层:文字图层,形状图层,智能对象图层,调整对象图层,剪切图层,特殊图层转换为普通图层使用栅格化来转换
  6. 设置图层样式:双击图层面板的某个图层,可以弹出图层样式编辑弹窗,可以通过这种方式来添加图层阴影等样式
  7. 正常情况下调整层影响的是所有它之下的层,如果要指定某个层,将某个调整层和要应用的层放在一起,返回按下alt键盘,将光标移到两个层之间,这时候会出现一个向下箭头的提示,点击就可以了,还有一种方式是将两个层组合成组,然后光标选定组,
    这时候默认模式为穿透,选择正常即可。
  8. 调整对象图层:
    亮度对比度调节图层:一般发灰的照片调整对比度,发暗的照片调整亮度。
    色阶调节图层:调整之“色阶”
    颜色曲线调整
    曝光度调节图层
    自然饱和度调节图层
    色相/饱和度调节
    色彩平衡调节
    黑白调节
    照片滤镜
    通道混合器
    反相
    色调分离
    阈值
    可选颜色
    渐变映射

二. 对象选择操作

  1. 矩形/椭圆选择工具,可以调整抗锯齿和羽化两个参数来修改选择效果。
  2. 使用魔棒工具来选中相似颜色的对象。这里最关键的有两个:“容差值”,容差值越小越精确但是选中的区域越小。“连续”:默认情况下只会选中连续的块,对于某些由于隔离导致而没选中的可以先取消连续后重新选择。
  3. 快速选择工具:很好用的一个选择工具能够快速得选择对象,比魔棒要精确
  4. 对象选择区域:这个速度有点慢,但是还是蛮精确的,适合于边缘比较清晰的场合。
  5. 套索工具,多边形套索,磁性套索,这些一般用于选择细节部分,按下Shift选择的区域会追加到已经选择的区域,按下Alt会从已经选择的区域减去。
  6. 使用选择菜单栏里面的颜色范围和焦点区域也可以进行选择。
  7. 边缘调整可以调整存在软边的情况:在使用某种工具初步选择完毕后可以使用选择并遮住来细节调整。
  8. 查看选中区域使用Q快捷键,要改变选区显示的方式可以双击工具栏底部倒数第二个按钮,对颜色透明度进行修改。反向选择可以使用Command + Shift + I 快捷键。Command + D取消选择
  9. 使用Command + T键可以切换到自由变换模式,按下Shift 可以锁住宽高不变。
  10. 可以对对象进行:操控变换,透视变换,缩放,旋转,斜切,扭曲,透视,变形/拆分变形,水平垂直翻转,都在编辑菜单拦里面。
  11. 使用图层蒙版进行抠图:使用任意一个选区工具对对象进行初略选择,然后点击图层面板下面的剪切蒙层,就会新建一个剪切蒙层,这时候可以按下alt点击图层面板上的蒙层就会切换显示模式,这时候使用黑色画笔将要去掉的地方绘制,使用白色画笔在要恢复的地方绘制。
  12. 通过钢笔工具绘制轮廓,然后制作几何蒙层。

三. 蒙版操作

  1. 使用图层蒙版进行抠图:使用任意一个选区工具对对象进行初略选择,然后点击图层面板下面的剪切蒙层,就会新建一个剪切蒙层,这时候可以按下alt点击图层面板上的蒙层就会切换显示模式,这时候使用黑色画笔将要去掉的地方绘制,使用白色画笔在要恢复的地方绘制。
  2. 几何蒙层:使用多边形或者其他形状绘制工具绘制出几何形状,然后在面板上新建一个剪切蒙层,和图层蒙层的区别是它的外部是灰色的不是黑色的。

四. 智能对象操作

  1. 在正常工作过程中一般会将图片等对象转换为智能对象,这样缩放的时候不会损失画质,并且所有的操作都是无损的。将对象转换为智能对象可以通过:图层 –> 智能对象 –> 转换为智能对象。或者选中图层,右击选择转换为智能对象。
  2. 除了图片还可以将矢量工具转换为智能对象。还可以通过文件 –> 置入链接智能对象,将外部对象导入到项目中。这样的好处是只要链接的智能对象修改了,所有导入的都会随之改变。这种情况在保存的时候注意使用文件 –> 打包 将这些链接也打包进来。

五. 绘图操作

  1. 几个很重要的快捷键需要记住:
  • B 调出画笔
  • [] 调大调小画笔
  • Control + Alt + 横向拖动 改变大小,纵向拖动改变硬度。
  • 数字键盘输入改变透明度
  1. 画笔设置有两个重要面板:画笔面板,画笔,还有顶部的画笔设置工具栏
  2. 自定义画笔:绘制形状,选中形状,点击面板右上角的更多按钮,弹出对话框中选中新建画笔预设。编辑画笔。
  3. PS中的矢量工具有:基本的形状绘制,直接选择和路径选择工具,钢笔工具。

六. 文字操作

  1. 文字操作主要有两个主面板:字符面板和段落面板,以及字符样式面板和段落样式面板。
  2. 路径文字
  3. 将文字作为图片蒙版:将图片拖到文字上方,按住Alt点击图片和文字的中间即可。
  4. 文字景深制作:将要放在文字上方的通过剪切蒙版扣出来,底下放一个一样的图片,将文字放置于二者中间。

七. 图片操作

  1. 图片修复工具:

    在使用修复工具的时候有几步操作是必须的:新建图层,点击运用到所有层,有画笔的还可以选择打开画笔压感。
    最简单的是通过污点修复工具和修复工具来修复,修复工具需要按下alt + 点击鼠标选择采样点。后续的修复都是基于这些采样点来修复的。
    仿章工具它的特点是直接将采样点复制到对应的区域,没有做进一步的融合处理。还可以调出仿章工具面板,点击一个仿章源,在一个页面选择仿章采样点,切换到另一个页面就可以使用仿章工具,将形状复制过去了。

  2. 智能滤镜:
    将对象转换为智能对象,然后在智能对象之上添加滤镜效果。

  3. 内容感知功能:
    内容感知包括内容感知移动(位于工具栏),内容感知缩放,内容识别填充,修补工具。
    内容感知移动/扩展的时候可以将原来的使用算法识别的内容填充,然后将新的内容通过算法融入到目标环境中。
    内容感知缩放和内容感知填充也是同样道理。修补工具算是一个比较实用的工具,可以将选定的区域,使用目标区域来替换。

  4. 细节微调工具:
    使用减淡,加深,和海绵工具可以对图像明度,暗度及饱和度细节调整。

八. 钢笔操作

  1. 在PS中钢笔工具有两种用途,一种用于绘制路径,一种用于绘制图形,绘制图形的钢笔工具有描边和填充颜色,以及线条的设置,绘制路径的则没有这些配置,一般用于选择轮廓等用途。
  2. 在顶点点击不拖动会在两个顶点之间生成一条直线。
  3. 将钢笔放在在已经绘制的路径上点击会增加锚点,如果将钢笔工具放在已经绘制的顶点上则会删除锚点。
  4. 在绘制到第一个顶点,可以闭合后将会在钢笔的旁边出现一个圆形小圆圈
  5. 在绘制完成后可以按下alt键,这时候光标会显示一个箭头,这时候沿着绘制路径的方向可以将尖角改成圆角。
  6. 要改变锚点的位置可以通过直接选择工具来移动锚点。在绘制过程中如果需要改变位置可以在未抬起鼠标之前通过按下space移动。
  7. 在绘制圆角之后要绘制一个直角可以通过alt + 点击切换到直角模式。
  8. 要移动句柄可以通过按下Command + 点击句柄 移动
  9. 绘制完成后在AI中可以使用简化工具对路径进行简化:对象 –> 路径 –> 简化
  10. 选中后可以作为选区或者矢量蒙版,这些都可以通过右击在菜单中选中。
  11. 沿着路径填充描边:选择路径模式,右击路径,在弹出的菜单中选择填充描边。然后选择使用哪种工具填充。

总结

PS 中的关键词:图像,变换(对应编辑菜单),选择,滤镜,图层

画板操作

一个文件可以保存多个画板,按下如下按钮: 或者 Shift + O 可以激活画板编辑模式:
这时候选中一个画板顶部会出现一个编辑工具拦可以对画板板式,宽高,方向等进行编辑:

画板的常用操作:

  1. 在新建文档的时候批量新建多个画板
  2. 画板全部重新排列
  3. 画板重命名
  4. 画板尺寸修改

导航操作

  1. Z 切换到放大镜模式 按下Alt键会切换到缩小模式
  2. Command + 0 会将选中的放大到居于工作区合适的位置
  3. Space 点击鼠标拖动会拖动整个画布
  4. 点击界面的左上角可以将多个文档同时打开,在这种模式可以很方便得将一个文档的对象,拖动到另一个文档。

首选项设置:

  1. 打开首选项设置 Command + K
  2. 键盘增量设置,每次按下键盘会移动一个键盘增量,按下Shift每次按下键盘会移动10个键盘增量。
  3. 单位设置

基本形状操作:

AI中有如下的基本形状:

相关快捷操作:

  1. M 矩形
  2. L 椭圆形
  3. 画出多边形和星型的时候按下上下方向键盘会增加和减少边数
  4. 使用Shift 画出正方形和圆形
  5. 如果按下后发现放置的位置需要修改,可以按下Space键盘拖动形状到目标位置
  6. Shift + M 快速切换描边色和填充色
  7. D 快速恢复到默认状态
  8. 快速改变形状大小,快速设置圆角,快速旋转
  9. 形状生成器 快捷键(Shift + M) 正常情况下是将对应的块合并,如果按下Alt后滑动,表示挖去。
  10. 使用Shaper Shift + N 来画形状

技能清单:

  1. 画出基本形状,多边形和星型可以快速增加和减少边数,画出正方形和圆形
  2. 修改填充色和描边色,快速切换填充色和描边色,快速恢复到默认状态
  3. 修改描边的粗细,边角,虚线,箭头
  4. 偏移路径
  5. 基本形状变换:包括位置,尺寸,旋转角度,圆角,这里涉及到了变换面板
  6. 打开缩放圆角,缩放描边和效果
  7. AI也支持其他绘图工具的布尔运算,但是区别是在未确认是最终操作的时候需要按下Alt 避免自动扩展
  8. 除了这些工具外还有一个非常强大的工具-形状生成器 快捷键(Shift + M) 正常情况下是将对应的块合并,如果按下Alt后滑动,表示挖去。
  9. 使用扩展
  10. 使用外观面板添加多个填充,多个描边的形状,

整理对象操作: 对齐,分组,放置到新图层,改变对象在层中的层级位置

  1. 对于某些已经确定的物体可以将其锁定起来,快捷键为Command + 2
  2. 使用魔板工具可以快速选中相似颜色的
  3. 套索工具可以选中物体的锚点进行操作。
  4. 最常用的两个选择工具,号称小黑和小白的V,A 组合。
  5. Command + A 选择全部画板的全部对象, Command + Alt + A 选择当前画板的全部对象,Command + Shift + A 取消选择
  6. 使用Command + G 对对象进行编组操作,Command + Shift + G 取消编组
  7. 在图层面板新建一个图层使用Command + F 黏贴到最前面,然后改变对应层的层次结构
  8. 使用Command + []可以改变对象在图层中的层级关系
  9. 在工作中可以打开对齐面板来操作物体间的对齐操作,在对齐过程中如果需要辅助线,可以从标尺上拖动到某个位置就可以在当前位置添加一条辅助线。

颜色操作

AI中的着色工具包括:

  1. 色板:负责选择颜色
  2. 颜色:负责某个颜色细调
  3. 颜色参考和Adobe Color Theme: 用于选择颜色
  4. 渐变工具:调整渐变
  5. 通过外观面板fx修改形状外观。
  6. 吸管工具 I
  7. 使用重新着色工具对某个对象进行调整颜色,一般用于没有灵感的情况下,尝试配色使用。

绘图操作

  1. N 铅笔工具

  2. B 画笔工具

  3. Shift B 斑点画笔工具

  4. 铅笔,画笔设置项:保真度,平滑度,保持编辑状态,在一定距离的时候自动闭合,在某个距离继续画下去的时候会自动从上一次位置继续。

  5. 上色方法:
    5.1 使用斑点画笔工具,它的特点是可以将多次画的一块联合起来,并且可以使用[]来调整画笔宽度
    5.2 使用实时上色工具进行上色

  6. 其他绘图辅助工具:
    6.1 路径平滑工具:抹去线条的尖锐不平滑的地方
    6.2 连接工具:将对应的断开的线连接起来,然后使用平滑工具,锚点工具来编辑
    6.3 剪刀工具:用于断开路径
    6.4 橡皮擦工具:用于擦除内容
    6.5 美工刀工具:用于切割形状
    7 绘图模式:
    7.1 正常绘图:在物体上一层绘图
    7.2 背面绘图:在物体的下一层绘图
    7.3 内部绘图:在物体轮廓内部绘图

  7. 钢笔工具:手动生成路径
    使用P快捷键调出钢笔工具。点击中间锚点会打断对称,直线只要点击不要拖动,而曲线需要拖动,起点和终点一个在高峰/低谷的一边,一个在另一边。如果不确定可以按着space键移动。
    画完可以通过A 锚点工具来调整,+ 和删除锚点来修改,对于转折点可以使用Command点击拖动句柄来调整handler的方向,不要一味地在尖角的地方点终止,结束 command 点击屏幕

  8. 其他路径绘图工具
    除了使用 钢笔工具还可以使用曲率工具和重塑工具来绘制边界
    曲率工具: 先勾勒出轮廓,然后在调整轮廓的曲率,点击锚点可以在平滑和尖锐模式下切换
    重塑工具: 在钢笔绘制过程中按下alt切换到重塑工具,按下shift能够切换到半圆型,可以用这种方式画八卦
    弧线工具: x 切换方向 上下键 调整幅度
    弧线工具还有一个很好用的功能就是画下一个弧线的时候按住~移动会形成一个头发类似的东西。

  9. 变换工具
    变形工具
    旋转扭曲工具
    缩融工具
    膨胀工具
    晶格工具
    扇贝工具
    褶皱工具
    自由变换
    操控变形
    旋转,镜像
    封套扭曲 可以作用于对象,或者文本:可以用变形,网格,或者顶部对象作为封套。工具的具体路径在: 对象 –> 封套扭曲

  10. 同心圆形状
    画单个 —-> command + G —-> 效果 —-> 扭曲变换—> 变换 —-> 点击变换图案

文字操作

  1. 字体的常用编辑项目都在字体面板中:
  2. 字距微调 alt + 左右键 alt + 上下 行距微调,选择字 shift + alt 微调基线
  3. 可以使用工具栏的修饰文字工具来旋转字体以及缩放文字大小
  4. 使用工具栏中的网格工具来对文字进行变形操作,变形的按钮将会出现在右上角顶部。还可以使用 “对象 —> 封套扭曲 -> 用顶层对象” 注意形状要位于上层
  5. 使用文字样式和段落样式来管理全局的样式,以便统一。
  6. 文字路径:画路径,切换到文字模式,贴近路径会出现提示,输入文字即可。点击选择工具会出现三个光标开始,结束,中间,开始和结束可以移动文本在path上的位置,中间是整体移动,还可以将字符移到路径上面和路径下面。
    在形状外部和内部填充文字:
    在外部填充文字需要按下alt 然后点击形状轮廓

    在内部填充只需要点击形状轮廓即可
  7. 可以设计3D文字:在效果 –> 3D中调出来调整面板
  8. 添加文字轮廓:
    通过apearance 选中文本,新增填充,移动顺序从上到下 填充,字符 轮廓。修改轮廓大小
  9. 使用文字作为剪切蒙版:command + 7
  10. 可以设置文本环绕: 对象 -> 文本环绕

剪切蒙版 (和Sketch 不同AI剪切蒙版的模版在上面,内容在下面):

  1. 形状剪切蒙版 :对象->剪切蒙版 或者Command + 7 上面作为蒙版,下面作为内容
  2. 透明度剪切蒙版 :渐变,黑的减去,白的留下底部 在透明度面板里面设置

符号/图案操作

在我的理解中:图形样式,字符样式,段落样式以及符号都是用于管理整个项目使得项目具有统一的设计规范。

  1. 符号:可以将某个对象,拖动到符号面板,建立动态连接,如果后续其他地方还需要这个符号,就直接从符号面板拖动。如果要修改可以通过点击符号面板中的符号统一修改,这样文档中的所有符号都同时修改。符号编辑工具:
  2. 图案:利用多个图像构成一个图案,之后可以添加到色版中作为填充使用。创建图案方法: 对象 -> 图案 -> 建立。 图案编辑可以使用图案面板。

十一 其他高级操作

  1. 数据可视化表格操作
  2. 渐变网格
  3. 绘制透视视图
  4. 混合对象

总结

AI 中最关键的几个关键字:形状(路径),绘图,笔刷,颜色,文本

常用快捷键:

快捷键 介绍
A 插入画板
R 插入矩形
U 插入圆角矩形
O 插入圆形
V 矢量工具
T 插入文本
P 画笔
S 切片 (Export group content only 只到处组的内容,不包含背景/trim transparent pixel,会删除多余的透明像素)
B 是否显示边界
F 是否填充
Tab 在组件列表中上下移动
Command + Alt + C/V 复制黏贴样式
Command + C/V 复制黏贴对象
Alt + 拖动 复制
Command + D 就地复制
Command + Alt + F 查找并替换颜色
Command + R 重命名组件
Control + C 选取颜色
Command + Shift H 隐藏
Command + Shift L 锁定
Command + G 成组
Command + Shift + G 分组
Command + 2 放大选中的
Command + 4 放大当前的画板
Command + Control + F 进入全屏
Command + 单击 直接选择
按住空格键 + 移动 移动画布
Command + Shift + D 切换数据
Command + Shift + E 导出
按住 option 不放,鼠标放到另一个元素上 查看间距
Command-K 图层矢量缩放
Command + Alt +/- 对字体进行缩放
Command + Shift { } 字体对齐
Command + - 缩放
Command + [] 层级上下更改

常见技巧:

  • 加减乘除
    做界面的时候经常会出现这样的情况,要把一个图形均分三等分、四等分的,或者说加上30px、40px,这时候要手动去计算,然后再去输入,很浪费时间成本,而且对于数学不好的人来说,还容易算错。
    其实 sketch 自带就有一些快捷方法,在右侧尺寸大小的面板处,可以直接在尺寸后输入「+」「-」「*」「/」,后面跟上数字,输完之后确定,就可以得到想要的计算结果了。

  • 不透明度设置
    当要改变一个元素的不透明度的时候,没有必要到右侧的参数面板来修改,可以直接按数字来调整,比如你想把不透明度改到65%,那么选中这个元素后,直接按数字65就好了,不满意还可以重新输入新的数字,当不透明度是整数的时候,可以直接输入数字1234567890,就不用输「80」「90」了。

  • 移动微调
    按住 shift 不放,选择「上下左右」可以快速移动10px

  • 鼠标、Shift、Option实现快速修改

    主界面中的右侧的属性栏中可以单击属性值以快速递增或递减该值(注意要把鼠标放在文字的上下方位内,出现左右箭头才可以快速修改)。如果按住 Shift键,Sketch 将相应地将值递增或递减10倍,按住 Option键会将其增加/减少十分之一。

  • 输入方式更改大小和圆角

    支持简单数学公式,支持样式单位L(中心点左对齐)R(中心点居右)C(中心点居中)M(中心点居中)T(中心点置顶)B(中心点底部对齐),支持圆角快速自定义(格式是X;Y;N;M,顺序是左上-右上-右下-左下),能帮助快速精确调整,是不是很高级?

  • 图层对齐
    图层对齐也是经常要用到的操作。直接与对象对齐的话,Sketch 会将图层对齐到所有选定对象的最外层。但要将图层与特定对象对齐,怎么操作呢?比如想让下图中两个元素以正方形为参照物进行居中,那应该先使用图层列表中的锁定按钮或使用 Shift-Command-L 锁定该图层,然后再执行对齐操作即可。

  • 快速定位到上一级层级
    在很多图层未整理,处于比较混乱的情况下,按 command+鼠标左键只能快速的选到某个具体图层,但是接着你又在想,如果我还能一键快速选到图层所在的组,那该多好。
    方法是在选择具体图层后,按键盘上的 esc 键,能快速选择图层所在的上一级,可能是组,也可以是更上一级的组,甚至能快速选到画板,是不是很方便?选择后如果要移动就按 option+command,还能避免误操作。

技能清单:

  • 形状/文本/图片编辑
    位置设置(X,Y,W,H,旋转角度,镜像,圆角)
    填充(渐变,色彩,图片),边框(颜色,边框位置,宽度,结束点,衔接部位,虚线样式),阴影,内阴影,模糊
    缩放,变换,旋转,横向翻转,纵向翻转,布局约束
    文本路径

  • 布尔运算/蒙版/钢笔工具/路径
    形状蒙版,透明蒙版
    关闭路径,反向路径,裁剪路径,旋转复制,偏移,文本路径
    [蒙版] 下层负责显示形状,上层负责显示内容,一旦添加蒙版,上层的所有都会受到影响,可以使用ignore underling mark layer,添加蒙版可以选中底部的决定形状的图形,然后点击mark with selected shape 或者工具栏的mask图标

  • 切图/标注/导出

  • 图层/编组/对齐/分布/Make Grid/Copy Rotate

  • Flaten/Outline

  • Text Style/Layer Style/Symbols 复用样式
    Symbol 嵌套Symbol: 创建最外框,将最外框打包成组,将组创建成Symbol,点击Symbol 进入Symbols 界面。在里面添加某个控件,将这个控件创建成Symbol,在Symbol中选中新建的Symbol(这里不要选错了),Command + D复制一份。修改属性,比如下面的bg和avator

较好的文章

1.Objective C 相关

2.iOS基础相关

3.iOS进阶相关

4.iOS 源码分析系列

5.音视频相关

6.iOS 设计相关

7.其他

本打算这个系列结束的,但是由于疫情影响在家的这几天有空闲时间,想着把这块也整理下,今天这篇文章打算带大家熟悉下Runtime API.其这部分和Runtime重要的数据结构紧密相关,通过objc/runtime.h无非就是获得类,对象,Method,SEL,IMP,Ivar,Property,Protocal相关类以及对应的属性。这块还是蛮有规律的,最早接触Runtime还是蛮抗拒的,后面才慢慢觉得它的强大。废话不多说这篇博客主要分两大部分,一部分介绍上面提到的这些数据结构相关的数据,另一部分介绍十分重要的类型编码和属性编码。搞定这两部分估计大家在使用上就会游刃有余了,但是还是建立在理解Runtime各个环节的基础上。

这里推荐大家用Cooci可编译苹果官方源码objc来写demo这样可以比较方便得调试整个过程,这也是最近才发现的一个很棒的开源库,之前研究底层代码的时候要是早点发现这个就不会那么纠结了。好东西不私藏。https://github.com/LGCooci/objc4_debug:

在开始之前建议大家通读下我的《iOS Runtime源码分析系列》的博客,当然在这篇博客介绍的时候还会简要提一下相关内容。好了我们开始我们的API之旅:

首先是类相关的,这些都以****class_****开头:

类相关:

//获取类名
OBJC_EXPORT const char * _Nonnull class_getName(Class _Nullable cls)

//获取isa
OBJC_EXPORT Class _Nullable object_getClass(id _Nullable obj)

//获取父类
OBJC_EXPORT Class _Nullable class_getSuperclass(Class _Nullable cls)

获取实例变量的信息
1.实例变量是指变量不是属性.每个属性都有对应的实例变量,比如下面例子中包含属性name,那么它对应的实例变量为_name
2.这个方法可以获取属性的变量,也可以获取私有变量
3.如果没有找到对应的实例变量,就会返回nil
4.返回的列表不包含父类的实例变量
Ivar class_getClassVariable(Class cls, const char *name)

获取属性:
这个方法只能获得属性,不能获得变量
objc_property_t class_getProperty(Class cls, const char *name)

获取实例方法信息
OBJC_EXPORT Method _Nullable class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name);

获取类方法信息
OBJC_EXPORT Method _Nullable class_getClassMethod(Class _Nullable cls, SEL _Nonnull name)

获取SEL对应的实现体,也就是IMP
IMP class_getMethodImplementation(Class cls, SEL name)

通过这部分接口可以获得类名,父类,实例变量,属性,实例方法,类方法和IMP,注意这里只是获取单个的并不是全部的数据。如果要获得全部的就需要class_copyXXXX了。

获取数据列表类:

//获取实例变量列表
//1.获取所有[==私有变量==]和属性对应的变量
//2.获取的私有变量的名和定义的名一样
//3.获取的属性的名前面都添加了[下划线]
//4.注意这里[不包括父类的实例变量]
OBJC_EXPORT Ivar _Nonnull * _Nullable class_copyIvarList(Class _Nullable cls, unsigned int * _Nullable outCount);

//获取方法列表
//1.获取所有实例方法,[==不包含静态方法==]
//2.不获取[父类的方法]
//3.[隐式的get set 方法也能获取到]
//4.[==可以获取分类和动态添加的方法==]。
OBJC_EXPORT Method _Nonnull * _Nullable class_copyMethodList(Class _Nullable cls, unsigned int * _Nullable outCount)

//获取协议列表
//不能获取分类实现的协议
OBJC_EXPORT Protocol * __unsafe_unretained _Nonnull * _Nullable
class_copyProtocolList(Class _Nullable cls, unsigned int * _Nullable outCount)

//获取属性列表
//1.获取的属性名不自动添加下划线
//2.[不能获取Category添加的属性]。
OBJC_EXPORT objc_property_t _Nonnull * _Nullable
class_copyPropertyList(Class _Nullable cls, unsigned int * _Nullable outCount)

这些方法都需要注意最后调用free释放掉存放copy来的空间

添加方法
OBJC_EXPORT BOOL
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
const char * _Nullable types)

添加实例变量
//1.只能给动态创建的类添加变量也就是用 objc_allocateClassPair 创建的类
//2.添加变量只能在函数 objc_allocateClassPair 和 class_getInstanceVariable 之间添加才有效
OBJC_EXPORT BOOL
class_addIvar(Class _Nullable cls, const char * _Nonnull name, size_t size,
uint8_t alignment, const char * _Nullable types)

添加协议
OBJC_EXPORT BOOL
class_addProtocol(Class _Nullable cls, Protocol * _Nonnull protocol)

//添加属性
//1. 添加属性不用在objc_registerClassPair之前,因为添加属性其实就是添加变量的set 和 get方法而已
//2. 添加的属性和变量不能用kvc设置值和取值
OBJC_EXPORT BOOL
class_addProperty(Class _Nullable cls, const char * _Nonnull name,
const objc_property_attribute_t * _Nullable attributes,
unsigned int attributeCount)

替换方法
OBJC_EXPORT IMP _Nullable
class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types)

替换属性
OBJC_EXPORT void
class_replaceProperty(Class _Nullable cls, const char * _Nonnull name,
const objc_property_attribute_t * _Nullable attributes,
unsigned int attributeCount)

创建实例
OBJC_EXPORT id _Nullable
class_createInstance(Class _Nullable cls, size_t extraBytes)

遵循协议
OBJC_EXPORT BOOL
class_conformsToProtocol(Class _Nullable cls, Protocol * _Nullable protocol)

判断是否响应某个SEL
OBJC_EXPORT BOOL
class_respondsToSelector(Class _Nullable cls, SEL _Nonnull sel)

下面是简要的实例:

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface IDLTestObject : NSObject {
NSString *privateInstanceVar;
}

@property(nonatomic, strong, readwrite) NSString *name;

@property(nonatomic, assign, readwrite) NSInteger age;

- (void)testInstanceMethod;

@end

NS_ASSUME_NONNULL_END
#import "IDLTestObject.h"

@implementation IDLTestObject

- (void)testInstanceMethod {
NSLog(@"run testInstanceMethod");
}

+ (void)testClassMethod {
NSLog(@"testclassMethod");
}

@end
//获取类名
const char *className = class_getName([IDLTestObject class]);
NSLog(@"className : %@",[NSString stringWithUTF8String:className]);

//获取父类
Class superClass = class_getSuperclass([IDLTestObject class]);
NSLog(@"superClass : %@", NSStringFromClass(superClass));

//获取实例对象的大小
size_t instanceSize = class_getInstanceSize([IDLTestObject class]);
NSLog(@"instanceSize = %zu",instanceSize);

//获取实例变量信息 如果没有找到会返回nil
Ivar nameVar = class_getInstanceVariable([IDLTestObject class], [@"_name" UTF8String]);
NSLog(@"InstanceVariable :%@",[NSString stringWithUTF8String:ivar_getName(nameVar)]);
NSLog(@"Instance Variable TypeEncoding:%@",[NSString stringWithUTF8String:ivar_getTypeEncoding(nameVar)]);
NSLog(@"Instance Variable Offset:%td",ivar_getOffset(nameVar));

//获取实例变量列表
unsigned int outCount = 0;
Ivar *varList = class_copyIvarList([IDLTestObject class], &outCount);
for (unsigned int index = 0 ;index < outCount; index++) {
NSLog(@"var %u = %@",index,[NSString stringWithUTF8String:ivar_getName(varList[index])]);
}
free(varList);

//获取对象属性
objc_property_t property = class_getProperty([IDLTestObject class], [@"age" UTF8String]);
NSLog(@"Instance Property Name :%@",[NSString stringWithUTF8String:property_getName(property)]);
NSLog(@"Instance Property Attribute:%@",[NSString stringWithUTF8String:property_getAttributes(property)]);

//获取实例方法
Method method = class_getInstanceMethod([IDLTestObject class], @selector(testInstanceMethod));
NSLog(@"get InstanceMethod %@",NSStringFromSelector(method_getName(method)));

//获取方法列表
Method *methodList = class_copyMethodList([IDLTestObject class], &outCount);
for (unsigned int index = 0; index < outCount; index++) {
NSLog(@"method %u = %@",index,NSStringFromSelector(method_getName(methodList[index])));
}
free(methodList);

//获取类方法
Method classMethod = class_getClassMethod([IDLTestObject class], @selector(testClassMethod));
NSLog(@"get classMethod %@",NSStringFromSelector(method_getName(classMethod)));

IMP testMethod = class_getMethodImplementation([IDLTestObject class], @selector(testInstanceMethod));
testMethod();


//给某个类添加方法
class_addMethod([IDLTestObject class],
@selector(addedMethod),
(IMP)addedMethodIMP,
"v@:");

[[IDLTestObject new] performSelector:@selector(addedMethod)];

objc_property_attribute_t type = { "T", "@\"NSString\"" }; //T:类型
objc_property_attribute_t ownership = { "C", "" }; // C:copy
objc_property_attribute_t nonatomic = { "N", "" }; // N:nonatomic
objc_property_attribute_t ivar = { "V", "_addedMethod" }; //V: 实例变量变量名
objc_property_attribute_t attributs[] = { type, ownership,nonatomic, ivar };
class_addProperty([IDLTestObject class], "addedMethod", attributs, 4);

//输出添加的成员变量
unsigned int count;
objc_property_t *propertyList = class_copyPropertyList([IDLTestObject class], &count);
for (unsigned int i = 0; i< count; i++) {
const char *name = property_getName(propertyList[i]);
NSLog(@"属性%@ 属性信息%@",[NSString stringWithUTF8String:name],
[NSString stringWithUTF8String:property_getAttributes(propertyList[i])]);
}
free(propertyList);

//替换方法
class_replaceMethod([IDLTestObject class],
@selector(testInstanceMethod),
(IMP)addedMethodIMP,
"v@:");
[[[IDLTestObject alloc] init] testInstanceMethod];

//创建实例
IDLTestObject *testClass = class_createInstance([IDLTestObject class], 0);
[testClass performSelector:@selector(testInstanceMethod)];

Class DynamicClass = objc_allocateClassPair([NSObject class], "DynamicClass", 0);
class_addIvar(DynamicClass, "_attribute0", sizeof(NSString *), log(sizeof(NSString *)), "i");
Ivar ivar = class_getInstanceVariable(DynamicClass, "_attribute0");
NSLog(@"DynamicClass var:%@",[NSString stringWithUTF8String:ivar_getName(ivar)]);
objc_registerClassPair(DynamicClass);

输出结果如下:

2020-02-08 19:05:51.294372+0800 objc-debug[43702:387877] className : IDLTestObject
2020-02-08 19:05:51.296220+0800 objc-debug[43702:387877] superClass : NSObject
2020-02-08 19:05:51.297028+0800 objc-debug[43702:387877] instanceSize = 32
2020-02-08 19:05:51.299444+0800 objc-debug[43702:387877] InstanceVariable :_name
2020-02-08 19:05:51.306411+0800 objc-debug[43702:387877] Instance Variable TypeEncoding:@"NSString"
2020-02-08 19:05:51.306766+0800 objc-debug[43702:387877] Instance Variable Offset:16
2020-02-08 19:05:51.307020+0800 objc-debug[43702:387877] var 0 = privateInstanceVar
2020-02-08 19:05:51.307322+0800 objc-debug[43702:387877] var 1 = _name
2020-02-08 19:05:51.307559+0800 objc-debug[43702:387877] var 2 = _age
2020-02-08 19:05:51.308250+0800 objc-debug[43702:387877] Instance Property Name :age
2020-02-08 19:05:51.308553+0800 objc-debug[43702:387877] Instance Property Attribute:Tq,N,V_age
2020-02-08 19:05:51.308878+0800 objc-debug[43702:387877] get InstanceMethod testInstanceMethod
2020-02-08 19:05:51.309351+0800 objc-debug[43702:387877] method 0 = testInstanceMethod
2020-02-08 19:05:51.309624+0800 objc-debug[43702:387877] method 1 = .cxx_destruct
2020-02-08 19:05:51.334783+0800 objc-debug[43702:387877] method 2 = name
2020-02-08 19:05:51.335325+0800 objc-debug[43702:387877] method 3 = setName:
2020-02-08 19:05:51.335856+0800 objc-debug[43702:387877] method 4 = age
2020-02-08 19:05:51.336313+0800 objc-debug[43702:387877] method 5 = setAge:
2020-02-08 19:05:51.336745+0800 objc-debug[43702:387877] get classMethod testClassMethod
2020-02-08 19:05:51.336909+0800 objc-debug[43702:387877] run testInstanceMethod
2020-02-08 19:05:51.340473+0800 objc-debug[43702:387877] run addedMethodIMP
2020-02-08 19:05:51.351977+0800 objc-debug[43702:387877] 属性addedMethod 属性信息T@"NSString",C,N,V_addedMethod
2020-02-08 19:05:51.352879+0800 objc-debug[43702:387877] 属性name 属性信息T@"NSString",&,N,V_name
2020-02-08 19:05:51.353429+0800 objc-debug[43702:387877] 属性age 属性信息Tq,N,V_age
2020-02-08 19:05:51.353906+0800 objc-debug[43702:387877] run addedMethodIMP
2020-02-08 19:05:51.354370+0800 objc-debug[43702:387877] run addedMethodIMP
2020-02-08 19:05:51.354378+0800 objc-debug[71958:585781] DynamicClass var:_attribute0
Ivar实例变量相关:
//获取变量名
OBJC_EXPORT const char * _Nullable
ivar_getName(Ivar _Nonnull v)

//获取变量类型描述
OBJC_EXPORT const char * _Nullable
ivar_getTypeEncoding(Ivar _Nonnull v)

//获取变量基地址偏移量
OBJC_EXPORT ptrdiff_t
ivar_getOffset(Ivar _Nonnull v)

上面的三个方法基本上对应于IVar的数据结构的各个字段,后面会单独介绍TypeEncoding和属性编码。

Property属性相关:
//获取属性名
OBJC_EXPORT const char * _Nonnull
property_getName(objc_property_t _Nonnull property)

//获取属性编码
OBJC_EXPORT const char * _Nullable
property_getAttributes(objc_property_t _Nonnull property)

//获取属性列表返回的objc_property_attribute_t的结构如下:
//typedef struct {
// const char *name;
// const char *value;
//} objc_property_attribute_t;
OBJC_EXPORT objc_property_attribute_t * _Nullable
property_copyAttributeList(objc_property_t _Nonnull property,
unsigned int * _Nullable outCount)

//获取属性的属性值
OBJC_EXPORT char * _Nullable
property_copyAttributeValue(objc_property_t _Nonnull property,
const char * _Nonnull attributeName)
Method方法相关:
//获得方法名
OBJC_EXPORT SEL _Nonnull
method_getName(Method _Nonnull m)

//获得方法的实现
OBJC_EXPORT IMP _Nonnull
method_getImplementation(Method _Nonnull m)
OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

//获得参数和返回值的类型编码
OBJC_EXPORT const char * _Nullable
method_getTypeEncoding(Method _Nonnull m)

//获得参数数量
OBJC_EXPORT unsigned int
method_getNumberOfArguments(Method _Nonnull m)

//获得返回类型
OBJC_EXPORT char * _Nonnull
method_copyReturnType(Method _Nonnull m)

//获得参数类型
OBJC_EXPORT char * _Nullable
method_copyArgumentType(Method _Nonnull m, unsigned int index)

//获得返回类型,指定存储空间
OBJC_EXPORT void
method_getReturnType(Method _Nonnull m, char * _Nonnull dst, size_t dst_len)

//获得参数类型,指定存储空间
OBJC_EXPORT void
method_getArgumentType(Method _Nonnull m, unsigned int index,
char * _Nullable dst, size_t dst_len)

//获得方法描述
OBJC_EXPORT struct objc_method_description * _Nonnull
method_getDescription(Method _Nonnull m)

设置方法实现
OBJC_EXPORT IMP _Nonnull
method_setImplementation(Method _Nonnull m, IMP _Nonnull imp)

//交换两个方法的实现
OBJC_EXPORT void
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)

之前博客已经介绍过,方法主要有方法名SEL,包含方法参数,返回值类型的编码,以及实现体IMP.这里最重要的是类型编码在后面统一介绍。这里还增加了可以分开获取方法参数和返回值类型编码的方法,以及方法参数数量。

SEL相关:
//返回SEL名字
OBJC_EXPORT const char * _Nonnull sel_getName(SEL _Nonnull sel)

//在系统中注册一个方法,将方法名映射到一个选择器,并返回这个选择器
OBJC_EXPORT SEL _Nonnull sel_registerName(const char * _Nonnull str)

//对比方法选择器
OBJC_EXPORT BOOL sel_isEqual(SEL _Nonnull lhs, SEL _Nonnull rhs)
协议Protocal相关:
//返回一个指定的协议
OBJC_EXPORT Protocol * _Nullable objc_getProtocol(const char * _Nonnull name);

//返回运行时所有的协议
OBJC_EXPORT Protocol * __unsafe_unretained _Nonnull * _Nullable
objc_copyProtocolList(unsigned int * _Nullable outCount);

//返回某个协议是否遵循另一个协议
OBJC_EXPORT BOOL
protocol_conformsToProtocol(Protocol * _Nullable proto,
Protocol * _Nullable other)

//返回协议是否相等
OBJC_EXPORT BOOL
protocol_isEqual(Protocol * _Nullable proto, Protocol * _Nullable other)

//返回协议名
OBJC_EXPORT const char * _Nonnull
protocol_getName(Protocol * _Nonnull proto)

//返回协议的某个方法描述
OBJC_EXPORT struct objc_method_description
protocol_getMethodDescription(Protocol * _Nonnull proto, SEL _Nonnull aSel,
BOOL isRequiredMethod, BOOL isInstanceMethod)

//返回协议的所有方法描述
OBJC_EXPORT struct objc_method_description * _Nullable
protocol_copyMethodDescriptionList(Protocol * _Nonnull proto,
BOOL isRequiredMethod,
BOOL isInstanceMethod,
unsigned int * _Nullable outCount)

//返回协议指定的属性
OBJC_EXPORT objc_property_t _Nullable
protocol_getProperty(Protocol * _Nonnull proto,
const char * _Nonnull name,
BOOL isRequiredProperty, BOOL isInstanceProperty)

//返回协议的所有属性
OBJC_EXPORT objc_property_t _Nonnull * _Nullable
protocol_copyPropertyList(Protocol * _Nonnull proto,
unsigned int * _Nullable outCount)

OBJC_EXPORT objc_property_t _Nonnull * _Nullable
protocol_copyPropertyList2(Protocol * _Nonnull proto,
unsigned int * _Nullable outCount,
BOOL isRequiredProperty, BOOL isInstanceProperty)
//返回某个协议遵循的协议列表
OBJC_EXPORT Protocol * __unsafe_unretained _Nonnull * _Nullable
protocol_copyProtocolList(Protocol * _Nonnull proto,
unsigned int * _Nullable outCount)

动态创建协议

//创建协议
OBJC_EXPORT Protocol * _Nullable
objc_allocateProtocol(const char * _Nonnull name)

//向Runtime注册协议
OBJC_EXPORT void
objc_registerProtocol(Protocol * _Nonnull proto)
OBJC_AVAILABLE(10.7, 4.3, 9.0, 1.0, 2.0);

//向协议添加方法描述
OBJC_EXPORT void
protocol_addMethodDescription(Protocol * _Nonnull proto, SEL _Nonnull name,
const char * _Nullable types,
BOOL isRequiredMethod, BOOL isInstanceMethod)

//添加协议
OBJC_EXPORT void
protocol_addProtocol(Protocol * _Nonnull proto, Protocol * _Nonnull addition)
OBJC_AVAILABLE(10.7, 4.3, 9.0, 1.0, 2.0);

给协议添加属性
OBJC_EXPORT void
protocol_addProperty(Protocol * _Nonnull proto, const char * _Nonnull name,
const objc_property_attribute_t * _Nullable attributes,
unsigned int attributeCount,
BOOL isRequiredProperty, BOOL isInstanceProperty)

类型编码与属性编码

编译器将每个方法的返回值和参数类型编码为一个字符串,并将其与方法的selector关联在一起。同时每个属性的类型也进行了编码,下面是这两部分的官方文档,供大家查阅使用。

我们给出一个例子,下面的例子会将方法的返回值以及参数编码打印出来:

这个方法如下所示:

+ (IDLReturnType *)testTypeEncoding:(NSString *)strValue intValue:(NSInteger)intValue {
return [IDLReturnType new];
}
Method testMethods = class_getClassMethod([IDLTestObject class], @selector(testTypeEncoding:intValue:));
NSLog(@"Type Encoding : %@",[NSString stringWithUTF8String:method_getTypeEncoding(testMethods)]);
char *returnType = method_copyReturnType(testMethods);
NSLog(@"Return Type: %@",[NSString stringWithUTF8String:returnType]);
free(returnType);

NSInteger numberOfArguments = method_getNumberOfArguments(testMethods);
char *argument = NULL;
for (unsigned int index = 0; index < numberOfArguments; index++) {
argument = method_copyArgumentType(testMethods, index);
NSLog(@"Index %d Return Type: %@", index,[NSString stringWithUTF8String:argument]);
}
free(argument);

输出内容为:

2020-02-09 10:21:22.231254+0800 objc-debug[84523:1305673] Type Encoding : @32@0:8@16q24
2020-02-09 10:21:22.231343+0800 objc-debug[84523:1305673] Return Type: @
2020-02-09 10:21:22.231425+0800 objc-debug[84523:1305673] Index 0 Return Type: @
2020-02-09 10:21:22.231503+0800 objc-debug[84523:1305673] Index 1 Return Type: :
2020-02-09 10:21:22.231579+0800 objc-debug[84523:1305673] Index 2 Return Type: @
2020-02-09 10:21:22.231653+0800 objc-debug[84523:1305673] Index 3 Return Type: q

我们看****@32@0:8@16q24****这实际上就是方法的类型编码。我们先看下类型编码表:


这里每个部分都包含两位,前面一个表示编码,后面表示这个数据对应的偏移量,返回的类型数据会将返回值放在最后面,所以****@32@0:8@16q24****对应部分的意义如下:

@32 返回值编码为@,也就是id类型,所处的偏移量为32。也就是q24的后面
@0 第一个参数的编码为@,也就是id类型,所处的偏移量为0,由于第一个参数恒为方法的接收者,所以是id类型。
:8 第二个参数的编码为: 也就是SEL类型,所处的偏移量为8,由于第二个参数恒为_cmd所以是SEL类型。
@16 第三个参数的编码为@ 也就是id类型,所处的偏移量为16,上面第三个参数为NSString*
q24 第四个参数的编码为q 也就是long long类型,由于用的是64位机器所以对得上。

Runtime为了我们方便,为我们提供了@encode编译器指令来获取它,当给定一个类型时,@encode返回这个类型的字符串编码,如下所示:

NSLog(@"char     : %s, %lu", @encode(char), sizeof(char));
NSLog(@"short : %s, %lu", @encode(short), sizeof(short));
NSLog(@"int : %s, %lu", @encode(int), sizeof(int));
NSLog(@"long : %s, %lu", @encode(long), sizeof(long));
NSLog(@"long long: %s, %lu", @encode(long long), sizeof(long long));
NSLog(@"float : %s, %lu", @encode(float), sizeof(float));
NSLog(@"double : %s, %lu", @encode(double), sizeof(double));
NSLog(@"NSInteger: %s, %lu", @encode(NSInteger), sizeof(NSInteger));
NSLog(@"CGFloat : %s, %lu", @encode(CGFloat), sizeof(CGFloat));
NSLog(@"int32_t : %s, %lu", @encode(int32_t), sizeof(int32_t));
NSLog(@"int64_t : %s, %lu", @encode(int64_t), sizeof(int64_t))

2020-02-09 11:04:28.116484+0800 objc-debug[91476:1363542] char     : c, 1
2020-02-09 11:04:28.116991+0800 objc-debug[91476:1363542] short : s, 2
2020-02-09 11:04:28.117745+0800 objc-debug[91476:1363542] int : i, 4
2020-02-09 11:04:28.118195+0800 objc-debug[91476:1363542] long : q, 8
2020-02-09 11:04:28.118943+0800 objc-debug[91476:1363542] long long: q, 8
2020-02-09 11:04:28.121076+0800 objc-debug[91476:1363542] float : f, 4
2020-02-09 11:04:28.123510+0800 objc-debug[91476:1363542] double : d, 8
2020-02-09 11:04:28.124016+0800 objc-debug[91476:1363542] NSInteger: q, 8
2020-02-09 11:04:28.124132+0800 objc-debug[91476:1363542] CGFloat : d, 8
2020-02-09 11:04:28.124211+0800 objc-debug[91476:1363542] int32_t : i, 4
2020-02-09 11:04:28.124283+0800 objc-debug[91476:1363542] int64_t : q, 8

一般我们会使用:

method_copyArgumentType(testMethods, index);

这种方式,避免涉及到偏移量等数据,直接获取参数类型。

  • 属性编码

前面提到一个例子:

objc_property_t property = class_getProperty([IDLTestObject class], [@"age" UTF8String]);
NSLog(@"Instance Property Name :%@",[NSString stringWithUTF8String:property_getName(property)]);
NSLog(@"Instance Property Attribute:%@",[NSString stringWithUTF8String:property_getAttributes(property)]);

age属性的声明如下所示:

@property(nonatomic, copy, readonly,getter=ageValue,setter=setAageValue:) NSString *age;

输出结果为:

2020-02-09 11:23:38.356033+0800 objc-debug[94653:1391447] Instance Property Attribute:T@"NSString",R,C,N,GageValue,SsetAageValue:,V_age

我们对照上表给大家分析:
属性编码是通过逗号进行分隔开的,T@”NSString”,R,C,N,GageValue,SsetAageValue:,V_age 表示的意义如下:

* T@"NSString" 属性类型为NSString
* R 属性类型为ReadOnly类型
* C 属性存储属性为Copy
* N 属性为nonatomic
* GageValue 属性指定getter方法为ageValue
* SsetAageValue: 属性指定setter方法为setAageValue:
* V_age 属性对应的实例变量为_age

对于类型编码和属性编码大致就这些。大家可以通过下面的表格熟悉下属性编码:

下面是模拟IDLPainterDeveloper继承IDLDeveloper以及IDLPainter的例子,直接上代码吧:

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface IDLDeveloper : NSObject

- (void)coding;

@end

NS_ASSUME_NONNULL_END
#import "IDLDeveloper.h"

@implementation IDLDeveloper

- (void)coding {
NSLog(@"I Can coding!");
}

@end
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface IDLPainter : NSObject

- (void)drawing;

@end

NS_ASSUME_NONNULL_END
#import "IDLPainter.h"

@implementation IDLPainter

- (void)drawing {
NSLog(@"I Can Drawing!");
}

@end
NS_ASSUME_NONNULL_BEGIN

@interface IDLPainterDeveloper : NSProxy

+ (instancetype)createPerson;

@end

NS_ASSUME_NONNULL_END
#import "IDLPainterDeveloper.h"

//super
#import "IDLPainter.h"
#import "IDLDeveloper.h"

@interface IDLPainterDeveloper ()

@property(nonatomic, strong, readwrite) NSMutableDictionary *methodDictionary;

@end

@implementation IDLPainterDeveloper

+ (instancetype)createPerson {
return [[IDLPainterDeveloper alloc] init];
}

- (instancetype)init {

IDLPainter *painter = [[IDLPainter alloc] init];
IDLDeveloper *developer = [[IDLDeveloper alloc] init];

[self inheriteMethodsFromSuperTarget:painter];
[self inheriteMethodsFromSuperTarget:developer];
return self;
}

- (void)inheriteMethodsFromSuperTarget:(id)target{
if(!target) return;
unsigned int numberOfMethods = 0;
Method *methodList = class_copyMethodList([target class], &numberOfMethods);
for (int i = 0; i < numberOfMethods; i++) {
SEL sel = method_getName(methodList[i]);
const char *methodName = sel_getName(sel);
[self.methodDictionary setObject:target forKey:[NSString stringWithUTF8String:methodName]];
}
free(methodList);
}

- (void)forwardInvocation:(NSInvocation *)invocation{
SEL sel = invocation.selector;
NSString *methodName = NSStringFromSelector(sel);

id target = self.methodDictionary[methodName];

if (target && [target respondsToSelector:sel]) {
[invocation invokeWithTarget:target];
} else {
[super forwardInvocation:invocation];
}
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
NSString *methodName = NSStringFromSelector(sel);
id target = self.methodDictionary[methodName];
if (target && [target respondsToSelector:sel]) {
return [target methodSignatureForSelector:sel];
} else {
return [super methodSignatureForSelector:sel];
}
}

- (NSMutableDictionary *)methodDictionary {
if(!_methodDictionary) {
_methodDictionary = [[NSMutableDictionary alloc] init];
}
return _methodDictionary;
}

@end

Runtime 用处蛮多的但是归结起来有如下几个:

1. 使用Runtime获得对象的属性,方法,协议等信息:
* 实现NSCoding的自动归档和解档
* 实现字典的模型和自动转换

这个大家可以阅读YYModel,MJExtension这些开源库,从中学习这部分的用法。

下面给出简化版的代码来方便大家理解Runtime在这方面的应用:

  • 实现NSCoding的自动归档和解档

这个比较简单,就是通过class_copyIvarList获取到类到各个实例变量,然后通过KVC方式获取或者设置当前的属性,这里需要注意的是YYModel中针对某些不支持KVC的类型通过向属性的setter/getter发送消息的方式来替换KVC.目前几乎所有的Objective-C对象都能使用KVC,但是一些纯Swift类和结构体是不支持KVC的。或许YYModel出于这方面考虑使用了通过发送消息的方式来取值和设值。

#ifndef CommonHeader_h
#define CommonHeader_h

#define INIT_WITH_CODER \
- (instancetype)initWithCoder:(NSCoder *)coder { \
if(self = [super init]) { \
unsigned int outCount = 0; \
Ivar *varList = class_copyIvarList([self class], &outCount); \
for (unsigned int index = 0; index < outCount; index++) { \
const char *var = ivar_getName(varList[index]); \
if(!var || !strlen(var)) continue; \
NSString *varName = [NSString stringWithUTF8String:var]; \
id value = [coder decodeObjectForKey:varName]; \
if(value) { \
[self setValue:value forKey:varName]; \
} \
} \
free(varList); \
} \
return self; \
} \

#define ENCODE_WITH_CODER \
- (void)encodeWithCoder:(NSCoder *)coder { \
unsigned int outCount = 0; \
Ivar *varList = class_copyIvarList([self class], &outCount); \
for (unsigned int index = 0; index < outCount; index++) { \
const char *var = ivar_getName(varList[index]); \
if(!var || !strlen(var)) continue; \
NSString *varName = [NSString stringWithUTF8String:var]; \
id value = [self valueForKey:varName]; \
[coder encodeObject:value forKey:varName]; \
} \
free(varList); \
} \

#endif /* CommonHeader_h */

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface IDLTopic : NSObject<NSCoding>

@property(nonatomic, strong) NSString *topicName;

@property(nonatomic, assign) NSInteger topicRankStar;

@end

@interface IDLUser : NSObject<NSCoding>

@property(nonatomic, strong) NSString *userName;

@property(nonatomic, assign) NSInteger userAge;

@property(nonatomic, strong) NSString *localAddress;

@property(nonatomic, strong) NSString *country;

@property(nonatomic, strong) NSArray<IDLTopic *> *topics;

@end

NS_ASSUME_NONNULL_END
#import "IDLUser.h"
#import <objc/runtime.h>
#import "CommonHeader.h"

@implementation IDLTopic

INIT_WITH_CODER

ENCODE_WITH_CODER

@end

@implementation IDLUser

+ (NSDictionary *)arrayElementModelTypeMap {
return @{@"topics":[IDLTopic class]};
}

INIT_WITH_CODER

ENCODE_WITH_CODER

@end
  • 字典转模型:
    这部分的思路就是通过class_copyIvarList获取Model的各个属性,然后去掉属性名字下的下划线,作为查询字典的key。取出值通过KVC设置到model,
    这里有两个比较关键的问题,当model属性为字典或者自定义类型的时候需要怎么处理,当model属性为数组的时候怎么处理。大家看下面代码:
NS_ASSUME_NONNULL_BEGIN

@interface IDLTopic : NSObject

@property(nonatomic, strong) NSString *topicName;

@property(nonatomic, assign) NSInteger topicRankStar;

@end

@interface IDLUser : NSObject

@property(nonatomic, strong) NSString *userName;

@property(nonatomic, assign) NSInteger userAge;

@property(nonatomic, strong) NSString *localAddress;

@property(nonatomic, strong) NSString *country;

@property(nonatomic, strong) NSArray<IDLTopic *> *topics;

@end

NS_ASSUME_NONNULL_END
#import "IDLUser.h"

@implementation IDLTopic

@end

@implementation IDLUser

+ (NSDictionary *)arrayElementModelTypeMap {
return @{@"topics":[IDLTopic class]};
}

@end
+ (instancetype)idl_modelWithDic:(NSDictionary *)dictionary {

if(!dictionary || ![dictionary count]) return nil;

id objc = [[self alloc] init];

unsigned int outCount = 0;
//没有参数的情况
Ivar *varList = class_copyIvarList([self class], &outCount);
if(!outCount) {
free(varList);
return objc;
}

for (unsigned int index = 0; index < outCount; index++) {

//获取到属性名称
NSString *varName = [NSString stringWithUTF8String:ivar_getName(varList[index])];
if(!varName || ![varName length]) continue;
//去掉下划线_
varName = [varName substringFromIndex:1];
//去掉@ 和 “ 之后的实例变量类型 获取到的时候类型为@"NSString"这种
NSString *varType = [NSString stringWithUTF8String:ivar_getTypeEncoding(varList[index])];
varType = [varType stringByReplacingOccurrencesOfString:@"\"" withString:@""];
varType = [varType stringByReplacingOccurrencesOfString:@"@" withString:@""];

//取得变量值
id varValue = dictionary[varName];

//自定义类型处理
if([varValue isKindOfClass:[NSDictionary class]] || ![varType hasPrefix:@"NS"]) {
//拿到自定义类型 递归生成 自定义的类型属性
Class modelClass = NSClassFromString(varType);
if(modelClass) {
varValue = [modelClass idl_modelWithDic:varValue];
}
}

//实例变量为数组类型
if([varValue isKindOfClass:[NSArray class]] || [varValue isKindOfClass:[NSMutableArray class]]) {
//获取映射的模型类型
if([self respondsToSelector:@selector(arrayElementModelTypeMap)]) {
//拿到元素类型
NSDictionary *arrayElementModelTypeMap = [self performSelector:@selector(arrayElementModelTypeMap)];
Class elementModelType = arrayElementModelTypeMap[varName];

NSMutableArray *array = [NSMutableArray new];
for (NSDictionary *dict in varValue/*这个为字典数组*/) {
id elementValue = [elementModelType idl_modelWithDic:dict];
[array addObject:elementValue];
}
varValue = array;
}
}

//赋值
if(varValue) {
[objc setValue:varValue forKey:varName];
}
}
free(varList);
return objc;
}

2. 通过Runtime动态创建一个类,给类增加方法,协议,属性等信息,调用私有方法:
  • 在方法决议阶段给类添加方法
void speak() {
NSLog(@"Hello World!");
}

@implementation IDLUser

+ (BOOL)resolveInstanceMethod:(SEL)sel {
if([NSStringFromSelector(sel) isEqualToString:@"speak"]) {
class_addMethod([self class], @selector(speak), (IMP)speak, "v@:");
}
return [super resolveInstanceMethod:sel];
}

@end

[user performSelector:@selector(speak)];

  • 调用私有方法
UIView *maskView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
maskView.backgroundColor = [UIColor redColor];
SEL selector = NSSelectorFromString(@"setMaskView:");
[self.view performSelector:selector withObject:maskView];
3. 使用Method Swizzling交换方法实现

Method Swizzling的主要用于改变已经存在的selector 的实现,我们知道每个方法都和一个selector以及IMP相对应。但是iOS的Runtime 提供了一种改变这种对应关系的方法,它就是Method Swizzling,直接上代码,这个例子来自 nshipster Method Swizzling.

Method Swizzling 大体都一个模子:

@implementation IDLViewController (IDLMethodSwizzling)

+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{

SEL originalSEL = @selector(viewWillAppear:);
SEL swizzlingSEL = @selector(idl_viewWillAppear:);

Class class = [self class];
Method originalMethod = class_getInstanceMethod(class, originalSEL);
Method swizzlingMethod = class_getInstanceMethod(class, swizzlingSEL);

// 如果要交换类方法可以使用下面代码替换上面的
// Class class = object_getClass((id)self);
// Method originalMethod = class_getClassMethod(class, originalSelector);
// Method swizzledMethod = class_getClassMethod(class, swizzledSelector);

BOOL didAddedSuccess = class_addMethod(class, originalSEL,
method_getImplementation(swizzlingMethod),
method_getTypeEncoding(swizzlingMethod));
if(didAddedSuccess) {
class_replaceMethod(class, swizzlingSEL,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzlingMethod);
}
});
}

- (void)idl_viewWillAppear:(BOOL)animated {
[self idl_viewWillAppear:animated];
NSLog(@"========>");
}

之所以放在load方法,主要有两点原因:

  1. load方法的时机在镜像加载的时候,这时候可以保证在任何逻辑开始之前就交换好了。
  2. load方法可以避免分类中的Method Swizzling 覆盖主类的Method Swizzling。

这里在最开始的时候会先试探得添加originalSEL对应的方法,如果我们类中还没有实现这个方法那么就会通过class_addMethod进行添加,这时候didAddedSuccess返回YES, 由于已经添加了originalSEL 指向swizzlingMethod的IMP,接下来就是通过class_replaceMethod,将swizzlingSEL指向originalMethod的IMP,从而完成方法的交换。
如果类中实现了originalSEL,那么didAddedSuccess返回NO。由于swizzlingSEL和swizzlingMethod的IMP也已经存在了,所以只要交换下这两个SEL的实现就可以了。

- (void)idl_viewWillAppear:(BOOL)animated {
[self idl_viewWillAppear:animated];
NSLog(@"========>");
}

看到上面代码,大家估计会觉得不对吧,实际上是这样的,经过Method Swizzling,一旦系统向UIViewController 发送 viewWillAppear:消息,就会执行****- (void)idl_viewWillAppear:(BOOL)animated****的IMP。也就是:

{
[self idl_viewWillAppear:animated];
NSLog(@"========>");
}

执行到****[self idl_viewWillAppear:animated]**** 就会执行原来的viewWillAppear: IMP实现。所以这个是没有问题的。

上面只是一个简单的例子,如果大家对Method Swizzling感兴趣,可以在GitHub上搜索 Method Swizzling。 其中jrswizzle,RSSwizzle大家可以了解下。实际项目中没有用过。有机会看下源码的实现再给大家分析下整个实现。无非就是兼容性好一点,但是实际项目中上面应该够用了。

4. 使用消息转发机制实现多继承,热修复,消息分发器,路由:

这部分大家可以看JSPatch,ASpect 这两个开源库。多继承见下一篇博客

5. 使用关联对象给Catogry添加属性
- (NSString *)catogiesName {
return objc_getAssociatedObject(self, _cmd);
}

- (void)setCatogiesName:(NSString *)catogiesName {
objc_setAssociatedObject(self, @selector(catogiesName), catogiesName, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

关于Runtime的API最主要的都集中在****<objc/runtime.h>,<objc/message.h>****这两个文件中大家可以在使用的时候进行查看。

如果大家想要更进一步学习Runtime 可以在Github搜索 Runtime