各位,你们怎么读emmmmm什么意思?

糗百君的飞船出了一点小毛病
糗百君的飞船出了一点小毛病……
莫慌, 点击 可以找到出路4月 15, 2018
前言iOS 最早名为 iPhone OS,是
公司专门为其硬件设备开发的操作系统,最初于 2007 年随第一代 iPhone 推出,后扩展为支持 Apple 公司旗下的其他硬件设备,如 iPod、iPad 等。
作为一名 iOS Developer,相信大多数人都有写出过造成 iOS 设备卡顿的代码经历,相应的也有过想方设法优化卡顿代码的经验。
本文将从 OpenGL 的角度结合 Apple 官方给出的部分资料,介绍 iOS Rendering Process 的概念及其整个底层渲染管道的各个流程。
相信在理解了 iOS Rendering Process 的底层各个阶段之后,我们可以在平日的开发工作之中写出性能更高的代码,在解决帧率不足的显示卡顿问题时也可以多一些思路~
iOS Rendering Process 概念
iOS Rendering 技术框架
OpenGL 主要渲染步骤
OpenGL Render Pipeline
Core Animation Pipeline
Commit Transaction
iOS Rendering Process 概念iOS Rendering Process 译为 iOS 渲染流程,本文特指 iOS 设备从设置将要显示的图元数据到最终在设备屏幕成像的整个过程。
在开始剖析 iOS Rendering Process 之前,我们需要对 iOS 的渲染概念有一个基本的认知:
基于平铺的渲染iOS 设备的屏幕分为 N * N 像素的图块,每个图块都适合于
缓存,几何体在图块内被大量拆分,只有在所有几何体全部提交之后才可以进行光栅化(Rasterization)。
Note: 这里的光栅化指将屏幕上面被大量拆分出来的几何体渲染为像素点的过程。
iOS Rendering 技术框架事实上 iOS 渲染相关的层级划分大概如下:
UIKit嘛~ 作为一名 iOS Developer 来说,应该对 UIKit 都不陌生,我们日常开发中使用的用户交互组件都来自于 UIKit Framework,我们通过设置 UIKit 组件的 Layout 以及 BackgroundColor 等属性来完成日常的界面绘画工作。
其实 UIKit Framework 自身并不具备在屏幕成像的能力,它主要负责对用户操作事件的响应,事件响应的传递大体是经过逐层的视图树遍历实现的。
那么我们日常写的 UIKit 组件为什么可以呈现在 iOS 设备的屏幕上呢?
Core AnimationCore Animation 其实是一个令人误解的命名。你可能认为它只是用来做动画的,但实际上它是从一个叫做 Layer Kit 这么一个不怎么和动画有关的名字演变而来的,所以做动画仅仅是 Core Animation 特性的冰山一角。
Core Animation 本质上可以理解为是一个复合引擎,旨在尽可能快的组合屏幕上不同的显示内容。这些显示内容被分解成独立的图层,即 CALayer,CALayer 才是你所能在屏幕上看见的一切的基础。
其实很多同学都应该知道 CALayer,UIKit 中需要在屏幕呈现的组件内部都有一个对应的 CALayer,也就是所谓的 Backing Layer。正是因为一一对应,所以 CALayer 也是树形结构的,我们称之为图层树。
视图的职责就是创建并管理这个图层,以确保当子视图在层级关系中添加或者被移除的时候,他们关联的图层也同样对应在层级关系树当中有相同的操作。
但是为什么 iOS 要基于 UIView 和 CALayer 提供两个平行的层级关系呢?为什么不用一个简单的层级关系来处理所有事情呢?
原因在于要做职责分离,这样也能避免很多重复代码。在 iOS 和 Mac OS X 两个平台上,事件和用户交互有很多地方的不同,基于多点触控的用户界面和基于鼠标键盘的交互有着本质的区别,这就是为什么 iOS 有 UIKit 和 UIView,而 Mac OS X 有 AppKit 和 NSView 的原因。他们功能上很相似,但是在实现上有着显著的区别。
Note: 实际上,这里并不是两个层级关系,而是四个,每一个都扮演不同的角色,除了视图树和图层树之外,还存在呈现树和渲染树。
OpenGL ES & Core GraphicsOpenGL ES 简称 GLES,即 OpenGL for Embedded Systems,是 OpenGL 的子集,通常面向图形硬件加速处理单元(GPU)渲染 2D 和 3D 计算机图形,例如视频游戏使用的计算机图形。
OpenGL ES 专为智能手机,平板电脑,视频游戏机和 PDA 等嵌入式系统而设计 。OpenGL ES 是“历史上应用最广泛的 3D 图形 API”。
Core Graphics Framework 基于 Quartz 高级绘图引擎。它提供了具有无与伦比的输出保真度的低级别轻量级 2D 渲染。您可以使用此框架来处理基于路径的绘图,转换,颜色管理,离屏渲染,图案,渐变和阴影,图像数据管理,图像创建和图像遮罩以及 PDF 文档创建,显示和分析。
Note: 在 Mac OS X 中,Core Graphics 还包括用于处理显示硬件,低级用户输入事件和窗口系统的服务。
Graphics Hardware 译为图形硬件,iOS 设备中也有自己的图形硬件设备,也就是我们经常提及的 GPU。
是一种专用电子电路,旨在快速操作和改变存储器,以加速在用于输出到显示设备的帧缓冲器中创建图像。GPU 被用于嵌入式系统,手机,个人电脑,工作站和游戏控制台。现代 GPU 在处理计算机图形和图像方面非常高效,并且 GPU 的高度并行结构使其在大块数据并行处理的算法中比通用 CPU 更有效。
OpenGL 主要渲染步骤 全称 Open Graphics Library,译为开放图形库,是用于渲染 2D 和 3D 矢量图形的跨语言,跨平台的应用程序编程接口(API)。OpenGL 可以直接访问 GPU,以实现硬件加速渲染。
一个用来渲染图像的 OpenGL 程序主要可以大致分为以下几个步骤:
设置图元数据
着色器-shader 计算图元数据(位置·颜色·其他)
光栅化-rasterization 渲染为像素
fragment shader,决定最终成像
其他操作(显示·隐藏·融合)
Note: 其实还有一些非必要的步骤,与本文主题不相关,这里点到为止。
我们日常开发时使用 UIKit 布局视图控件,设置透明度等等都属于设置图元数据这步,这也是我们日常开发中可以影响 OpenGL 渲染的主要步骤。
OpenGL Render Pipeline如果有同学看过 WWDC 的一些演讲稿或者接触过一些 OpenGL 知识,应该对 Render Pipeline 这个专业术语并不陌生。
不过 Render Pipeline 实在是一个初次见面不太容易理解的词,它译为渲染管道,也有译为渲染管线的…
其实 Render Pipeline 指的是从应用程序数据转换到最终渲染的图像之间的一系列数据处理过程。
好比我们上文中提到的 OpenGL 主要渲染步骤一样,我们开发应用程序时在设置图元数据这步为视图控件的设定布局,背景颜色,透明度以及阴影等等数据。
下面以 OpenGL 4.5 的 Render Pipeline 为例介绍一下:
这些图元数据流入 OpenGL 中,传入顶点着色器(vetex shader),然后顶点着色器对其进行着色器内部的处理后流出。之后可能进入细分着色阶段(tessellation shading stage),其中又有可能分为细分控制着色器和细分赋值着色器两部分处理,还可能会进入几何着色阶段(geometry shading stage),数据从中传递。最后都会走片元着色阶段(fragment shading stage)。
Note: 图元数据是以 copy 的形式流入 shader 的,shader 一般会以特殊的类似全局变量的形式接收数据。
OpenGL 在最终成像之前还会经历一个阶段名为计算着色阶段(compute shaing stage),这个阶段 OpenGL 会计算最重要在屏幕中成像的像素位置以及颜色,如果在之前提交代码时用到了 CALayer 会引起 blending 的显示效果(例如 Shadow)或者视图颜色或内容图片的 alpha 通道开启,都将会加大这个阶段 OpenGL 的工作量。
Core Animation Pipeline上文说到了 iOS 设备之所以可以成像不是因为 UIKit 而是因为 LayerKit,即 Core Animation。
Core Animation 图层,即 CALayer 中包含一个属性 contents,我们可以通过给这个属性赋值来控制 CALayer 成像的内容。这个属性的类型定义为 id,在程序编译时不论我们给 contents 赋予任何类型的值,都是可以编译通过的。但实践中,如果 contents 赋值类型不是 CGImage,那么你将会得到一个空白图层。
Note: 造成 contents 属性的奇怪表现的原因是 Mac OS X 的历史包袱,它之所以被定义为 id 类型是因为在 Mac OS X 中这个属性对 CGImage 和 NSImage 类型的值都起作用。但是在 iOS 中,如果你赋予一个 UIImage 属性的值,仅仅会得到一个空白图层。
说完 Core Animation 的 contents 属性,下面介绍一下 iOS 中 Core Animation Pipeline:
在 Application 中布局 UIKit 视图控件间接的关联 Core Animation 图层
Core Animation 图层相关的数据提交到 iOS Render Server,即 OpenGL ES & Core Graphics
Render Server 将与 GPU 通信把数据经过处理之后传递给 GPU
GPU 调用 iOS 当前设备渲染相关的图形设备 Display
Note: 由于 iOS 设备目前的显示屏最大支持 60 FPS 的刷新率,所以每个处理间隔为 16.67 ms。
可以看到从 Commit Transaction 之后我们的图元数据就将会在下一次 RunLoop 时被 Application 发送给底层的 Render Server,底层 Render Server 直接面向 GPU 经过一些列的数据处理将处理完毕的数据传递给 GPU,然后 GPU 负责渲染工作,根据当前 iOS 设备的屏幕计算图像像素位置以及像素 alpha 通道混色计算等等最终在当前 iOS 设备的显示屏中呈现图像。
嘛~ 由于 Core Animation Pipeline 中 Render Server 包含 OpenGL ES & Core Graphics,其中 OpenGL ES 的渲染可以参考上文 OpenGL Render Pipeline 理解。
Commit TransactionCore Animation Pipeline 的整个管线中 iOS 常规开发一般可以影响到的范围也就仅仅是在 Application 中布局 UIKit 视图控件间接的关联 Core Animation 图层这一级,即 Commit Transaction 之前的一些操作。
那么在 Commit Transaction 之前我们一般要做的事情有哪些?
Layout,构建视图
Display,绘制视图
Prepare,额外的 Core Animation 工作
Commit,打包图层并将它们发送到 Render Server
Layout在 Layout 阶段我们能做的是把 constraint 写的尽量高效,iOS 的 Layout Constraint 类似于 Android 的 Relative Layout。
Note: Emmmmm… 据观察 iOS 的 Layout Constraint 在书写时应该尽量少的依赖于视图树中同层级的兄弟视图节点,它会拖慢整个视图树的 Layout 计算过程。
这个阶段的 Layout 计算工作是在 CPU 完成的,包括 layoutSubviews 方法的重载,addSubview: 方法填充子视图等
Display其实这里的 Display 仅仅是我们设置 iOS 设备要最终成像的图元数据而已,重载视图 drawRect: 方法可以自定义 UIView 的显示,其原理是在 drawRect: 方法内部绘制 bitmap。
Note: 重载 drawRect: 方法绘制 bitmap 过程使用 CPU 和 内存。
所以重载 drawRect: 使用不当会造成 CPU 负载过重,App 内存飙升等问题。
Prepare这个步骤属于附加步骤,一般处理图像的解码 & 转换等操作。
CommitCommit 步骤指打包图层并将它们发送到 Render Server。
Note: Commit 操作会递归执行,由于图层和视图一样是以树形结构存在的,当图层树过于复杂时 Commit 操作的开销也会非常大。
CATransactionCATransaction 是 Core Animation 中用于将多个图层树操作分配到渲染树的原子更新中的机制,对图层树的每个修改都必须是事务的一部分。
CATransaction 类没有属性或者实例方法,并且也不能用 +alloc 和 -init 方法创建它,我们只能用类方法 +begin 和 +commit 分别来入栈或者出栈。
事实上任何可动画化的图层属性都会被添加到栈顶的事务,你可以通过 +setAnimationDuration: 方法设置当前事务的动画时间,或者通过 +animationDuration 方法来获取时长值(默认 0.25 秒)。
Core Animation 在每个 RunLoop 周期中自动开始一次新的事务,即使你不显式地使用 [CATransaction begin] 开始一次事务,在一个特定 RunLoop 循环中的任何属性的变化都会被收集起来,然后做一次 0.25 秒的动画(CALayer 隐式动画)。
Note: CATransaction 支持嵌套。
Animation对于 App 用户交互体验提升最明显的工作莫过于使用动画了,那么 iOS 是如何处理动画的渲染过程的呢?
日常开发中如果不是特别复杂的动画我们一般会使用 UIView Animation 实现,iOS 将 UIView Animation 的处理过程分为以下三个阶段:
调用 animateWithDuration:animations: 方法
在 Animation Block 中进行 Layout,Display,Prepare,Commit
Render Server 根据 Animation 逐帧渲染
Note: 原理是 animateWithDuration:animations: 内部使用了 CATransaction 来将整个 Animation Block 中的代码作为原子操作 commit 给了 RunLoop。
基于 CATransaction 实现链式动画事实上大多数的动画交互都是有动画执行顺序的,尽管 UIView Animation 很强大,但是在写一些顺序动画时使用 UIView Animation 只能在 + (void)animateWithDuration:delay:options:animations:completion: 方法的 completion block 中层级嵌套,写成一坨一坨 block 堆砌而成的代码,实在是难以阅读更别提后期维护了。
在得知 UIView Animation 使用了 CATransaction 时,我们不禁会想到这个 completion block 是不是也是基于 CATransaction 实现的呢?
Bingo!CATransaction 中有 +completionBlock 以及 +setCompletionBlock: 方法可以对应于 UIView Animation 的 completion block 的书写。
Note: 我的一个开源库
在动画顺序链接时也用到了 CATransaction。
全文总结结合上下文不难梳理出一个 iOS 最基本的完整渲染经过(Rendering pass)。
性能检测思路基于整篇文章的内容归纳一下我们在日常的开发工作中遇到性能问题时检测问题代码的思路:
Core Animation instrument
CPU or GPU
降低使用率节约能耗
Time Profiler instrument
不必要的 CPU 渲染
GPU 渲染更理想,但要清楚 CPU 渲染在何时有意义
Time Profiler instrument
过多的 offscreen passes
Core Animation instrument
过多的 blending
Core Animation instrument
奇怪的图片格式或大小
避免实时转换或调整大小
Core Animation instrument
开销昂贵的视图或特效
理解当前方案的开销成本
Xcode View Debugger
想象不到的层次结构
了解实际的视图层次结构
Xcode View Debugger
文章写得比较用心(是我个人的原创文章,转载请注明 ),如果发现错误会优先在我的个人博客中更新。如果有任何问题欢迎在我的微博
希望我的文章可以为你带来价值~
4月 5, 2018
前言Emmmmm… 这个清明节本来打算去青海湖骑行的,不过因为北京 4 月飞雪的天气骤变加上自己没注意添衣… 最终还是没能出了帝都 ╮(╯▽╰)╭
不过这也省下了一些开销~ 于是用本来准备出去踏青的 ? 入了一把垂涎已久的键盘,也就是本文的主角 —— HHKB 酱!
嘛~ 知道 HHKB 是在 2015 年,记得还是在某乎的一篇回答中看到的,答主是一个妹子,大意是这个妹子偷偷看到了程序猿男票的购物车中躺着一把 HHKB,然后这个对键盘一窍不通的妹子历尽波折将 HHKB 入手送给自己程序猿男票的暖心小故事。
当时的我刚刚毕业一年左右吧,也是正想买一把键盘来敲代码,提升自己的输出(事实证明,并没有太大帮助 ^_^||),见证自己的成长(这个打油的键帽们可以证明哟)。于是就搜到了这篇回答,看得我心里一暖,无比羡慕答主男票的同时自己默默的继续浏览其他关于键盘的东西…
最后在 Filco 和 HHKB 之间纠结挣扎了许久,选择了 Filco 87 青轴奶绿配色,不过想入一把 HHKB 的种子却埋在了心里…
有了大 F 的陪伴,HHKB 的艹长得依旧很快,每次在使用 ctrl maping command 键时心里的艹都会拔高一截,尤其是最近已经到了不能不拔的地步…
所以趁着这次机会,果断入手,写篇文章记录一下自己的开箱感受,说不定对某些同学有帮助呢~
Note: 前排提示,多图预警!
HHKBHHKB 全称 Happy Hacking Keyboard,于 1996 年 12 月 20 日诞生于日本,系
旗下子公司 PFU 生产的紧凑型键盘,以逼格甚高的键盘配列、优秀流畅的敲击手感、高昂的售价为外设发烧友和码农们所熟知,亦以被 Hackers 钟爱而闻名。
关于 HHKB 的评测贴很多,这里只挑一些我认为值得聊得点来写:
HHKB 的键盘配列为何如此设计?
什么是静电容轴?
谁捧红了 HHKB?
标志性配列HHKB 是由日本 Hacker
和 PFU 研究所共同设计,从名字就可以看出此系列键盘的用户定位是 Hacker,这块键盘的配列从第一代设计至今未变,可以说是 HHKB 的标志性配列。
键盘非 67 键的普通主键区小键盘传统配列,为了 Hackers 可以更好的在 Emacs 和 Vim 下使用,键盘将高频键位 —— Ctrl 上移至 Cap 处,然后砍掉了 Cap… 并默认把 Esc 键下沉至低频键位 ~ 处,同时还把 Del 键下沉至 | 键处,甚至砍掉了 67 键配列中大多数人认为高频使用的方向键!
静电容轴HHKB 既不是大多数人用的量产薄膜键盘,也不是近些年来逐渐火热的机械键盘,而是采用了
无接触式电容开关设计。
普通薄膜键盘的触发开关是在橡胶下黏一块导电薄膜,通过这个导电薄膜触发按键开关。
机械键盘在机械轴体内部加入铜片,按键时通过机械轴体轴心挤压铜片触发按键开关。
静电容键盘通过碗状橡胶触发开关,触发原理是在按键过程中电极间距改变产生电容值变化,进而触发键盘讯号。
Note: 很多同学对机械键盘有误解,其实机械键盘轴体内部也是有弹簧的,轴体回弹也是依靠这个弹簧实现。
钟爱 HHKB 的大神
,GUN Emacs & GCC 大佬。
,C艹 之父。
入手渠道我入手的是 HHKB Professional BT (Bluetooth) 版本,主要是看中了无线的优势,毕竟对于 Mac 来说,一个键盘占用一个 USB 接口太过奢侈了…
截止至下单时,日亚的售价是 29700 日元,约合人民币 1750 元。
某猫有一家认证过的 HHKB 官方旗舰店,售价 2388 元人民币,店铺限时活动 -200 元,加上满 200 即减 20 的满减活动,最终下单价为 2088 元。
考虑到行货保修问题,以及海淘邮寄可能出现的一些状况果断选择了国行。
开箱照嘛~ 快递没什么好说的,店家包顺丰次日达,顺丰的服务一如既往的让人满意。
清明小长假第一天就到了,开箱签收之后洗手拍照留念(笑)。
Note: 再次提示,多图预警!
嘛~ 吐槽一下,HHKB Pro BT 的这个电池仓略丑,好在宝宝敲键盘的时候看不到它…
手感体验买之前体验了同事的 HHKB Pro Type-S 和 HHKB Pro BT,对比发现手感上并无太大偏差,不过虽然 Type-S 缩短键程没有特别明显的手感体验差异,但是静音是真的差了一个梯度。BT 版本的声音并不吵,个人感觉应该比 Filco 红轴要略小,开放式办公应该问题不大。
不过讲真,HHKB 的手感并没有网上传的那么绵软流畅,质感也差了手边 Filco 青轴一段距离,也可能是我青轴用多了吧…
具体描述的话,HHKB 的手感略柔和,没有青轴按下按键触发开关时的清脆声,整体给我的感觉有点神似红轴,但是又不像红轴那样一触到底…
HHKB 和 Filco 都是信仰之物,上面的比较也是在较为苛刻的程度上我个人主观感受而已,毕竟这个价位的东西了,使用体验都会让人感到很舒服~
在蓝牙连接状态下敲代码,完全没有误敲,重复触发以及遗漏触发的情况,个人感觉还是十分满意哈~
键帽Emmmmm… 一开始并没有入 HHKB 的官方彩色键帽套装,理由是贵… 而且怕自己用不惯的话不方便退货。
不过在连续敲了两天键盘之后,感觉手感真的是玄学啊~ 慢慢的竟然有点喜欢 HHKB 这种静电容轴的手感了,尽管目前仍然认为不及我的大 F 青轴,但是并不排斥~
终于在网上看了很多搭配了 HHKB 官方彩色键帽的毒图之后,下单入手!
嘛~ 头图就是搭配了彩色键帽之后的 HHKB (^U^)ノ~YO
HHKB 键盘配列不太适合 Windows 用户,不过非常适合 Mac OS X 下使用,一但养成肌肉记忆将会非常方便。
HHKB 手感偏绵软,给我个人的感觉还算比较舒服,但是不及大 F 青轴的手感。
HHKB Pro BT 蓝牙连接使用体验极好,没有遗漏以及重复触发按键的情况出现。
彩色键帽真的是可以瞬间提升 HHKB 极具内敛的颜值。
最后放一下已经服役了将近 3 年的大 F 和新伙伴的合影。
3月 25, 2018
前言Emmmmm… Objective-C Class Properties 早在 WWDC 2016 中就已经公示,给 Objective-C 加入这个特性主要是为了与 Swift 类型属性相互操作。
官方是这么说明的:
Interoperate with Swift type properties.
嘛~ 虽然是为了配合 Swift 加入的新特性,不过聊胜于无哈!
Note: 值得一提的是 Objective-C Class Properties 语法特性虽然是 WWDC 2016 加入的,不过由于是 Xcode 8 中 LLVM Compiler 的特性,因此也适用于 iOS 10 之前的部署版本哟~
Objective-C Class Properties
对于 LLVM 的定义:
Note: The LLVM Project is a collection of modular and reusable compiler and toolchain technologies.
Emmmmm… 有趣的是,有的文章把 LLVM 强行展开为 “low level virtual machine” 译为 “低级别虚拟机”,不过在
可以看到官方明示 LLVM 与传统的虚拟机没有一毛钱关系,名称 “LLVM” 本身不是缩写,它仅仅是项目的名称而已~
嘛~ 可能有的同学不能理解为何 LLVM 是一个编译器工具链集合?这就要从 Apple 的编译器历史讲起咯~
很久很久以前… 算了,我感觉要跑题了(囧),这里简单列一下 Apple 采用过的编译方案吧:
LLVM & GCC
LLVM Compiler
GCC 是一套由 GNU 开发的编程语言编译器,最初作为
的编译器使用,后面发展成为类 Unix 操作系统以及 Apple Mac OS X 操作系统的标准编译器。
原本 GCC 仅能处理 C 语言的编译,不过 GCC 很快扩展以支持 C++,之后的 GCC 越发全面,支持 Objective-C,Fortran,Ada,以及 Go 语言。
值得一提的是 GCC 是一套以 GPL 以及 LGPL 许可证锁发行的 100% 自由软件,这意味着用户可以自由地运行,拷贝,分发,学习,修改并改进该软件。
LLVM & GCCLLVM 我们前面介绍过了,是模块化 & 可重用性编译器以及工具链技术集合。
LLVM 能够进行程序语言的 编译期优化、链接优化、在线编译优化、代码生成。
LLVM Compiler前面介绍过 GCC 支持很多语言,系统架构庞大而笨重,而 Apple 大量使用的 Objective-C 在 GCC 中顺位(优先级)较低。此外,GCC 作为一个纯粹的编译系统,在与 IDE 配合方面的表现也很差。
So,Apple 决定从零开始写 C,C++,Objective-C 的编译器 Clang。
至此,Apple 彻底与 GCC 了断。
Objective-C Class Properties
Objective-C Class Properties 作为 Objective-C 新语法特性在
中公示,表示 Xcode 8 之后可以使用这一新语法特性。
使用方式很简单:
Declared with class flag
Accessed with dot syntax
Never synthesized
Use @dynamic to defer to runtime
Declared with class flag123@interface MyType : NSObject@property (class) NSString *someS@end
Accessed with dot syntax1NSLog(@"format string: %@", MyType.someString);
Never synthesized12345@implementation MyTypestatic NSString *_someString = nil;+ (NSString *)someString { return _someS }+ (void)setSomeString:(NSString *)newString { _someString = newS }@end
123456@implementation MyType@dynamic (class) someS+ (BOOL)resolveClassMethod:(SEL) name {...}@end
解耦笔者在做项目组件下沉时,遇到一个问题,正好适用于 Objective-C Class Properties 发挥:将要下沉的组件库中某系统类 Categroy 引用了业务层某方法。
业务层应该依赖于将要下沉的组件,而组件既然要下沉就不应该再反过来依赖上层业务实现!
按照常规思路,想要把上层业务中被依赖的部分一起随组件下沉,但是发现被依赖的部分虽然也属于一个较为基础的模块,不过此模块现阶段不做下沉…
后来经过组内大佬指点,使用 Objective-C Class Properties 解决了这个问题,即将上层业务被依赖的部分化作将要下沉组件依赖方系统类 Categroy 的 Class Properties。
Note: 在 Categroy 中写 Objective-C Class Properties 需要使用 Runtime 关联方法。
介绍了 LLVM 顺便提到了 Apple 的编译系统发展简史。
使用官方 Demo 简单介绍了 Objective-C Class Properties 语法特性的书写方式。
提供了一种巧妙使用 Objective-C Class Properties 解耦的思路。
文章写得比较用心(是我个人的原创文章,转载请注明 ),如果发现错误会优先在我的
中更新。如果有任何问题欢迎在我的微博
希望我的文章可以为你带来价值~
3月 10, 2018
前言Emmmmm… 不知不觉已经一个多月没有提笔写文章了。
年前的一段时间确实比较忙,一方面想要尽可能的站好最后一班岗,把公司交给自己的事情做好;另一方面也在寻找新的机会,幸运的是在这一过程中得到了很多牛人的赏识和认可,获得了一些帮助以及内推机会,真的很感恩。
这次看机会都是在比较知名的项目或大厂中寻找的,前前后后忙碌了一个多月的时间,分别面试了头条,知乎,美团·点评,新浪,阿里巴巴这 5 家公司。
比较遗憾的是由于面试头条的时间点在 1 月 初,清楚记得元旦时期自己的三天假期基本都投入到了个人开源项目
的迭代中,准备不充分加上较长时间没有参加过正式面试导致最后没能通过,辜负了彬哥的内推(囧)。
值得庆幸的是后面几家公司都如愿拿到了 offer,尤其在拿到阿里巴巴口头 offer 之后的欣喜若狂仿佛就在昨天(笑),不过由于个人原因没能加入阿里巴巴,最后选择了美团·点评核心部门的 offer。
本来想趁着春招之际把自己年前的面试经历总结一下,写一篇面试攻略。转念一想自己的博客里面还是应该记录一些自己的想法,于是才有这一篇水文出来(笑)。
嘛~ 既然是水文,就不会有什么干货在后面粗线了。想了解上述公司面试流程以及面试题的同学可以就此打住,不用再往下看了,以免浪费时间哟~
迁徙回顾 2017 年的大事,应该就是从深圳迁徙到北京了吧… 从 14 年毕业之后就去了深圳,这一待就是 3 年,不知不觉已经对那座年轻的城市有了一些感情,一下子回到北方竟还有些舍不得。
还记得刚毕业的那段日子,住在公司提供的应届生宿舍里,与其他小伙伴吃在一起,住在一起。随后从公司宿舍搬出来,拿着仅够维持在深圳生活的工资,过着紧张而迷茫的日子。拿出自己一年多攒下的钱买了人生中第一台 Mac 和 iPhone,开始自学 iOS 开发并且第一次跳槽。
后面又经历了许许多多的第一次,也包括这次迁徙。
嘛~ 毕竟是北方长大的孩子,初来北京的感觉就是亲切,这里的建筑,植物,小吃… 一切的一切都有着久违的亲切感。
来北京的第一份工作是在皇城根儿旁边的胡同里,一如既往的创业公司(笑),貌似除了校招去的公司是比较大的上市公司之外,自从转了 iOS 开发之后的几次选择都选了创业公司…
当时清楚记得是 CTO 亲自面试,见面的第一印象就是很亲切和善,风度翩翩。聊了聊发现公司虽然是初创团队,但是每个人的素质和背景都很出众,尤其是研发团队的综合素质很高,我一个 211 垫底大学进去的时候不敢大声说话(笑)。
半年多的工作中,兢兢业业,生怕因为自己的失误拖了整个团队的后腿,好在一直没有出什么纰漏,上级对我的工作也比较满意,在里面与各位同事相处的也很愉快…
不过好事多磨,团队内部也发生过一些摩擦,带我一起去胡同里面小学打球的两个大哥先后离职,最后我也由于种种原因离开了…
虽然相处时间算不上长,但是在 ELSEWHERE 的这段时间成长很多,与大家相处的也很愉快,特别是研发部的小伙伴们,现在还一直有来往呢。也许,下次搬家会考虑在一起合租一套大房子呢~
这里也被我命名为 ELSEWHERE,旨在纪念这段来北京后的第一份工作,也顺便祝福 ELSEWHERE 能杀出重围,越走越远吧…
未来想自己毕业三年有余,却一事无成…
说出来自己都想笑,一直选择创业公司是因为想要改变世界…
自己当初在大学里拿到奖牌后吹过的牛逼还历历在目…
以前一直不懂为什么社会是个大染缸,现在身上已经被沾染上一些社会的颜色之后才明白这个比喻是多么的贴切而又无可奈何。时间也确实是一把利器,这才三年多的光景,就把梦想越磨越小,小到不见…
对自己说,也许现在我还没有创业的资本,等日后多些积累定要卷土重来…
对自己说,也许去大厂并非只是做一颗螺丝钉,毕竟用户量大,自己做的事情即使再小再微不足道,都是可以对用户端有些许影响的,从某种意义上讲也实现了改变人们的生活,改变了世界…
以后的事,谁又能说的准呢?只希望自己可以变得越来越强…
1月 17, 2018
前言 译为 “面向切面编程”,是通过预编译方式和运行期动态代理实现程序功能统一维护的一种技术。利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
Emmmmm…AOP 目前是较为热门的一个话题,尽管你也许没有听说过它,但是你的项目中可能已经渗入了它,例如:用户统计(不添加一行代码即实现对所有 ViewController 的跟踪日志)。
对于 iOS 开发者而言,无外乎 Swift 和 Objective-C 两种主流开发语言:
Swift 受限于 ABI 尚未稳定,动态性依赖 dynamic 修饰符,在 Runtime 没有留给我们太多的发挥空间(前几日新增了 swift-5.0-branch 分支,写这篇文章时看了一眼 181 commits behind master ?)。
Objective-C 在动态性上相对 Swift 具有无限大的优势,这几年 Objective-C Runtime 相关文章多如牛毛,相信现在的 iOSer 都具备一定的 Runtime 相关知识。
作为 Objective-C 语言编写的 AOP 库,适用于 iOS 和 Mac OS X,使用体验简单愉快,已经在 GitHub 摘得 5k+ Star。Aspects 内部实现比较健全,考虑到了 Hook 安全方面可能发生的种种问题,非常值得我们学习。
Note: 本文内引用 Aspects 源码版本为 v1.4.2,要求读者具备一定的 Runtime 知识。
Aspects 简介
Aspects 结构剖析
Aspects 核心代码剖析
优秀 AOP 库应该具备的特质
在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。
,即 “面向切面编程” 是一种编程范式,或者说是一种编程思想,它解决了
的延伸问题。
什么时候需要使用 AOP光是给个概念可能初次接触 AOP 的人还是无法 Get 到其中微秒,拿我们前言中举的例子?,假设随着我们所在的公司逐步发展,之前第三方的用户页面统计已经不能满足需求了,公司要求实现一个我们自己的用户页面统计。
嘛~ 我们来理一下 OOP 思想下该怎么办?
一个熟悉 OOP 思想的程序猿会理所应当的想到要把用户页面统计这一任务放到 ViewController 中;
考虑到一个个的手动添加统计代码要死人(而且还会漏,以后新增 ViewController 也要手动加),于是想到了 OOP 思想中的继承;
不巧由于项目久远,所有的 ViewController 都是直接继承自系统类 UIViewController(笑),此时选择抽一个项目 RootViewController,替换所有 ViewController 继承 RootViewController;
然后在 RootViewController 的 viewWillAppear: 和 viewWillDisappear: 方法加入时间统计代码,记录 ViewController 以及 Router 传参。
你会想,明明 OOP 也能解决问题是不是?不要急,再假设你们公司有多个 App,你被抽调至基础技术组专门给这些 App 写通用组件,要把之前实现过的用户页面统计重新以通用的形式实现,提供给你们公司所有的 App 使用。
MMP,使用标准 OOP 思想貌似无解啊…这个时候就是 AOP 的用武之地了。
这里简单给个思路:Hook UIViewController 的 viewWillAppear: 和 viewWillDisappear: 方法,在原方法执行之后记录需要统计的信息上报即可。
Note: 简单通过 Method Swizzling 来 Hook 不是不可以,但是有很多安全隐患!
Aspects 简介
是一个使用起来简单愉快的 AOP 库,使用 Objective-C 编写,适用于 iOS 与 Mac OS X。
Aspects 内部实现考虑到了很多 Hook 可能引发的问题,笔者在看源码的过程中抠的比较细,真的是受益匪浅。
Aspects 简单易用,作者通过在 NSObject (Aspects) 分类中暴露出的两个接口分别提供了对实例和 Class 的 Hook 实现:
12345678910111213@interface NSObject (Aspects)+ (id&AspectToken&)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)- (id&AspectToken&)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)@end
Aspects 支持实例 Hook,相较其他 Objective-C AOP 库而言可操作粒度更小,适合的场景更加多样化。作为使用者无需进行更多的操作即可 Hook 指定实例或者 Class 的指定 SEL,AspectOptions 参数可以指定 Hook 的点,以及是否执行一次之后就撤销 Hook。
Aspects 结构剖析
Emmmmm…尽管 Aspects 只有不到千行的源码,但是其内部实现考虑到了很多 Hook 相关的安全问题和其他细节,对比其他 Objective-C AOP 开源项目来说 Aspects 更为健全,所以我自己在扒 Aspects 源码时也看的比较仔细。
Aspects 内部结构Aspects 内部定义了两个协议:
AspectToken - 用于注销 Hook
AspectInfo - 嵌入 Hook 中的 Block 首位参数
此外 Aspects 内部还定义了 4 个类:
AspectInfo - 切面信息,遵循 AspectInfo 协议
AspectIdentifier - 切面 ID,应该遵循 AspectToken 协议(作者漏掉了,已提 PR)
AspectsContainer - 切面容器
AspectTracker - 切面跟踪器
以及一个结构体:
AspectBlockRef - 即 _AspectBlock,充当内部 Block
如果你扒一遍源码,还会发现两个内部静态全局变量:
static NSMutableDictionary *swizzledClassesD
static NSMutableSet *swizzledC
现在你也许还不能理解为什么要定义这么多东西,别急~ 我们后面都会分析到。
Aspects 协议按照上面列出的顺序,先来介绍一些 Aspects 声明的协议。
AspectTokenAspectToken 协议旨在让使用者可以灵活的注销之前添加过的 Hook,内部规定遵守此协议的对象须实现 remove 方法。
12345678@protocol AspectToken &NSObject&- (BOOL)@end
AspectInfo 协议旨在规范对一个切面,即 aspect 的 Hook 内部信息的纰漏,我们在 Hook 时添加切面的 Block 第一个参数就遵守此协议。
12345678910111213@protocol AspectInfo &NSObject&- (id)- (NSInvocation *)originalI- (NSArray *)@end
Note: 装箱是一个开销昂贵操作,所以用到再去执行。
Aspects 内部类接着协议,我们下面详细介绍一下 Aspects 的内部类。
Note: AspectInfo 在这里是一个 Class,其遵守上文中讲到的 AspectInfo 协议,不要混淆。
AspectInfo 类定义:
123456789@interface AspectInfo : NSObject &AspectInfo&- (id)initWithInstance:(__unsafe_unretained id)instance invocation:(NSInvocation *)@property (nonatomic, unsafe_unretained, readonly) id@property (nonatomic, strong, readonly) NSArray *@property (nonatomic, strong, readonly) NSInvocation *originalI@end
Note: 关于装箱,对于提供一个 NSInvocation 就可以拿到其 arguments 这一点上,ReactiveCocoa 团队提供了很大贡献(细节见 Aspects 内部 NSInvocation 分类)。
AspectInfo 比较简单,参考 ReactiveCocoa 团队提供的 NSInvocation 参数通用方法可将参数装箱为 NSValue,简单来说 AspectInfo 扮演了一个提供 Hook 信息的角色。
AspectIdentifierAspectIdentifier 类定义:
12345678910111213@interface AspectIdentifier : NSObject+ (instancetype)identifierWithSelector:(SEL)selector object:(id)object options:(AspectOptions)options block:(id)block error:(NSError **)- (BOOL)invokeWithInfo:(id&AspectInfo&)@property (nonatomic, assign) SEL@property (nonatomic, strong) id@property (nonatomic, strong) NSMethodSignature *blockS@property (nonatomic, weak) id@property (nonatomic, assign) AspectO@end
Note: AspectIdentifier 实际上是添加切面的 Block 的第一个参数,其应该遵循 AspectToken 协议,事实上也的确如此,其提供了 remove 方法的实现。
AspectIdentifier 内部需要注意的是由于使用 Block 来写 Hook 中我们加的料,这里生成了 blockSignature,在 AspectIdentifier 初始化的过程中会去判断 blockSignature 与入参 object 的 selector 得到的 methodSignature 的兼容性,兼容性判断成功才会顺利初始化。
AspectsContainerAspectsContainer 类定义:
1234567891011@interface AspectsContainer : NSObject- (void)addAspect:(AspectIdentifier *)aspect withOptions:(AspectOptions)injectP- (BOOL)removeAspect:(id)- (BOOL)hasA@property (atomic, copy) NSArray *beforeA@property (atomic, copy) NSArray *insteadA@property (atomic, copy) NSArray *afterA@end
AspectsContainer 作为切面的容器类,关联指定对象的指定方法,内部有三个切面队列,分别容纳关联指定对象的指定方法中相对应 AspectOption 的 Hook:
NSArray *beforeA - AspectPositionBefore
NSArray *insteadA - AspectPositionInstead
NSArray *afterA - AspectPositionAfter
为什么要说关联呢?因为 AspectsContainer 是在 NSObject 分类中通过 AssociatedObject 方法与当前要 Hook 的目标关联在一起的。
Note: 关联目标是 Hook 之后的 Selector,即 aliasSelector(原始 SEL 名称加 aspects_ 前缀对应的 SEL)。
AspectTrackerAspectTracker 类定义:
123456789@interface AspectTracker : NSObject- (id)initWithTrackedClass:(Class)trackedClass parent:(AspectTracker *)@property (nonatomic, strong) Class trackedC@property (nonatomic, strong) NSMutableSet *selectorN@property (nonatomic, weak) AspectTracker *parentE@end
AspectTracker 作为切面追踪器,原理大致如下:
12345678910111213currentClass =AspectTracker *parentTracker = nil;do {
AspectTracker *tracker = swizzledClassesDict[currentClass];
if (!tracker) {
tracker = [[AspectTracker alloc] initWithTrackedClass:currentClass parent:parentTracker];
swizzledClassesDict[(id&NSCopying&)currentClass] =
[tracker.selectorNames addObject:selectorName];
parentTracker =}while ((currentClass = class_getSuperclass(currentClass)));
Note: 聪明的你应该已经注意到了全局变量 swizzledClassesDict 中的 value 对应着 AspectTracker 指针。
嘛~ 就是说 AspectTracker 是从下而上追踪,最底层的 parentEntry 为 nil,父类的 parentEntry 为子类的 tracker。
Aspects 内部结构体AspectBlockRefAspectBlockRef,即 struct _AspectBlock,其定义如下:
1234567891011121314151617typedef struct _AspectBlock { __unused C AspectBlockF __unused int void (__unused *invoke)(struct _AspectBlock *block, ...); struct {
unsigned long int
unsigned long int
void (*copy)(void *dst, const void *src);
void (*dispose)(const void *);
const char *
const char * } * } *AspectBlockR
Emmmmm…没什么特别的,大家应该比较眼熟吧。
Note: __unused 宏定义实际上是 __attribute__((unused)) GCC 定语,旨在告诉编译器“如果我没有在后面使用到这个变量也别警告我”。
嘛~ 想起之前自己挖的坑还没有填,事实上自己也不知道什么时候填(笑):
之前挖坑说要写一篇文章记录一些阅读源码时发现的代码书写技巧
之前挖坑说要封装一个 WKWebView 给群里的兄弟参考
不要急~ 你瞧伦家不是都记得嘛(至于什么时候填坑嘛就…咳咳)
Aspects 静态全局变量static NSMutableDictionary *swizzledClassesDstatic NSMutableDictionary *swizzledClassesD 在 Aspects 中扮演着已混写类字典的角色,其内部结构应该是这样的:
1&Class : AspectTracker *&
Aspects 内部提供了专门访问这个全局字典的方法:
12345678static NSMutableDictionary *aspect_getSwizzledClassesDict() {
static NSMutableDictionary *swizzledClassesD
static dispatch_once_t
dispatch_once(&pred, ^{
swizzledClassesDict = [NSMutableDictionary new];
return swizzledClassesD}
这个全局变量可以简单理解为记录整个 Hook 影响的 Class 包含其 SuperClass 的追踪记录的全局字典。
static NSMutableSet *swizzledCstatic NSMutableSet *swizzledC 在 Aspects 中担当记录已混写类的角色,其内部结构如下:
1&NSStringFromClass(Class)&
Aspects 内部提供一个用于修改这个全局变量内容的方法:
12345678910static void _aspect_modifySwizzledClasses(void (^block)(NSMutableSet *swizzledClasses)) {
static NSMutableSet *swizzledC
static dispatch_once_t
dispatch_once(&pred, ^{
swizzledClasses = [NSMutableSet new];
@synchronized(swizzledClasses) {
block(swizzledClasses);
}}
Note: 注意 @synchronized(swizzledClasses)。
这个全局变量记录了 forwardInvocation: 被混写的的类名称。
Note: 注意在用途上与 static NSMutableDictionary *swizzledClassesD 区分理解。
Aspects 核心代码剖析
嘛~ Aspects 的整体实现代码不超过一千行,而且考虑的情况也比较全面,非常值得大家花时间去读一下,这里我只准备给出自己对其核心代码的理解。
Hook Class && Hook InstanceAspects 不光支持 Hook Class 还支持 Hook Instance,这提供了更小粒度的控制,配合 Hook 的撤销功能可以更加灵活精准的做我们想做的事~
Aspects 为了能区别 Class 和 Instance 的逻辑,实现了名为 aspect_hookClass 的方法,我认为其中的实现值得我用一部分篇幅来单独讲解,也觉得读者们有必要花点时间理解这里的实现逻辑。
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152static Class aspect_hookClass(NSObject *self, NSError **error) {
NSCParameterAssert(self);
Class statedClass = self.
Class baseClass = object_getClass(self);
NSString *className = NSStringFromClass(baseClass);
if ([className hasSuffix:AspectsSubclassSuffix]) {
return baseC
}else if (class_isMetaClass(baseClass)) {
return aspect_swizzleClassInPlace((Class)self);
}else if (statedClass != baseClass) {
return aspect_swizzleClassInPlace(baseClass);
const char *subclassName = [className stringByAppendingString:AspectsSubclassSuffix].UTF8S
Class subclass = objc_getClass(subclassName);
if (subclass == nil) {
subclass = objc_allocateClassPair(baseClass, subclassName, 0);
if (subclass == nil) {
NSString *errrorDesc = [NSString stringWithFormat:@"objc_allocateClassPair failed to allocate class %s.", subclassName];
AspectError(AspectErrorFailedToAllocateClassPair, errrorDesc);
return nil;
aspect_swizzleForwardInvocation(subclass);
aspect_hookedGetClass(subclass, statedClass);
aspect_hookedGetClass(object_getClass(subclass), statedClass);
objc_registerClassPair(subclass);
object_setClass(self, subclass);
return}
Note: 其实这里的难点就在于对 .class 和 object_getClass 的区分。
.class 当 target 是 Instance 则返回 Class,当 target 是 Class 则返回自身
object_getClass 返回 isa 指针的指向
Note: 动态创建一个 Class 的完整步骤也是我们应该注意的。
objc_allocateClassPair
class_addMethod
class_addIvar
objc_registerClassPair
嘛~ 难点和重点都讲完了,大家结合注释理解其中的逻辑应该没什么困难了,有什么问题可以找我一起交流~
Hook 的实现在上面 aspect_hookClass 方法中,不仅仅是返回一个要 Hook 的 Class,期间还做了一些细节操作,不论是 Class 还是 Instance,都会调用 aspect_swizzleForwardInvocation 方法,这个方法没什么难点,简单贴一下代码让大家有个印象:
123456789101112static void aspect_swizzleForwardInvocation(Class klass) {
NSCParameterAssert(klass);
IMP originalImplementation = class_replaceMethod(klass, @selector(forwardInvocation:), (IMP)__ASPECTS_ARE_BEING_CALLED__, "v@:@");
if (originalImplementation) {
class_addMethod(klass, NSSelectorFromString(AspectsForwardInvocationSelectorName), originalImplementation, "v@:@");
AspectLog(@"Aspects: %@ is now aspect aware.", NSStringFromClass(klass));}
上面的方法就是把要 Hook 的目标 Class 的 forwardInvocation: 混写了,混写之后 forwardInvocation: 的具体实现在 __ASPECTS_ARE_BEING_CALLED__ 中,里面能看到 invoke 标识位的不同是如何实现的,还有一些其他的实现细节:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869#define aspect_invoke(aspects, info) \for (AspectIdentifier *aspect in aspects) {\
[aspect invokeWithInfo:info];\
if (aspect.options & AspectOptionAutomaticRemoval) { \
aspectsToRemove = [aspectsToRemove?:@[] arrayByAddingObject:aspect]; \
} \}static void __ASPECTS_ARE_BEING_CALLED__(__unsafe_unretained NSObject *self, SEL selector, NSInvocation *invocation) {
NSCParameterAssert(self);
NSCParameterAssert(invocation);
SEL originalSelector = invocation.
SEL aliasSelector = aspect_aliasForSelector(invocation.selector);
invocation.selector = aliasS
AspectsContainer *objectContainer = objc_getAssociatedObject(self, aliasSelector);
AspectsContainer *classContainer = aspect_getContainerForClass(object_getClass(self), aliasSelector);
AspectInfo *info = [[AspectInfo alloc] initWithInstance:self invocation:invocation];
NSArray *aspectsToRemove = nil;
aspect_invoke(classContainer.beforeAspects, info);
aspect_invoke(objectContainer.beforeAspects, info);
BOOL respondsToAlias = YES;
if (objectContainer.insteadAspects.count || classContainer.insteadAspects.count) {
aspect_invoke(classContainer.insteadAspects, info);
aspect_invoke(objectContainer.insteadAspects, info);
}else {
Class klass = object_getClass(invocation.target);
if ((respondsToAlias = [klass instancesRespondToSelector:aliasSelector])) {
[invocation invoke];
}while (!respondsToAlias && (klass = class_getSuperclass(klass)));
aspect_invoke(classContainer.afterAspects, info);
aspect_invoke(objectContainer.afterAspects, info);
if (!respondsToAlias) {
invocation.selector = originalS
SEL originalForwardInvocationSEL = NSSelectorFromString(AspectsForwardInvocationSelectorName);
if ([self respondsToSelector:originalForwardInvocationSEL]) {
((void( *)(id, SEL, NSInvocation *))objc_msgSend)(self, originalForwardInvocationSEL, invocation);
}else {
[self doesNotRecognizeSelector:invocation.selector];
[aspectsToRemove makeObjectsPerformSelector:@selector(remove)];}#undef aspect_invoke
Note: aspect_invoke 宏定义的作用域。
代码实现对应了 Hook 的 AspectOptions 参数的 Before,Instead 和 After。
aspect_invoke 中 aspectsToRemove 是一个 NSArray,里面容纳着需要被销户的 Hook,即 AspectIdentifier(之后会调用 remove 移除)。
遍历 invocation.target 及其 superClass 找到实例可以响应 aliasSelector 的点 invoke 实现代码。
Block HookAspects 让我们在指定 Class 或 Instance 的特定 Selector 执行时,根据 AspectOptions 插入我们自己的 Block 做 Hook,而这个 Block 内部有我们想要的有关于当前 Target 和 Selector 的信息,我们来看一下 Aspects 是怎么办到的:
123456789101112131415161718192021222324252627282930313233343536373839404142- (BOOL)invokeWithInfo:(id&AspectInfo&)info {
NSInvocation *blockInvocation = [NSInvocation invocationWithMethodSignature:self.blockSignature];
NSInvocation *originalInvocation = info.originalI
NSUInteger numberOfArguments = self.blockSignature.numberOfA
if (numberOfArguments & originalInvocation.methodSignature.numberOfArguments) {
AspectLogError(@"Block has too many arguments. Not calling %@", info);
return NO;
if (numberOfArguments & 1) {
[blockInvocation setArgument:&info atIndex:1];
void *argBuf = NULL;
for (NSUInteger idx = 2; idx & numberOfA idx++) {
const char *type = [originalInvocation.methodSignature getArgumentTypeAtIndex:idx];
NSUInteger argS
NSGetSizeAndAlignment(type, &argSize, NULL);
if (!(argBuf = reallocf(argBuf, argSize))) {
AspectLogError(@"Failed to allocate memory for block invocation.");
return NO;
[originalInvocation getArgument:argBuf atIndex:idx];
[blockInvocation setArgument:argBuf atIndex:idx];
[blockInvocation invokeWithTarget:self.block];
if (argBuf != NULL) {
free(argBuf);
return YES;}
考虑两个问题:
[blockInvocation setArgument:&info atIndex:1]; 为什么要在索引 1 处插入呢?
for (NSUInteger idx = 2; idx & numberOfA idx++) 为什么要从索引 2 开始遍历参数呢?
嘛~ 如果你对 Block 的 Runtime 结构以及执行过程下断点研究一下就全都明白了,感兴趣的同学有疑问可以联系我(与真正勤奋好学的人交流又有谁会不乐意呢?笑~)
优秀 AOP 库应该具备的特质
良好的使用体验
可控粒度小
使用 Block 做 Hook
支持撤销 Hook
良好的使用体验Aspects 使用 NSObject + Categroy 的方式提供接口,非常巧妙的涵盖了 Instance 和 Class。
Aspects 提供的接口保持高度一致(本着易用,简单,方便的原则设计接口和整个框架的实现会让你的开源项目更容易被人们接纳和使用):
123456789+ (id&AspectToken&)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)- (id&AspectToken&)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)
Note: 其实接口这里对于 block 的参数自动补全可以更进一步,不过 Aspects 当初是没有办法做到的,单从接口设计这块已经很优秀了。
可控粒度小Aspects 不仅支持大部分 AOP 框架应该做到的对于 Class 的 Hook,还支持粒度更小的 Instance Hook,而其在内部实现中为了支持 Instance Hook 所做的代码也非常值得我们参考和学习(已在上文 Aspects 核心代码剖析 处单独分析)。
为使用者提供更为自由的 Hook 方式以达到更加精准的控制是每个使用者乐于见到的事。
使用 Block 做 HookAspects 使用 Block 来做 Hook 应该考虑到了很多东西,支持使用者通过在 Block 中获取到相关的信息,书写自己额外的操作就可以实现 Hook 需求。
支持撤销 HookAspects 还支持撤销之前做的 Hook 以及已混写的 Method,为了实现这个功能 Aspects 设计了全局容器,把 Hook 和混写用全局容器做记录,让一切都可以复原,这不正是我们想要的吗?
安全性嘛~ 我们在学习 Runtime 的时候,就应该看到过不少文章讲解 Method Swizzling 要注意的安全性问题,由于用到了大量 Runtime 方法,加上 AOP 是面向整个切面的,所以一单发现问题就会比较严重,涉及的面会比较广,而且难以调试。
Note: 我们不能因为容易造成问题就可以回避 Method Swizzling,就好比大学老师讲到递归时强调容易引起循环调用,很多人就在内心回避使用递归,甚至于非常适合使用递归来写的算法题(这里指递归来写会易读写、易维护)只会用复杂的方式来思考。
文章简单介绍了 AOP 的概念,希望能给各位读者对 AOP 思想的理解提供微薄的帮助。
文章系统的剖析了 Aspects 开源库的内部结构,希望能让大家在浏览 Aspects 源码时快速定位代码位置,找到核心内容。
文章重点分析了 Aspects 的核心代码,提炼了一些笔者认为值得注意的点,但愿可以在大家扒源码时提供一些指引。
文章结尾总结了 Aspects 作为一个比较优秀
文章写得比较用心(是我个人的原创文章,转载请注明 ),如果发现错误会优先在我的
中更新。如果有任何问题欢迎在我的微博
希望我的文章可以为你带来价值~
12月 23, 2017
Emmmmm…这篇文章发布出来可能正逢圣诞节?,Merry Christmas!
前言Web 页面中的 JS 与 iOS Native 如何交互是每个 iOS 猿必须掌握的技能。而 JS 和 iOS Native 就好比两块没有交集的大陆,如果想要使它们相互通信就必须要建立一座“桥梁”。
思考一下,如果项目组让你去造这座“桥”,如何才能做到既优雅又实用?
本文将结合 WebViewJavascriptBridge 源码逐步带大家找到答案。
是盛名已久的 JSBridge 库,早在 2011 年就被作者
发布到 GitHub,直到现在作者还在积极维护中,目前该项目已收获近 1w star 咯,其源码非常值得我们学习。
WebViewJavascriptBridge 的代码逻辑清晰,风格良好,加上自身代码量比较小使得其源码阅读非常轻松(可能需要一些 JS 基础)。更加难能可贵的是它仅使用了少量代码就实现了对于 Mac OS X 的 WebView 以及 iOS 平台的 UIWebView 和 WKWebView 三种组件的完美支持。
我对 WebViewJavascriptBridge 的评价是小而美,这类小而美的源码非常利于我们对其实现思想的学习(本文分析 WebViewJavascriptBridge 源码版本为 v6.0.3)。
关于 iOS 与 JS 的原生交互知识,之前我有写过一篇文章,文章除了介绍 JavaScriptCore 库以及 UIWebView 和 WKWebView 与 JS 原生交互的方法之外还捎带提到了
的发展简史,文末还提供了一个 。
所以这篇文章不会再把重点放在 iOS 与 JS 的原生交互了,本文旨在介绍
的设计思路和实现原理,对 iOS 与 JS 原生交互知识感兴趣的同学推荐去阅读上面提到的文章,应该会有点儿帮助(笑)。
WebViewJavascriptBridge 简介
WebViewJavascriptBridge && WKWebViewJavascriptBridge 探究
WebViewJavascriptBridgeBase - JS 调用 Native 实现原理剖析
WebViewJavascriptBridge_JS - Native 调用 JS 实现解读
WebViewJavascriptBridge 的“桥梁美学”
WebViewJavascriptBridge 简介
WebViewJavascriptBridge 是用于在 WKWebView,UIWebView 和 WebView 中的 Obj-C 和 JavaScript 之间发送消息的 iOS / OSX 桥接器。
有许多不错的项目都有使用 WebViewJavascriptBridge,这里简单列一部分(笑):
… & many more!
关于 WebViewJavascriptBridge 的具体使用方法详见其 。
在读完 WebViewJavascriptBridge 的源码之后我将其划分为三个层级:
WebViewJavascriptBridge && WKWebViewJavascriptBridge
WebViewJavascriptBridgeBase
WebViewJavascriptBridge_JS
其中 WebViewJavascriptBridge && WKWebViewJavascriptBridge 作为接口层主要负责提供方便的接口,隐藏实现细节,其实现细节都是通过实现层 WebViewJavascriptBridgeBase 去做的,而 WebViewJavascriptBridge_JS 作为 JS 层其实存储了一段 JS 代码,在需要的时候注入到当前 WebView 组件中,最终实现 Native 与 JS 的交互。
WebViewJavascriptBridge && WKWebViewJavascriptBridge 探究
WebViewJavascriptBridge 和 WKWebViewJavascriptBridge 作为接口层分别对应于 UIWebView 和 WKWebView 组件,我们来简单看一下这两个文件暴露出的信息:
WebViewJavascriptBridge 暴露信息:
123456789101112131415@interface WebViewJavascriptBridge : WVJB_WEBVIEW_DELEGATE_INTERFACE+ (instancetype)bridgeForWebView:(id)webV + (instancetype)bridge:(id)webV + (void)enableL + (void)setLogMaxLength:(int) - (void)registerHandler:(NSString*)handlerName handler:(WVJBHandler) - (void)removeHandler:(NSString*)handlerN - (void)callHandler:(NSString*)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseC - (void)setWebViewDelegate:(id)webViewD - (void)disableJavscriptAlertBoxSafetyT @end
WKWebViewJavascriptBridge 暴露信息:
1234567891011121314@interface WKWebViewJavascriptBridge : NSObject&WKNavigationDelegate, WebViewJavascriptBridgeBaseDelegate&+ (instancetype)bridgeForWebView:(WKWebView*)webV+ (void)enableL- (void)registerHandler:(NSString*)handlerName handler:(WVJBHandler)- (void)removeHandler:(NSString*)handlerN- (void)callHandler:(NSString*)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseC- (void)- (void)setWebViewDelegate:(id)webViewD- (void)disableJavscriptAlertBoxSafetyT@end
Note: disableJavscriptAlertBoxSafetyTimeout 方法是通过禁用 JS 端 AlertBox 的安全时长来加速网桥消息传递的。如果想使用那么需要和前端约定好,如果禁用之后前端 JS 代码仍有调用 AlertBox 相关代码(alert, confirm, 或 prompt)则程序将被挂起,所以这个方法是不安全的,如无特殊需求笔者不推荐使用。
可以看得出来这两个文件暴露出的接口几乎一致,其中 WebViewJavascriptBridge 中使用了宏定义 WVJB_WEBVIEW_DELEGATE_INTERFACE 来分别适配 iOS 和 Mac OS X 平台的 UIWebView 和 WebView 组件需要实现的代理方法。
WebViewJavascriptBridge 中的宏定义其实 WebViewJavascriptBridge 中为了适配 iOS 和 Mac OS X 平台的 UIWebView 和 WebView 组件使用了一系列的宏定义,其源码比较简单:
123456789101112#if defined __MAC_OS_X_VERSION_MAX_ALLOWED
#define WVJB_PLATFORM_OSX
#define WVJB_WEBVIEW_TYPE WebView
#define WVJB_WEBVIEW_DELEGATE_TYPE NSObject&WebViewJavascriptBridgeBaseDelegate&
#define WVJB_WEBVIEW_DELEGATE_INTERFACE NSObject&WebViewJavascriptBridgeBaseDelegate, WebPolicyDelegate&#elif defined __IPHONE_OS_VERSION_MAX_ALLOWED
#import &UIKit/UIWebView.h&
#define WVJB_PLATFORM_IOS
#define WVJB_WEBVIEW_TYPE UIWebView
#define WVJB_WEBVIEW_DELEGATE_TYPE NSObject&UIWebViewDelegate&
#define WVJB_WEBVIEW_DELEGATE_INTERFACE NSObject&UIWebViewDelegate, WebViewJavascriptBridgeBaseDelegate&#endif
分别根据所在平台不同定义了 WVJB_WEBVIEW_TYPE,WVJB_WEBVIEW_DELEGATE_TYPE 以及刚才提到的 WVJB_WEBVIEW_DELEGATE_INTERFACE 宏定义,并且分别定义了 WVJB_PLATFORM_OSX 和 WVJB_PLATFORM_IOS 便于之后的实现源码区分当前平台时使用,下面的 supportsWKWebView 宏定义也是同样的道理:
123#if (__MAC_OS_X_VERSION_MAX_ALLOWED & __MAC_10_9 || __IPHONE_OS_VERSION_MAX_ALLOWED &= __IPHONE_7_1)#define supportsWKWebView#endif
在引入头文件的时候可以通过这个 supportsWKWebView 宏灵活引入所需的头文件:
123456789#if defined supportsWKWebView#import &WebKit/WebKit.h&#endif#if defined(supportsWKWebView)#import "WKWebViewJavascriptBridge.h"#endif
WebViewJavascriptBridge 的实现分析我们接着看一下 WebViewJavascriptBridge 的实现部分,首先从内部变量信息看起:
123456789101112#if __has_feature(objc_arc_weak)
#define WVJB_WEAK __weak#else
#define WVJB_WEAK __unsafe_unretained#endif@implementation WebViewJavascriptBridge {
WVJB_WEAK WVJB_WEBVIEW_TYPE* _webV
WVJB_WEAK id _webViewD
long _uniqueId;
WebViewJavascriptBridgeBase *_ }
上文提到 WebViewJavascriptBridge 和 WKWebViewJavascriptBridge 的 .h 文件暴露接口信息非常相似,那么我们要不要看看 WKWebViewJavascriptBridge 的内部变量信息呢?
1234567@implementation WKWebViewJavascriptBridge {
__weak WKWebView* _webV
__weak id&WKNavigationDelegate& _webViewD
long _uniqueId;
WebViewJavascriptBridgeBase *_}
嘛~ 这俩货简直是一个妈生的。其实这是作者故意为之,因为作者想对外提供一套接口,即 WebViewJavascriptBridge,我们只需要使用 WebViewJavascriptBridge 就可以自动根据绑定的 WebView 组件的不同生成与之对应的 JSBridge 实例。
123456789101112131415161718192021+ (instancetype)bridge:(id)webView {#if defined supportsWKWebView
if ([webView isKindOfClass:[WKWebView class]]) {
return (WebViewJavascriptBridge*) [WKWebViewJavascriptBridge bridgeForWebView:webView];
}#endif
if ([webView isKindOfClass:[WVJB_WEBVIEW_TYPE class]]) {
WebViewJavascriptBridge* bridge = [[self alloc] init];
[bridge _platformSpecificSetup:webView];
[NSException raise:@"BadWebViewType" format:@"Unknown web view type."];
return nil;}
我们可以看到上面的代码,实现并不复杂。如果支持 WKWebView 的话(#if defined supportsWKWebView)则去判断当前绑定的 WebView 组件是否从属于 WKWebView,这样可以返回 WKWebViewJavascriptBridge 实例,否则返回 WebViewJavascriptBridge 实例,最后如果入参 webView 的类型不满足判断条件则抛出 BadWebViewType 异常。
还有一个关于 _webViewDelegate 的小细节,本来不打算讲的,但是还是提一下吧(囧)。其实在 WebViewJavascriptBridge 以及 WKWebViewJavascriptBridge 的初始化实现过程中,会把当前 WebView 组件的代理绑定为自己:
123456789101112131415- (void) _platformSpecificSetup:(WVJB_WEBVIEW_TYPE*)webView {
_webView = webV
_webView.delegate = self;
_base = [[WebViewJavascriptBridgeBase alloc] init];
_base.delegate = self;}- (void) _setupInstance:(WKWebView*)webView {
_webView = webV
_webView.navigationDelegate = self;
_base = [[WebViewJavascriptBridgeBase alloc] init];
_base.delegate = self;}
Note: 替换组件的代理将其代理绑定为 bridge 自己是因为 WebViewJavascriptBridge 的实现原理上是利用我之前的文章中讲过的假 Request 方法实现的,所以需要监听 WebView 组件的代理方法获取加载之前的 Request.URL 并做处理。这也是为什么 WebViewJavascriptBridge 提供了一个接口 setWebViewDelegate: 存储了一个逻辑上的 _webViewDelegate,这个 _webViewDelegate 也需要遵循 WebView 组件的代理协议,这样在 WebViewJavascriptBridge 内部不同的代理方法中做完 bridge 要做的事情只有就会再去调用 _webViewDelegate 对应的代理方法,其实可以理解为 WebViewJavascriptBridge 对当前 WebView 组件的代理做了 hook。
对于 WebViewJavascriptBridge 中暴露的初始化以外的所有接口,其内部实现都是通过 WebViewJavascriptBridgeBase 来实现的。这样做的好处就是即使 WebViewJavascriptBridge 因为绑定了 WKWebView 返回了 WKWebViewJavascriptBridge 实例,只要接口一致,对 JSBridge 发送相同的消息,就会有相同的实现(都是由 WebViewJavascriptBridgeBase 类实现的)。
WebViewJavascriptBridgeBase - JS 调用 Native 实现原理剖析
作为 WebViewJavascriptBridge 的实现层,WebViewJavascriptBridgeBase 的命名也可以体现出其是作为整座“桥梁”桥墩一般的存在,我们还是按照老规矩先看一下 WebViewJavascriptBridgeBase.h 暴露的信息,好对其有一个整体的印象:
12345678910111213141516171819202122232425262728293031typedef void (^WVJBResponseCallback)(id responseData); typedef void (^WVJBHandler)(id data, WVJBResponseCallback responseCallback); typedef NSDictionary WVJBM @protocol WebViewJavascriptBridgeBaseDelegate &NSObject&- (NSString*) _evaluateJavascript:(NSString*)javascriptC@end@interface WebViewJavascriptBridgeBase : NSObject@property (weak, nonatomic) id &WebViewJavascriptBridgeBaseDelegate& @property (strong, nonatomic) NSMutableArray* startupMessageQ @property (strong, nonatomic) NSMutableDictionary* responseC @property (strong, nonatomic) NSMutableDictionary* messageH @property (strong, nonatomic) WVJBHandler messageH + (void)enableL + (void)setLogMaxLength:(int) - (void) - (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerN - (void)flushMessageQueue:(NSString *)messageQueueS - (void)injectJavascriptF - (BOOL)isWebViewJavascriptBridgeURL:(NSURL*) - (BOOL)isQueueMessageURL:(NSURL*) - (BOOL)isBridgeLoadedURL:(NSURL*) - (void)logUnkownMessage:(NSURL*) - (NSString *)webViewJavascriptCheckC - (NSString *)webViewJavascriptFetchQueyC - (void)disableJavscriptAlertBoxSafetyT @end
嘛~ 从 .h 文件中我们可以看到整个 WebViewJavascriptBridgeBase 所暴露出来的信息,属性层面上需要对以下 4 个属性加深印象,之后分析实现的过程中会带入这些属性:
id &WebViewJavascriptBridgeBaseDelegate& delegate 代理,可以通过代理让当前 bridge 绑定的 WebView 组件执行 JS 代码
NSMutableArray* startupMessageQ 启动消息队列,存放 Obj-C 发送给 JS 的消息(可以理解为存放 WVJBMessage 类型)
NSMutableDictionary* responseC 回调 blocks 字典,存放 WVJBResponseCallback 类型的 block
NSMutableDictionary* messageH Obj-C 端已注册的 handlers 字典,存放 WVJBHandler 类型的 block
Emmmmm…接口层面看一下注释就好了,后面分析实现的时候会捎带讲解一些接口,剩下一些跟实现无关的接口内容感兴趣的同学推荐自己扒源码哈。
我们在对 WebViewJavascriptBridgeBase 整体有了一个初始印象之后就可以自己写一个页面,简单的嵌入一些 JS 跑一遍流程,在中间下断点扒源码,这样我们对于 Native 与 JS 的交互流程就可以一清二楚了。
下面模拟一遍 JS 通过 WebViewJavascriptBridge 调用 Native 功能的流程分析 WebViewJavascriptBridgeBase 的相关实现(考虑现在的时间点决定以 WKWebView 为例讲解,即针对 WKWebViewJavascriptBridge 源码讲解):
1.监听假 Request 并注入 WebViewJavascriptBridge_JS 内的 JS 代码上文说到 WebViewJavascriptBridge 的实现其实本质上是利用了我之前的文章中讲过的假 Request 方法实现的,那么我们就从监听假 Request 开始讲起吧。
123456789101112131415161718192021222324252627282930- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
if (webView != _webView) { return; }
NSURL *url = navigationAction.request.URL;
__strong typeof(_webViewDelegate) strongDelegate = _webViewD
if ([_base isWebViewJavascriptBridgeURL:url]) {
if ([_base isBridgeLoadedURL:url]) {
[_base injectJavascriptFile];
} else if ([_base isQueueMessageURL:url]) {
[self WKFlushMessageQueue];
} else {
[_base logUnkownMessage:url];
decisionHandler(WKNavigationActionPolicyCancel);
if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:decidePolicyForNavigationAction:decisionHandler:)]) {
[_webViewDelegate webView:webView decidePolicyForNavigationAction:navigationAction decisionHandler:decisionHandler];
} else {
decisionHandler(WKNavigationActionPolicyAllow);
}}
Note: 之前说过 WebViewJavascriptBridge 会 hook 绑定的 WebView 的代理方法,这一点 WKWebViewJavascriptBridge 也一样,在加入自己的代码之后会判断是否有 _webViewDelegate 响应这个代理方法,如果有则调用。
我们还是把注意力放到注释中核心代码的位置,里面会先判断当前 url 是否为 bridge url:
12345#define kOldProtocolScheme @"wvjbscheme"#define kNewProtocolScheme @"https"#define kQueueHasMessage
@"__wvjb_queue_message__"#define kBridgeLoaded
@"__bridge_loaded__"
的使用方法中第 4 步明确指出要复制粘贴 setupWebViewJavascriptBridge 方法到前段 JS 中,我们先来看一下这段 JS 方法源码:
1234567891011121314function setupWebViewJavascriptBridge(callback) { if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); } if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); } window.WVJBCallbacks = [callback];
var WVJBIframe = document.createElement('iframe');
WVJBIframe.style.display = 'none';
WVJBIframe.src = 'https://__bridge_loaded__';
document.documentElement.appendChild(WVJBIframe); setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)}
上面的代码创建了一个不显示的 iframe 并将其 src 置为 https://__bridge_loaded__,与上文中 kBridgeLoaded 宏定义一致,即用于 isBridgeLoadedURL: 方法中判定当前 url 是否为 BridgeLoadedURL。
Note: 假 Request 的发起有两种方式,-1:location.href -2:iframe。通过 location.href 有个问题,就是如果 JS 多次调用原生的方法也就是 location.href 的值多次变化,Native 端只能接受到最后一次请求,前面的请求会被忽略掉,所以这里 WebViewJavascriptBridge 选择使用 iframe,后面不再解释。
因为加入了 src 为 https://__bridge_loaded__ 的 iframe 元素,我们上面截获 url 的代理方法就会拿到一个 https://__bridge_loaded__ 的 url,由于 https 满足判定 WebViewJavascriptBridgeURL,将会进入核心代码区域接着会被判定为 BridgeLoadedURL 执行注入 JS 代码的方法,即 [_base injectJavascriptFile];。
1234567891011121314- (void)injectJavascriptFile {
NSString *js = WebViewJavascriptBridge_js();
[self _evaluateJavascript:js];
if (self.startupMessageQueue) {
NSArray* queue = self.startupMessageQ
self.startupMessageQueue = nil;
for (id queuedMessage in queue) {
[self _dispatchMessage:queuedMessage];
}}
至此,第一步交互已完成。关于 WebViewJavascriptBridge_JS 内部的 JS 代码我们放到后面的章节解读,现在可以简单理解为 WebViewJavascriptBridge 在 JS 端的具体实现代码。
2.JS 端调用 callHandler 方法之后 Native 端究竟是如何响应的? 中指出 JS 端的操作方式:
123456789101112setupWebViewJavascriptBridge(function(bridge) {
bridge.registerHandler('JS Echo', function(data, responseCallback) {
console.log("JS Echo called with:", data)
responseCallback(data) }) bridge.callHandler('ObjC Echo', {'key':'value'}, function responseCallback(responseData) {
console.log("JS received response:", responseData) })})
我们知道 JS 端调用 setupWebViewJavascriptBridge 方法会走我们刚才分析过的第一步,即监听假 Request 并注入 WebViewJavascriptBridge_JS 内的 JS 代码。那么当 JS 端调用 bridge.callHandler 时,Native 端究竟是如何做出响应的呢?这里我们需要先稍微解读一下之前注入的 WebViewJavascriptBridge_JS 中的 JS 代码:
123456789101112131415161718192021222324function callHandler(handlerName, data, responseCallback) { if (arguments.length == 2 && typeof data == 'function') {
responseCallback =
data = null; } _doSend({ handlerName:handlerName, data:data }, responseCallback);}function _doSend(message, responseCallback) { if (responseCallback) {
var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
responseCallbacks[callbackId] = responseC
message['callbackId'] = callbackId; } sendMessageQueue.push(message); messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;} var CUSTOM_PROTOCOL_SCHEME = 'https';var QUEUE_HAS_MESSAGE = '__wvjb_queue_message__';
可以看到 JS 端的代码中有 callHandler 函数的实现,其内部将入参 handlerName 以及 data 以字典形式作为参数调用 _doSend 方法,我们看一下 _doSend 方法的实现:
_doSend 方法内部会先判断入参中是否有回调
如果有回调则根据规则生成 callbackId 并且将回调 block 保存到 responseCallbacks 字典(囧~ JS 不叫字典的,我是为了 iOS 读者看着方便),之后给消息也加入一个键值对保存刚才生成的 callbackId
之后给 sendMessageQueue 队列加入 message
将 messagingIframe.src 设置为 https://__wvjb_queue_message__
好,点到为止,对于 WebViewJavascriptBridge_JS 内的 JS 端其他源码我们放着后面看。注意这里加入了一个 src 为 https://__wvjb_queue_message__ 的 messagingIframe,它也是一个不可见的 iframe。这样 Native 端会收到一个 url 为 https://__wvjb_queue_message__ 的 request,回到第 1 步中获取到假的 request 之后会进行各项判定,这次会满足 [_base isQueueMessageURL:url] 的判定调用 Native 的 WKFlushMessageQueue 方法。
1234567891011121314- (void)WKFlushMessageQueue {
[_webView evaluateJavaScript:[_base webViewJavascriptFetchQueyCommand] completionHandler:^(NSString* result, NSError* error) {
if (error != nil) {
NSLog(@"WebViewJavascriptBridge: WARNING: Error when trying to fetch data from WKWebView: %@", error);
[_base flushMessageQueue:result];
}];}- (NSString *)webViewJavascriptFetchQueyCommand {
return @"WebViewJavascriptBridge._fetchQueue();";}
可见 Native 端会在刷新队列中调用 JS 端的 WebViewJavascriptBridge._fetchQueue(); 方法,我们来看一下 JS 端此方法的具体实现:
123456789function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
return messageQueueS}
这个方法会把当前 JS 端 sendMessageQueue 消息队列以 JSON 的形式返回,而 Native 端会调用 [_base flushMessageQueue:result]; 将拿到的 JSON 形式消息队列作为参数调用 flushMessageQueue: 方法,这个方法是整个框架 Native 端的精华所在,就是稍微有点长(笑)。
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657- (void)flushMessageQueue:(NSString *)messageQueueString {
if (messageQueueString == nil || messageQueueString.length == 0) {
NSLog(@"WebViewJavascriptBridge: WARNING: ObjC got nil while fetching the message queue JSON from webview. This can happen if the WebViewJavascriptBridge JS is not currently present in the webview, e.g if the webview just loaded a new page.");
id messages = [self _deserializeMessageJSON:messageQueueString];
for (WVJBMessage* message in messages) {
if (![message isKindOfClass:[WVJBMessage class]]) {
NSLog(@"WebViewJavascriptBridge: WARNING: Invalid %@ received: %@", [message class], message);
[self _log:@"RCVD" json:message];
NSString* responseId = message[@"responseId"];
if (responseId) {
WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
responseCallback(message[@"responseData"]);
[self.responseCallbacks removeObjectForKey:responseId];
} else {
WVJBResponseCallback responseCallback = NULL;
NSString* callbackId = message[@"callbackId"];
if (callbackId) {
responseCallback = ^(id responseData) {
if (responseData == nil) {
responseData = [NSNull null];
WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
[self _queueMessage:msg];
} else {
responseCallback = ^(id ignoreResponseData) {
WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
if (!handler) {
NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
handler(message[@"data"], responseCallback);
}}
嘛~ flushMessageQueue: 方法作为整个 Native 端的核心,有点长是可以理解的。我们简单理一下它的实现思路:
将 JSON 形式的入参转换为 Native 对象,即消息队列,这里面消息类型是之前定义过的 WVJBMessage,即字典
如果消息中含有 “responseId” 则表明是之前 Native 调用的 JS 方法回调过来的消息(因为 JS 端和 Native 端实现逻辑是对等的,所以这个地方不明白的可以参考下面的分析)
如果消息中不含 “responseId” 则表明是 JS 端通过 callHandler 函数正常调用 Native 端过来的消息
尝试获取消息中的 “callbackId”,如果 JS 本次消息需要 Native 响应之后回调才会有这个键值,具体参见上文中 JS 端 _doSend 部分源码分析。如取到 “callbackId” 则需生成一个回调 block,回调 block 内部将 “callbackId” 作为 msg 的 “responseId” 执行 _queueMessage 将消息发送给 JS 端(JS 端处理消息逻辑与 Native 端一致,所以上面使用 “responseId” 判断当前消息是否为回调方法传递过来的消息是很容易理解的)
尝试以消息中的 “handlerName” 从 messageHandlers(上文提到过,是保存 Native 端注册过的 handler 的字典)取到对应的 handler block,如果取到则执行代码块,否则打印错误日志
Note: 这个消息处理的方法虽然长,但是逻辑清晰,而且有效的解决了 JS 与 Native 相互调用的过程中参数传递的问题(包括回调),此外 JS 端的消息处理逻辑与 Native 端保持一致,实现了逻辑对称,非常值得我们学习。
WebViewJavascriptBridge_JS - Native 调用 JS 实现解读
Emmmmm…这一章节主要讲 JS 端注入的代码,即 WebViewJavascriptBridge_JS 中的 JS 源码。由于我没做过前段,能力不足,水平有限,可能有谬误希望各位读者发现的话及时指正,感激不尽。预警,由于 JS 端和上文分析过的 Native 端逻辑对称且上文已经分析过部分 JS 端的函数,所以下面的 JS 源码没有另做拆分,为避免被大段 JS 代码糊脸不感兴趣的同学可以直接看代码后面的总结。
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146;(function() {
if (window.WebViewJavascriptBridge) {
return; }
if (!window.onerror) {
window.onerror = function(msg, url, line) {
console.log("WebViewJavascriptBridge: ERROR:" + msg + "@" + url + ":" + line);
} }
window.WebViewJavascriptBridge = {
registerHandler: registerHandler,
callHandler: callHandler,
disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
_fetchQueue: _fetchQueue,
_handleMessageFromObjC: _handleMessageFromObjC };
var messagingI
var sendMessageQueue = [];
var messageHandlers = {};
var CUSTOM_PROTOCOL_SCHEME = 'https'; var QUEUE_HAS_MESSAGE = '__wvjb_queue_message__';
var responseCallbacks = {};
var uniqueId = 1;
var dispatchMessagesWithTimeoutSafety = true;
function disableJavscriptAlertBoxSafetyTimeout() {
dispatchMessagesWithTimeoutSafety = false; }
function registerHandler(handlerName, handler) {
messageHandlers[handlerName] = }
function callHandler(handlerName, data, responseCallback) {
if (arguments.length == 2 && typeof data == 'function') {
responseCallback =
data = null;
_doSend({ handlerName:handlerName, data:data }, responseCallback); }
function _doSend(message, responseCallback) {
if (responseCallback) {
var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
responseCallbacks[callbackId] = responseC
message['callbackId'] = callbackId;
sendMessageQueue.push(message);
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE; }
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue);
sendMessageQueue = [];
return messageQueueS }
function _handleMessageFromObjC(messageJSON) {
_dispatchMessageFromObjC(messageJSON); }

我要回帖

更多关于 emmmmm什么意思 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信