仅博客成员可见mc成员是什么意思思

以小巧的体积做到了让JS调用/替换任意OC方法,让iOS APP具备的能力,在实现 JSPatch 过程中遇到过很多困难也踩过很多坑,有些还是挺值得分享的。本篇文章从基础原理、方法调用和方法替换三块内容介绍整个 JSPatch 的实现原理,并把实现过程中的想法和碰到的坑也尽可能记录下来。
能做到通过JS调用和改写OC方法最根本的原因是 Objective-C 是动态语言,OC上所有方法的调用/类的生成都通过 Objective-C Runtime 在运行时进行,我们可以通过类名/方法名反射得到相应的类和方法:
Class class = NSClassFromString(&UIViewController&);
id viewController = [[class alloc] init];
SEL selector = NSSelectorFromString(&viewDidLoad&);
[viewController performSelector:selector];
也可以替换某个类的方法为新的实现:
static void newViewDidLoad(id slf, SEL sel) {}
class_replaceMethod(class, selector, newViewDidLoad, @&&);
还可以新注册一个类,为类添加方法:
Class cls = objc_allocateClassPair(superCls, &JPObject&, 0);
objc_registerClassPair(cls);
class_addMethod(cls, selector, implement, typedesc);
对于 Objective-C 对象模型和动态消息发送的原理已有很多文章阐述得很详细,例如,这里就不详细阐述了。理论上你可以在运行时通过类名/方法名调用到任何OC方法,替换任何类的实现以及新增任意类。所以 JSPatch 的原理就是:JS传递字符串给OC,OC通过 Runtime 接口调用和替换OC方法。这是最基础的原理,实际实现过程还有很多怪要打,接下来看看具体是怎样实现的。
是最近业余做的项目,只需在项目中引入极小的引擎,就可以使用JavaScript调用任何Objective-C的原生接口,获得脚本语言的能力:动态更新APP,替换项目原生代码修复bug。
是否有过这样的经历:新版本上线后发现有个严重的bug,可能会导致crash率激增,可能会使网络请求无法发出,这时能做的只是赶紧修复bug然后提交等待漫长的AppStore审核,再盼望用户快点升级,付出巨大的人力和时间成本,才能完成此次bug的修复。
使用JSPatch可以解决这样的问题,只需在项目中引入JSPatch,就可以在发现bug时下发JS脚本补丁,替换原生方法,无需更新APP即时修复bug。
@implementation JPTableViewController
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
NSString *content = self.dataSource[[indexPath row]];
//可能会超出数组范围导致crash
JPViewController *ctrl = [[JPViewController alloc] initWithContent:content];
[self.navigationController pushViewController:ctrl];
上述代码中取数组元素处可能会超出数组范围导致crash。如果在项目里引用了JSPatch,就可以下发JS脚本修复这个bug:
#import “JPEngine.m&
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
[JPEngine startEngine];
[NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@&http://cnbang.net/bugfix.JS&]] queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
NSString *script = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (script) {
[JPEngine evaluateScript:script];
return YES;
defineClass(&JPTableViewController&, {
//instance method definitions
tableView_didSelectRowAtIndexPath: function(tableView, indexPath) {
var row = indexPath.row()
if (self.dataSource().length & row) {
//加上判断越界的逻辑
var content = self.dataArr()[row];
var ctrl = JPViewController.alloc().initWithContent(content);
self.navigationController().pushViewController(ctrl);
这样 JPTableViewController 里的 -tableView:didSelectRowAtIndexPath: 就替换成了这个JS脚本里的实现,在用户无感知的情况下修复了这个bug。
更多的使用文档和demo请参考。
JSPatch用iOS内置的JavaScriptCore.framework作为JS引擎,但没有用它JSExport的特性进行JS-OC函数互调,而是通过Objective-C Runtime,从JS传递要调用的类名函数名到Objective-C,再使用NSInvocation动态调用对应的OC方法。详细的实现原理以及实现过程中遇到的各种坑和hack方法会另有文章介绍。
目前已经有一些方案可以实现动态打补丁,例如,可以用Lua调用OC方法,相对于WaxPatch,JSPatch的优势是:
JS比Lua在应用开发领域有更广泛的应用,目前前端开发和终端开发有融合的趋势,作为扩展的脚本语言,JS是不二之选。
2.符合Apple规则
JSPatch更符合Apple的规则。里3.3.2提到不可动态下发可执行代码,但通过苹果JavaScriptCore.framework或WebKit执行的代码除外,JS正是通过JavaScriptCore.framework执行的。
使用系统内置的JavaScriptCore.framework,无需内嵌脚本引擎,体积小巧。
4.支持block
wax在几年前就停止了开发和维护,不支持Objective-C里block跟Lua程序的互传,虽然一些第三方已经实现block,但使用时参数上也有比较多的限制。
相对于WaxPatch,JSPatch劣势在于不支持iOS6,因为需要引入JavaScriptCore.framework。另外目前内存的使用上会高于wax,持续改进中。
JSPatch让脚本语言获得调用所有原生OC方法的能力,不像web前端把能力局限在浏览器,使用上会有一些安全风险:
1.若在网络传输过程中下发明文JS,可能会被中间人篡改JS脚本,执行任意方法,盗取APP里的相关信息。可以对传输过程进行加密,或用直接使用https解决。
2.若下载完后的JS保存在本地没有加密,在未越狱的机器上用户也可以手动替换或篡改脚本。这点危害没有第一点大,因为操作者是手机拥有者,不存在APP内相关信息被盗用的风险。若要避免用户修改代码影响APP运行,可以选择简单的加密存储。
JSPatch可以动态打补丁,自由修改APP里的代码,理论上还可以完全用JSPatch实现一个业务模块,甚至整个APP,跟wax一样,但不推荐这么做,因为:
JSPatch和wax一样都是通过Objective-C Runtime的接口通过字符串反射找到对应的类和方法进行调用,这中间的字符串处理会损耗一定的性能,另外两种语言间的类型转换也有性能损耗,若用来做一个完整的业务模块,大量的频繁来回互调,可能有性能问题。
开发过程中需要用OC的思维写JS/Lua,丧失了脚本语言自己的特性。
JSPatch和wax都没有IDE支持,开发效率低。
若想动态为APP添加模块,目前React Native给出了很好的方案,解决了上述三个问题:
JS/OC不会频繁通信,会在事件触发时批量传递,提高效率。(详见)
开发过程无需考虑OC的感受,遵从React框架的思想进行纯JS开发就行,剩下的事情React Native帮你处理好了。
React Native连都准备好了。
所以动态添加业务模块目前还是推荐尝试React Native,但React Native并不会提供原生OC接口的反射调用和方法替换,无法做到修改原生代码,JSPatch以小巧的引擎补足这个缺口,配合React Native用统一的JS语言让一个原生APP时刻处于可扩展可修改的状态。
目前JSPatch处于开发阶段,稳定性和功能还存在一些问题,欢迎大家提建议/bug/PR,一起来做这个项目。
分类: Tags:
介绍了怎样把HTML+CSS解析转换成NSAttributeString,本篇接着看看怎样把NSAttributeString渲染出来。
先简单介绍下CoreText,CoreText是iOS/OSX里的文字渲染引擎,在iOS/OSX上看到的所有文字在底层都是由CoreText去渲染。
CoreText会把一行里连在一起相同属性的文字合在一起作为一个CTRun,每一行是一个CTLine,多行合在一起组成CTFrame。如上图,第一行的文字有两种样式,第一部分是加粗,第二部分是斜体,因为样式不同所以分成了两个CTRun,CTLine包含了这两个CTRun,CTFrame包含了所有CTLine。
一个NSAttributeString可以通过CoreText提供的方法生成CTFramesetter,CTFramesetter是用于创建CTFrame的工厂,给CTFramesetter一个CGPath,或者简单理解为给他一个框框,它就会通过它持有的CTTypesetter生成CTFrame,CTFrame生成时里面包含的CTLine和CTRun就全部生成好了,可以直接绘制到画布上。CTFrame/CTLine/CTRun都提供了渲染接口,但前两者是封装,最后实际都是调用到CTRun的渲染接口去绘制。
如果要用CoreText渲染NSAttributeString,可以简单生成CTFramesetter,再生成CTFrame,在UIView的drawRect方法里直接把CTFrame绘制到当前画布上:
- (void) drawRect:(CGRect)rect
UIBezierPath *path = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, 320, 400)];
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)content);
CTFrame frame = CTFramesetterCreateFrame(framesetter,CFRangeMake(0, 0), [path CGPath] , NULL);
CGContextRef ctx = UIGraphicsGetCurrentContext();
CTFrameDraw(frame, ctx);
CoreText会按NSAttributeString里的样式属性把文字渲染出来。这种是最简单的粗粒度的渲染方式,但如果需要对文字渲染再做进一步处理,例如添加背景色等这些CoreText没有支持的属性,或者要在文字中间插入图片,就不能简单绘制CTFrame,需要逐行或逐个CTRun处理。
DTCoreText需要处理穿插在文字里的各类Attachment,并支持文字背景色,段缩进等CoreText不支持的属性,不能简单把NSAttributeString扔给CoreText渲染了事,需要做更细致的处理。DTCoreText分了几层,整体结构图:
最上层是使用者,可以是Controller,例如项目里示例的DemoTextViewController,也可以是某视图类。接着是DTCoreText封装好的各个控件,自带的有Label,TextView和Cell,这些控件的文字渲染都由DTAttributedTextContentView负责,非文字部分例如图片/视频等元素会在上层使用者那里通过delegate传给DTAttributedTextContentView。DTCoreTextLayouter / DTCoreTextLayoutFrame / DTCoreTextLayoutLine / DTCoreTextGlyphRun这四个类分别对应CoreText里的CTFramesetter / CTFrame / CTLine / CTRun,模仿了CoreText的模式,功能和作用一样,只是在它们基础上添加了功能。接下来看看每一个类具体做了什么事情。
DTAttributedTextContentView
继承自UIView,作为DTCoreTextLayoutFrame和上层控件的中间层,负责按需求绘制内容,大致做了以下几件事:
1.支持CATiledLayer分段渲染
把UIView的layerClass设为就能实现分区域渲染,即只渲染显示在屏幕上的区域,类似那些地图APP的效果,主要用于像TextView这样可能内容很长的控件,避免一次性把全部内容渲染出来,只渲染能看到的部分,提高性能。使用CATiledLayer后,在方法里用CGContextGetClipBoundingBox通过context取得当前显示的区域,DTCoreTextLayoutFrame只渲染这个区域的内容就行了。
2.生成DTCoreTextLayoutFrame并绘制
通过上层传进来的NSAttributeString生成DTCoreTextLayouter和DTCoreTextLayoutFrame,进行各种配置后用DTCoreTextLayoutFrame渲染文字到当前layer上,这些配置包括 是否显示图片链接/限定行数/断行规则等。
3.处理Attachment和Link
在方法里遍历DTCoreTextLayoutFrame里的每一个DTCoreTextGlyphRun,找出有附件和链接的Run进行处理,附件包括图片/视频等,创建这些附件对应的view,把这些view按DTCoreTextGlyphRun计算好的位置添加到专门存放附件和链接的customViews上完事。
实际上这些附件view的创建是在上层使用者那里,DTAttributedTextContentView通过delegate把每个附件的内容和对应的frame传到上层生成相应的view再给回来,这样做估计是因为对附件的处理每个使用者的需求都不一样,不应该直接写死在底层,例如有些使用者要求图片需要点击后放大,视频需要用自己的控件等。
DTCoreTextLayouter
负责生成和缓存DTCoreTextLayoutFrame,相当于CTFramesetter和CTFrame的关系,做的事很简单,就是通过NSAttributeString生成CTFramesetter,再根据不同的rect生成DTCoreTextLayoutFrame,并缓存这些frame。
DTCoreTextLayoutFrame
是最重要的一个类,负责渲染文字,主要做了两件事:生成行和渲染每一行。
生成DTCoreTextLayoutLine
会创建出当前frame范围内可见的每一行DTCoreTextLayoutLine,创建过程中做的处理包括:
1.支持整段缩进
从NSAttributeString里取出当前行是否有表示缩进的DTTextBlock,如果需要缩进,要计算出当前行缩进后的宽度和位置。
2.支持截断加省略号
上层像Label/TextView这样的控件是限制了宽高的,如果内容超出了宽高,就需要对最后一行进行处理,在合适的位置加”…”。
这里有个问题,就是必须在渲染到超出宽高的那一行时,才知道要处理的最后一行是什么。例如一个TextView高40,文字每行高15,在渲染第三行时高已经到45,发现已经超出了TextView的高度,这时知道只能渲染到第二行,但当前已经处理到第三行了,需要把第二行拿出来截断加”…”。
另外除了超出高度,在超出外部传进来的numberOfLines时也要截断,为了统一流程,这里的做法是在渲染超出高度时记录总共可以渲染多少行(),然后全部重新来,从头到尾再生成每一行,这时已经知道总共有多少行,在生成最后一行时处理就行了。这样做优点是简单粗暴避免重复代码,缺点是浪费性能,前面所有行都要重新排一遍。
3.支持hyphen
hyphen是连字符号,就是让英文单词在合适的位置换行并加上破折号”-”。CoreText原生不支持hyphen,断行方式只有按单词断行和安字母断行。这里hyphen的实现方式是:在所有英文单词里可以加破折号的位置全部加上占位符0x00AD,例如location->lo-ca-tion->lo0x00ADca0x00ADtion。0x00AD是不可见字符,CoreText不会渲染这个字符,但在这个字符的位置是可以断行的,CoreText不再认为location是一个单词,会在占位符处换行。DTCoreText做的处理就是如果发现换行处是占位符0x00AD,就替换成破折号”-”,所以要支持hyphen,传进来的内容就必须是所有单词都写好占位符的,否则无效。
4.计算每一行在当前frame的位置
在生成每一行时是不知道这一行在当前frame的位置的,需要自己手动计算。每一行的x坐标容易确定,但y坐标的计算就要费一番功夫。要考虑的因素有当前行高,上一行位置,行距,段间距,padding,baseline等。
如图,每一行以baseline为基准,需要计算出这一行的baseline在当前frame的Y坐标值,asent与descent是CoreText给出的值,asent+descent就是行高。推算当前行baseline位置的流程是:
A.计算上一行的行末位置,即baseline+descent
B.计算上一行行间距的一半,例如1.5倍行间距,就是 ((1.5 – 1)*asent+descent)/2
C.计算当前行行间距的一半,算法同上,只是这一行的行间距不一定与上一行一致。这里两行各算一半也是为了不同行间距的中和。
D.上述计算结果相加,再加上当前行asent值,就得到当前行的baseline Y坐标值。
除了上述主流程,还针对首行,段首段尾,DTTextBlock的留白和附件Attachment做了处理,计算的逻辑在。
5.处理对齐
要对每一种对齐方式进行处理,右对齐和居中对齐需要计算出行的x坐标值,两端对齐需要通过CTLineCreateJustifiedLine方法重新创建出一个两端对齐的行,针对两端对齐这里还要了两件事,一是段末不做两端对齐,二是若内容长度不够(默认是不足行宽的60%)也不做两端对齐,避免文字间距拉伸得太厉害效果差。
6.封装成DTCoreTextLayoutLine
经过上述处理,每一行的CTLine对象以及这一行的位置信息都有了,把这些封装成DTCoreTextLayoutLine保存起来,任务就完成了。
DTCoreTextLayoutFrame对外提供了方法,用于把上述生成的每一行都渲染到传进来的context画布上。做的处理包括:
1.绘制DTTextBlock样式
DTCoreText支持段落加背景色,在这里会先找出所有DTTextBlock,通过一系列麻烦的方法取到这些block的坐标和大小,把它们对应的背景色画出来。
2.绘制附件
实现了DTTextAttachmentDrawing接口的附件可以在这里跟文字一起绘制出来,在DTCoreText里图片附件就是实现了DTTextAttachmentDrawing接口,可以直接把图片在这里绘制出来。实际上图片附件的渲染DTCoreText提供了两种方式,上面介绍DTAttributedTextContentView时说图片附件也可以在上层让用户自行添加,若要在上层自行添加,可以传参数告诉DTCoreTextLayoutFrame绘制时不要处理图片附件。
3.绘制文字和阴影
最后就是再遍历每一行DTCoreTextLayoutLine以及行里的每一个DTCoreTextGlyphRun,调用它的-drawInContext:方法逐个run绘制到画布上。绘制时需要算好每个Run的位置,调用CGContextSetTextPosition定位到指定位置绘制文字。绘制文字同时还处理了阴影效果,CoreText不直接支持文字阴影效果,但可以用CoreGraphic的接口在绘制时加上阴影,这里还支持同时存在多个shadow -_-!
DTCoreTextLayoutLine
封装了CTLine,做的事包括:
1.生成GlyphRun
通过CTLine可以取出所这一行里的CTRun,计算每个CTRun的位置,封装生成DTCoreTextGlyphRun。
2.计算属性和提供辅助方法
计算并保存了这一行asent/descent/lineHeight等属性,提供各种辅助方法方便获取这一行里的信息,包括通过stringIndex获取对应文字的坐标等,CTLine相关的几个方法例如CTLineGetOffsetForStringIndex() / CTLineGetStringIndexForPosition()也有相应的封装。
DTCoreTextGlyphRun
里做的事跟DTCoreTextLayoutLine差不多,只是在渲染方法里额外做了一些事,首先支持文字背景色,这是CoreText原生不支持的,如果Attribute里有背景色的属性,这里会绘制出来。然后支持iOS6以下文字的下划线和删除线,iOS6以前CoreText是不支持下划线和删除线的,这里自己做了处理把它画上去。
整个流程最核心的就是DTCoreTextLayoutFrame生成行和渲染的实现,相当于把CoreText原生的CTFramesetterCreateFrame / CTFrameDraw再自己实现了一遍,在实现的过程加上自己特殊的需求,从中我们也可以大致了解到CTFrame/CTLine内部大致实现是怎样的。CoreText已经提供了足够细粒度的接口让使用者可以按自己意愿去随意排版,DTCoreText这一系列的处理给出了很好的示例可供参考。
分类: Tags:
是facebook刚开源的框架,可以用javascript直接开发原生APP,先不说这个框架后续是否能得到大众认可,单从源码来说,这个框架源码里有非常多的设计思想和实现方式值得学习,本篇先来看看它最基础的JavaScript-ObjectC通信机制(以下简称JS/OC)。
React Native用iOS自带的JavaScriptCore作为JS的解析引擎,但并没有用到JavaScriptCore提供的一些可以让JS与OC互调的特性,而是自己实现了一套机制,这套机制可以通用于所有JS引擎上,在没有JavaScriptCore的情况下也可以用webview代替,实际上项目里就已经有了用webview作为解析引擎的实现,应该是用于兼容iOS7以下没有JavascriptCore的版本。
普通的JS-OC通信实际上很简单,OC向JS传信息有现成的接口,像webview提供的-stringByEvaluatingJavaScriptFromString方法可以直接在当前context上执行一段JS脚本,并且可以获取执行后的返回值,这个返回值就相当于JS向OC传递信息。React Native也是以此为基础,通过各种手段,实现了在OC定义一个模块方法,JS可以直接调用这个模块方法并还可以无缝衔接回调。
举个例子,OC定义了一个模块RCTSQLManager,里面有个方法-query:successCallback:,JS可以直接调用RCTSQLManager.query并通过回调获取执行结果。:
@implement RCTSQLManager
- (void)query:(NSString *)queryData successCallback:(RCTResponseSenderBlOCk)responseSender
RCT_EXPORT();
NSString *ret = @&ret&
responseSender(ret);
RCTSQLManager.query(&SELECT * FROM table&, function(result) {
//result == &ret&;
接下来看看它是怎样实现的。
模块配置表
首先OC要告诉JS它有什么模块,模块里有什么方法,JS才知道有这些方法后才有可能去调用这些方法。这里的实现是OC生成一份模块配置表传给JS,配置表里包括了所有模块和模块里方法的信息。例:
&remoteModuleConfig&: {
&RCTSQLManager&: {
&methods&: {
&query&: {
&type&: &remote&,
&methodID&: 0
&moduleID&: 4
OC端和JS端分别各有一个bridge,两个bridge都保存了同样一份模块配置表,JS调用OC模块方法时,通过bridge里的配置表把模块方法转为模块ID和方法ID传给OC,OC通过bridge的模块配置表找到对应的方法执行之,以上述代码为例,流程大概是这样(先不考虑callback):
在了解这个调用流程之前,我们先来看看OC的模块配置表式怎么来的。我们在新建一个OC模块时,JS和OC都不需要为新的模块手动去某个地方添加一些配置,模块配置表是自动生成的,只要项目里有一个模块,就会把这个模块加到配置表上,那这个模块配置表是怎样自动生成的呢?分两个步骤:
1.取所有模块类
每个模块类都实现了RCTBridgeModule接口,可以通过runtime接口objc_getClassList或objc_copyClassList取出项目里所有类,然后逐个判断是否实现了RCTBridgeModule接口,就可以找到所有模块类,实现在方法里。
2.取模块里暴露给JS的方法
一个模块里可以有很多方法,一些是可以暴露给JS直接调用的,一些是私有的不想暴露给JS,怎样做到提取这些暴露的方法呢?我能想到的方法是对要暴露的方法名制定一些规则,比如用RCTExport_作为前缀,然后用runtime方法class_getInstanceMethod取出所有方法名字,提取以RCTExport_为前缀的方法,但这样做恶心的地方是每个方法必须加前缀。React Native用了另一种黑魔法似的方法解决这个问题:编译属性__attribute__。
在上述例子中我们看到模块方法里有句代码:RCT_EXPORT(),模块里的方法加上这个宏就可以实现暴露给JS,无需其他规则,那这个宏做了什么呢?来看看它的定义:
#define RCT_EXPORT(JS_name) __attribute__((used, section(&__DATA,RCTExport& \
))) static const char *__rct_export_entry__[] = { __func__, #JS_name }
这个宏的作用是用编译属性__attribute__给二进制文件新建一个section,属于__DATA数据段,名字为RCTExport,并在这个段里加入当前方法名。编译器在编译时会找到__attribute__进行处理,为生成的可执行文件加入相应的内容。效果可以从看出来:
# Sections:
# Address Size Segment Section
0xx000C0180 __TEXT __text
0x10011EFA0 0x __DATA RCTExport
0x 0x __DATA __common
0x 0x __DATA __bss
0x10011EFA0 0x [ 4] -[RCTStatusBarManager setStyle:animated:].__rct_export_entry__
0x10011EFB0 0x [ 4] -[RCTStatusBarManager setHidden:withAnimation:].__rct_export_entry__
0x10011EFC0 0x [ 5] -[RCTSourceCode getScriptText:failureCallback:].__rct_export_entry__
0x10011EFD0 0x [ 7] -[RCTAlertManager alertWithArgs:callback:].__rct_export_entry__
可以看到可执行文件数据段多了个RCTExport段,内容就是各个要暴露给JS的方法。这些内容是可以在运行时获取到的,在RCTBridge.m的方法里获取这些内容,提取每个方法的类名和方法名,就完成了提取模块里暴露给JS方法的工作。
整体的模块类/方法提取实现在方法里。
接下来看看JS调用OC模块方法的详细流程,包括callback回调。这时需要细化一下上述的调用流程图:
看起来有点复杂,不过一步步说明,应该很容易弄清楚整个流程,图中每个流程都标了序号,从发起调用到执行回调总共有11个步骤,详细说明下这些步骤:
1.JS端调用某个OC模块暴露出来的方法。
2.把上一步的调用分解为ModuleName,MethodName,arguments,再扔给MessageQueue处理。
在初始化时模块配置表上的每一个模块都生成了对应的remoteModule对象,对象里也生成了跟模块配置表里一一对应的方法,这些方法里可以拿到自身的模块名,方法名,并对callback进行一些处理,再移交给MessageQueue。具体实现在BatchedBridgeFactory.js的里,整个实现区区24行代码,感受下JS的魔力吧。
3.在这一步把JS的callback函数缓存在MessageQueue的一个成员变量里,用CallbackID代表callback。在通过保存在MessageQueue的模块配置表把上一步传进来的ModuleName和MethodName转为ModuleID和MethodID。
4.把上述步骤得到的ModuleID,MethodId,CallbackID和其他参数argus传给OC。至于具体是怎么传的,后面再说。
5.OC接收到消息,通过模块配置表拿到对应的模块和方法。
实际上模块配置表已经经过处理了,跟JS一样,在初始化时OC也对模块配置表上的每一个模块生成了对应的实例并缓存起来,模块上的每一个方法也都生成了对应的对象,这里通过ModuleID和MethodID取到对应的Module实例和RCTModuleMethod实例进行调用。具体实现在_handleRequestNumber:moduleID:methodID:params:。
6.RCTModuleMethod对JS传过来的每一个参数进行处理。
RCTModuleMethod可以拿到OC要调用的目标方法的每个参数类型,处理JS类型到目标类型的转换,所有JS传过来的数字都是NSNumber,这里会转成对应的int/long/double等类型,更重要的是会为block类型参数的生成一个block。
例如-(void)select:(int)index response:(RCTResponseSenderBlock)callback 这个方法,拿到两个参数的类型为int,block,JS传过来的两个参数类型是NSNumber,NSString(CallbackID),这时会把NSNumber转为int,NSString(CallbackID)转为一个block,block的内容是把回调的值和CallbackID传回给JS。
这些参数组装完毕后,通过NSInvocation动态调用相应的OC模块方法。
7.OC模块方法调用完,执行block回调。
8.调用到第6步说明的RCTModuleMethod生成的block。
9.block里带着CallbackID和block传过来的参数去调JS里MessageQueue的方法invokeCallbackAndReturnFlushedQueue。
10.MessageQueue通过CallbackID找到相应的JS callback方法。
11.调用callback方法,并把OC带过来的参数一起传过去,完成回调。
整个流程就是这样,简单概括下,差不多就是:JS函数调用转ModuleID/MethodID -> callback转CallbackID -> OC根据ID拿到方法 -> 处理参数 -> 调用OC方法 -> 回调CallbackID -> JS通过CallbackID拿到callback执行
上述第4步留下一个问题,JS是怎样把数据传给OC,让OC去调相应方法的?
答案是通过返回值。JS不会主动传递数据给OC,在调OC方法时,会在上述第4步把ModuleID,MethodID等数据加到一个队列里,等OC过来调JS的任意方法时,再把这个队列返回给OC,此时OC再执行这个队列里要调用的方法。
一开始不明白,设计成JS无法直接调用OC,需要在OC去调JS时才通过返回值触发调用,整个程序还能跑得通吗。后来想想纯native开发里的事件响应机制,就有点理解了。native开发里,什么时候会执行代码?只在有事件触发的时候,这个事件可以是启动事件,触摸事件,timer事件,系统事件,回调事件。而在React Native里,这些事件发生时OC都会调用JS相应的模块方法去处理,处理完这些事件后再执行JS想让OC执行的方法,而没有事件发生的时候,是不会执行任何代码的,这跟native开发里事件响应机制是一致的。
说到OC调用JS,再补充一下,实际上模块配置表除了有上述OC的模块remoteModules外,还保存了JS模块localModules,OC调JS某些模块的方法时,也是通过传递ModuleID和MethodID去调用的,都会走到方法把两个ID和参数传给JS的,跟JS调OC原理差不多,就不再赘述了。
整个React Native的JS-OC通信机制大致就是这样了,关键点在于:模块化,模块配置表,传递ID,封装调用,事件响应,其设计思想和实现方法很值得学习借鉴。
分类: Tags:
是个开源的iOS富文本组件,它可以解析HTML与CSS最终用CoreText绘制出来,通常用于在一些需要显示富文本的场景下代替低性能的UIWebView,来看看它是怎样解析和渲染HTML+CSS的,总体上分成两步:
数据解析—把HTML+CSS转换成NSAttributeString
渲染—用CoreText把NSAttributeString内容渲染出来,再加上图片等元素
本篇先介绍第一步,数据解析的实现。
整体流程如图,HTML字符串传入,通过的回调解析后生成dom树,dom树的每个节点都是自定义的,通过解析每个元素对应的样式,这时每个DTHTMLElement已经包含了节点的内容和样式,最后从DTHTMLElement生成NSAttributeString。这一切都是在解析dom的过程中同步进行,为了分析方便,我们还是把它分为三个步骤:
解析HTML,生成dom树
解析CSS,合并得到每个dom节点对应的样式
生成NSAttributeString
接下来详细介绍这三个步骤的实现方式。
iOS/OSX自带了XML/HTML的解析引擎libxml,它提供了两种解析html的接口:
直接根据HTML字符串在内存生成一颗dom树,使用者可以自由遍历这颗dom树。这个方法的优点是使用简单方便,缺点一是内存使用多,无论多大的html文件都会一次性生成dom树放在内存里,二是性能不高,它生成dom树时遍历了一遍,用户使用时又遍历了一遍。
SAX的解析方式不会返回一个dom树,而是把解析过程都暴露给使用者,通过回调函数告诉调用者当前解析到了什么元素/内容,让使用者决定怎么处理。举个例子,对&p&content&/p&这段html进行解析时,解析器找到&p&标签就会回调startElement方法,告诉使用者找到了一个标签的开始标志,tag是p。接着解析到content,会回调_characters,告诉使用者解析到文本内容,最后解析&/p&回调endElement,告诉调用者遇到标签结束标志。
这种解析方式的优点一是占用内存少,它的解析是流式的,不需要一次性传入整个内容,也不生成占内存的dom树,二是性能高,相当于使用时边解析边处理,而不是解析完生成dom树后再遍历dom树进行处理,少了一步。缺点是使用复杂。
DTCoreText采用的是SAX解析方式,主要原因应该还是为了性能考虑。DTHTMLParser把libxml这种解析方式的c接口封装成OC接口,用delegate的方式通知回调用者各个解析事件,除此之外还做了几件事:
处理文本编码
转换数据格式,把dom节点的attribute转成NSDictionary,error换成NSError,bytes换成NSData。
因为libxml一次只解析一小部分字符串,如果dom的内容特别长,libxml会分多次回调_characters,DTHTMLParser做了数据拼合的工作,确保回调给delegate的内容数据是完整的。
DTHTMLAttributeStringBuilder接收DTHTMLParser的回调,生成dom树,节点是自定义的DTHTMLElement,有指向父节点的引用以及子节点数组,生成dom树的实现逻辑很简单:
实例变量_currentTag用于保存当正在解析的节点(回调了startElement未回调endElement的节点)。
startElement回调时,假设当前回调里找到的节点是elem,把elem设为_currentTag的子节点,再把_currentTag变量设为elem。
找到文本内容foundCharacters回调时,内容作为一个节点,设为_currentTag的子节点。
endElement回调时,_currentTag变量设为_currentTag的父节点。
简化代码:
- (void)startElement:(NSString *)elemName
Element *elem = [Element elementWithName:elemName];
if (_currentTag) {
[_currentTag.childNodes addObject:elem];
elem.parentNode = _currentT
_currentTag =
- (void)foundCharacters:(NSString *)ctn
Element *elem = [[Element alloc] initWithString:ctn];
[_currentTag.childNodes addObject:elem];
elem.parentNode = _currentT
- (void)endElement:(NSString *)elemName
_currentTag = _currentTag.parentN
这套逻辑循环下来,就生成了dom树。
不同的DTHTMLElement子类。
在生成dom树节点时,DTHTMLElement会根据传入的标签名生成不同的子类,包括文本DTTextHTMLElement,超链接DTAnchorHTMLElement,列表DTListItemHTMLElement等,这些子类实现了各自特殊的样式和转换成NSAttributeString的逻辑,后面会提到。
特殊标签逻辑
DTHTMLAttributedStringBuilder在回调startElement和endElement里会对不同标签做一些特殊处理,例如&style&标签要解析里面的css内容,&link&标签要读取文件再解析css内容,&h1&标签要设置元素的headerLevel等。可能为了代码好看些,这些处理逻辑是放_tagStartHandlers/_tagEndHandlers这两个dictionary,key是标签名,value是处理的block,startElement/endElement时根据元素名调用相应的block。
多线程解析
为了解析的速度更快,DTHTMLAttributedStringBuilder生成了三个dispatch_queue,分别是
解析html的_dataParsingQueue,生成dom树的_treeBuildingQueue,以及组装NSAttributeString的_stringAssemblyQueue,把解析过程有序地分派到这三条线程里并行执行,并用dispatch_group_wait阻塞等到所有任务都完成时同步返回结果。
对样式CSS的解析大致流程是这样:css原数据-&结构化NSDictionary-&合并样式-&DTHTMLElement属性。最终为每一个DTHTMLElement解析出这个元素的最终样式,接下来看看每一步是怎么做的。
css文本最终需要变成结构化的NSDictionary,便于为dom节点匹配选择器和处理,例如:
font-size:14
background: #
最终要转变成
@“body”: @{
@“font-size”: @“14px”,
@“background”: @“#fff”
@“.hd”: @{@“width”: @“100px”},
css数据的解析比较简单,不需要词法分析,只需要字符串匹配,分两步走:
A.css块解析,分离css选择器与内容
上面例子的css,需要先分离选择器和内容,解析成@{@”body”: @“font-size:14\nbackground: #”, @“.hd”: @”width:100”},怎么做?
DTCSSStylesheet的方法是遍历每个字符,定一个标志位置braceMarker,找到’{‘,就把braceMaker到这个’{‘字符间的字符串提取出来,就是选择器,braceMaker重设为’{‘的下一个位置,继续找下一个字符,直到找到’}’,把braceMaker到这个’}’间的字符串提取出来,就是选择器对应的内容,css块解析就完成了。简化的代码:
- (void)parseStyleBlock:(NSString *)css
int braceMarker = 0;
for (int i = 0; i & css. i ++) {
if (c == ‘{‘) {
selector = [ css substringWithRange:NSMakeRange(braceMarker, i-braceMarker)];
braceMarker = i + 1;
if (c == ‘}‘) {
NSString *rule = [ css substringWithRange:NSMakeRange(braceMarker, i-braceMarker)];
[self addRule:rule withSelector:selector];
braceMarker = i + 1;
举个例子,上述css中,解析body{}这个块,起始标志位置braceMarker=0,开始遍历字符串,找到’{’,位置idx=5,从braceMaker开始到’{‘的前一个字符就是key,于是subString(0,4),也就是’body’就是选择器,接着把braceMaker设为’{’的下一个位置,braceMaker=6,继续往下找,找到’}’,位置idx=35,于是subString(6,35-1)就是css内容。
DTCSSStylesheet的实现里考虑了注释的去除,还考虑了css内容里出现’{‘’}’字符的情况,搜索过程通过braceLevel确保第一层{}block才解析。不过DTCSSStylesheet没有考虑@import,@chartset等特殊css字段。
B.解析内容
对简单的css内容(类似font-size:14background: #)的解析可以很简单,用;号分割,再用:号分割就行了,但这样无法应对一些异常,例如内容中间加个注释就挂了。DTCoreText的实现是用NSScaner按顺序扫关键字,先找selector,再找冒号’:’,接着处理值,具体实现在。
2.合并样式
影响一个dom节点css样式的内容分布在四个地方,一是全局默认样式,二是HTML里&link&标签外联的css文件,三是HTML里&style&标签里的内容,四是dom节点style属性(例&a style=“color:white”&)。
DTHTMLAttributeStringBuilder持有一个_globalStyleSheet,在初始化时就加载并解析了全局默认样式。接着在解析HTML生成dom树的过程中,如果遇到&style&标签,会对标签里的css内容进行解析,然后合并入_globalStyleSheet,&link&标签也一样,会根据文件路径读取css文件内容并解析合并,自此上述前三个点都合并在_globalStyleSheet里了。接着要解析一个dom节点的样式时,调用_globalStyleSheet的mergedStyleDictionaryForElement:方法,把节点传进来,用节点的tagName/class/id等属性在_globalStyleSheet表里匹配到相应的css样式,接着解析节点自身的style属性合并,就得到了这个节点里所有的css样式。
3.转化为DTHTMLElement属性
DTHTMLElement的applyStyleDictionary:方法会把css属性转换为DTHTMLElement自身的属性,方法就是一个个属性去处理了,十分繁琐。
影响一个dom节点样式的其实还有两个点,一是dom里某些属性(例&p align=“left”&),二是从父节点继承下来的样式。分别在DTHTMLElement的两个方法inheritAttributesFromElement:和interpretAttribute里处理了,从父节点继承下来的不是css样式,而是处理好的DTHTMLElement属性。因为解析HTML是顺序解析,在解析子节点时父节点的样式一定已经解析完成,所以可以直接从父节点继承解析好的DTHTMLElement属性。这两点都是直接操作DTHTMLElement属性,不涉及CSS。
派生选择器
DTCSSStylesheet的选择器是支持派生选择器的,类似li a{},只匹配在&li&节点下的&a&节点,其他&a&不匹配。实现方式跟浏览器实现原理一样,拿到要匹配的DTHTMLElement节点,先把_globalStyleSheet里的所有派生选择器找出来,从右开始匹配,匹配到就遍历元素的父节点看是否匹配左边的选择器。例如li a{},先看节点是否&a&,若是,遍历节点的父节点,若找到有一个父节点是&li&,则匹配成功,否则匹配不成功,继续寻找下一个。具体实现在matchingComplexCascadingSelectorsForElement:方法里。
选择器权重
DTCSSStylesheet的选择器是有权重的,内联样式&id选择器&class选择器&派生选择器&tag选择器,权重高的会覆盖权重低的样式,若权重相等,则按书写的位置排,位置在后面的覆盖前面的。DTCSSStylesheet给每个selector定了权重值,id为100,class为10,其他为1,派生选择器的权重为各个selector类型权重的相加,在解析css时把选择器的权重和出现的顺序都保存起来,匹配时按权重和顺序值决定覆盖的规则。
一些css属性是有缩写的方式的,例如font:10就包括了font-size和font-weight,margin:10px 0;就包括了margin的四个方向,在对一个DTHTMLElement寻找匹配属性时,会把这些这些缩写全部处理展开,方便DTHTMLElement再进一步处理。处理的实现在_uncompressShorthands:里。
自此dom树上每个节点以及它们的样式都解析完成,只差最后一步转为NSAttributeString。
生成NSAttributeString
经过上述两大步骤后,dom树上的各DTHTMLElement节点都保存了各自完整的内容和样式,每个DTHTMLElement都可以完整地转换成NSAttributeString然后进行渲染。DTHTMLElement有个attributedString方法,负责生成对应的NSAttributeString,实现方式是把所有子节点的attributedString拼起来返回,递归调用直到叶子节点。
叶子节点有多种类型,包括文本DTTextHTMLElement,超链接DTAnchorHTMLElement,列表DTListItemHTMLElement等,它们都会根据attributesForAttributedStringRepresentation方法把DTHTMLElement的属性转化为CoreText认得的样式表,应用在自身内容上,生成NSAttributeString返回。只有叶子节点才会真正生成NSAttributeString内容,其他节点只会把所有子节点的内容拼起来。简化代码:
//普通节点:
- (NSAttributeString *)attributedString
NSMutableAttributedString *tmpString = [[NSMutableAttributedString alloc] init];
for (Element *elem in self.childNodes) {
[tmpString appendString:[elem attributedString]];
return tmpS
//叶子节点(文本内容节点DTTextHTMLElement为例):
- (NSAttributeString *)attributedString
NSDictionary *attributes = [self attributesForAttributedStringRepresentation];
return [[NSAttributedString alloc] initWithString:_text attributes:attributes];
因为调用一个DTHTMLElement的attributedString方法就可以得到它所有子节点拼合的NSAttributeString,所以只要调用body节点的attributeString,就可以获得最终的NSAttributeString。但是很多时候使用者传进来的只是一个html片段而不是一个完整的页面,很可能没有body节点,DTHTMLAttributeStringBuilder里处理了这种情况,不直接使用body的attributedString,body节点/没有父节点的节点/父节点是body的节点都会调用一次attributedString方法生成NSAttributeString,在DTHTMLAttributeStringBuilder里拼合成最终结果。
Coretext只能渲染文字,那多媒体元素像图片/视频等是怎样渲染的?首先在多媒体出现的地方,会在NSAttributeString里插入一个占位符,这个占位符的attribute属性里包含了多媒体对象,渲染到这个占位符时我们可以取出attribute里的多媒体对象,再通过addSubView之类的方式渲染上去。在我们渲染多媒体对象前还需要让CoreText知道这个多媒体占多大空间,让CoreText渲染文字时留出空白,实现方式是在占位符的attribute里加上kCTRunDelegateAttributeName,CoreText在渲染时会先回调attribute上这个键对应的callback,在callback里通过多媒体对象告诉CoreText需要留多大空位就行了。
display=none的节点不输出NSAttributeString。
display=block的节点(例如&p&&div&&h1&),会在内容后再加个换行符,根据css规则,后面的元素不能跟它同行,除非是float,目前不支持float属性。
上一个元素是display=inline(例如&a&&strong&&span&),当前元素是display=block时,需要在内容前面添加换行。inline是不换行的,block又要要单独一行,所以需要做这个判断。
dom节点上的一些属性也会加入NSAttributeString的attribute。
.fl strong {
&h1&第六章&/h1&
&p class=&fl&&我将来要当一名麦田里的守望者。有那么一群孩子在一大块麦田里玩。&strong&几千几万的小孩子&/strong&,附近没有一个大人,我是说—除了我。我呢。就在那混帐的悬崖边。我的职务就是在那守望。要是有哪个孩子往悬崖边来,我就把他捉住—我是说孩子们都是在狂奔,也不知道自己是在往哪儿跑。我得从什么地方出来,把他们捉住。我整天就干这样的事,我只想做个麦田里的守望者。&/p&
&p&&img src=&./catcher.png& /&&/p&
这个html经过这三步转换,变成了以下NSAttributeString:
{CTForegroundColor = &&CGColor 0x7fcf22d17a00&...&;DTHeaderLevel = 1;NSFont = &&UICTFont: 0x7fcf25050ed0& font-family: \&Times New Roman\&; font-weight: font-style: font-size: 12.00pt&;NSParagraphStyle = &&CTParagraphStyle: 0x7fcf22d18ca0&{...}&;
我将来要当一名麦田里的守望者。有那么一群孩子在一大块麦田里玩。
{CTForegroundColor = &&CGColor 0x7fcf22d17a00&...&;NSFont = &&UICTFont: 0x7fcf25050ed0& font-family: \&Times New Roman\&; font-weight: font-style: font-size: 12.00pt&;NSParagraphStyle = &&CTParagraphStyle: 0x7fcf&{...}&;}
几千几万的小孩子
{CTForegroundColor = &&CGColor 0x7fcf22d17a00&...&;NSFont = &&UICTFont: 0x7fcf25050ed0& font-family: \&Times New Roman\&; font-weight: font-style: font-size: 12.00pt&;NSParagraphStyle = &&CTParagraphStyle: 0x7fcf22d2e530&{...}&;}
,附近没有一个大人,我是说—除了我。我呢。就在那混帐的悬崖边。我的职务就是在那守望。要是有哪个孩子往悬崖边来,我就把他捉住—我是说孩子们都是在狂奔,也不知道自己是在往哪儿跑。我得从什么地方出来,把他们捉住。我整天就干这样的事,我只想做个麦田里的守望者。
{CTForegroundColor = &&CGColor 0x7fcf22d17a00&...&;NSFont = &&UICTFont: 0x7fcf25050ed0& font-family: \&Times New Roman\&; font-weight: font-style: font-size: 12.00pt&;NSParagraphStyle = &&CTParagraphStyle: 0x7fcf&{...}&;}
{CTForegroundColor = &&CGColor 0x7fcf22d17a00&...&;CTRunDelegate = &&CTRunDelegate 0x7fcf22d1d7cef0]&&;DTAttachmentParagraphSpacing = 0;
NSAttachmentAttributeName = &&DTImageTextAttachment: 0x7fcf22d241b0&&;NSFont = &&UICTFont: 0x7fcf25050ed0& font-family: \&Times New Roman\&; font-weight: font-style: font-size: 12.00pt&;NSParagraphStyle = &&CTParagraphStyle: 0x7fcf&{...}&;
接下来就可以拿这个NSAttributeString用CoreText渲染了。
分类: Tags:
是Path团队开发的一个开源库,用于提升图片的加载和渲染速度,让基于图片的列表滑动起来更顺畅,来看看它是怎么做的。
iOS从磁盘加载一张图片,使用UIImageVIew显示在屏幕上,需要经过以下步骤:
从磁盘拷贝数据到内核缓冲区
从内核缓冲区复制数据到用户空间
生成UIImageView,把图像数据赋值给UIImageView
如果图像数据为未解码的PNG/JPG,解码为位图数据
CATransaction捕获到UIImageView layer树的变化
主线程Runloop提交CATransaction,开始进行图像渲染
6.1 如果数据没有字节对齐,Core Animation会再拷贝一份数据,进行字节对齐。
6.2 GPU处理位图数据,进行渲染。
FastImageCache分别优化了2,4,6.1三个步骤:
使用mmap内存映射,省去了上述第2步数据从内核空间拷贝到用户空间的操作。
缓存解码后的位图数据到磁盘,下次从磁盘读取时省去第4步解码的操作。
生成字节对齐的数据,防止上述第6.1步CoreAnimation在渲染时再拷贝一份数据。
接下来具体介绍这三个优化点以及它的实现。
平常我们读取磁盘上的一个文件,上层API调用到最后会使用系统方法read()读取数据,内核把磁盘数据读入内核缓冲区,用户再从内核缓冲区读取数据复制到用户内存空间,这里有一次内存拷贝的时间消耗,并且读取后整个文件数据就已经存在于用户内存中,占用了进程的内存空间。
FastImageCache采用了另一种读写文件的方法,就是用mmap把文件映射到用户空间里的虚拟内存,文件中的位置在虚拟内存中有了对应的地址,可以像操作内存一样操作这个文件,相当于已经把整个文件放入内存,但在真正使用到这些数据前却不会消耗物理内存,也不会有读写磁盘的操作,只有真正使用这些数据时,也就是图像准备渲染在屏幕上时,虚拟内存管理系统VMS才根据缺页加载的机制从磁盘加载对应的数据块到物理内存,再进行渲染。这样的文件读写文件方式少了数据从内核缓存到用户空间的拷贝,效率很高。
一般我们使用的图像是JPG/PNG,这些图像数据不是位图,而是是经过编码压缩后的数据,使用它渲染到屏幕之前需要进行解码转成位图数据,这个解码操作是比较耗时的,并且没有GPU硬解码,只能通过CPU,iOS默认会在主线程对图像进行解码。很多库都解决了图像解码的问题,不过由于解码后的图像太大,一般不会缓存到磁盘,SDWebImage的做法是把解码操作从主线程移到子线程,让耗时的解码操作不占用主线程的时间。
FastImageCache也是在子线程解码图像,不同的是它会缓存解码后的图像到磁盘。因为解码后的图像体积很大,FastImageCache对这些图像数据做了系列缓存管理,详见下文实现部分。另外缓存的图像体积大也是使用内存映射读取文件的原因,小文件使用内存映射无优势,内存拷贝的量少,拷贝后占用用户内存也不高,文件越大内存映射优势越大。
Core Animation在图像数据非字节对齐的情况下渲染前会先拷贝一份图像数据,官方文档没有对这次拷贝行为作说明,模拟器和Instrument里有高亮显示“copied images”的功能,但似乎它有bug,即使某张图片没有被高亮显示出渲染时被copy,从调用堆栈上也还是能看到调用了CA::Render::copy_image方法:
那什么是字节对齐呢,按我的理解,为了性能,底层渲染图像时不是一个像素一个像素渲染,而是一块一块渲染,数据是一块块地取,就可能遇到这一块连续的内存数据里结尾的数据不是图像的内容,是内存里其他的数据,可能越界读取导致一些奇怪的东西混入,所以在渲染之前CoreAnimation要把数据拷贝一份进行处理,确保每一块都是图像数据,对于不足一块的数据置空。大致图示:(pixel是图像像素数据,data是内存里其他数据)
块的大小应该是跟CPU cache line有关,ARMv7是32byte,A9是64byte,在A9下CoreAnimation应该是按64byte作为一块数据去读取和渲染,让图像数据对齐64byte就可以避免CoreAnimation再拷贝一份数据进行修补。FastImageCache做的字节对齐就是这个事情。
FastImageCache把同个类型和尺寸的图像都放在一个文件里,根据文件偏移取单张图片,类似web的css雪碧图,这里称为ImageTable。这样做主要是为了方便统一管理图片缓存,控制缓存的大小,整个FastImageCache就是在管理一个个ImageTable的数据。整体实现的数据结构如图:
一些补充和说明:
ImageTable
一个ImageFormat对应一个ImageTable,ImageFormat指定了ImageTable里图像渲染格式/大小等信息,ImageTable里的图像数据都由ImageFormat规定了统一的尺寸,每张图像大小都是一样的。
一个ImageTable一个实体文件,并有另一个文件保存这个ImageTable的meta信息。
图像使用entityUUID作为唯一标示符,由用户定义,通常是图像url的hash值。ImageTable Meta的indexMap记录了entityUUID-&entryIndex的映射,通过indexMap就可以用图像的entityUUID找到缓存数据在ImageTable对应的位置。
ImageTableEntry
ImageTable的实体数据是ImageTableEntry,每个entry有两部分数据,一部分是对齐后的图像数据,另一部分是meta信息,meta保存这张图像的UUID和原图UUID,用于校验图像数据的正确性。
Entry数据是按内存分页大小对齐的,数据大小是内存分页大小的整数倍,这样可以保证虚拟内存缺页加载时使用最少的内存页加载一张图像。
图像数据做了字节对齐处理,CoreAnimation使用时无需再处理拷贝。具体做法是CGBitmapContextCreate创建位图画布时bytesPerRow参数传64倍数。
ImageTable和实体数据Entry间多了层Chunk,Chunk是逻辑上的数据划分,N个Entry作为一个Chunk,内存映射mmap操作是以chunk为单位的,每一个chunk执行一次mmap把这个chunk的内容映射到虚拟内存。为什么要多一层chunk呢,按我的理解,这样做是为了灵活控制mmap的大小和调用次数,若对整个ImageTable执行mmap,载入虚拟内存的文件过大,若对每个Entry做mmap,调用次数会太多。
用户可以定义整个ImageTable里最大缓存的图像数量,在有新图像需要缓存时,如果缓存没有超过限制,会以chunk为单位扩展文件大小,顺序写下去。如果已超过最大缓存限制,会把最少使用的缓存替换掉,实现方法是每次使用图像都会把UUID插入到MRUEntries数组的开头,MRUEntries按最近使用顺序排列了图像UUID,数组里最后一个图像就是最少使用的。被替换掉的图片下次需要再使用时,再走一次取原图—解压—存储的流程。
FastImageCache适合用于tableView里缓存每个cell上同样规格的图像,优点是能极大加快第一次从磁盘加载这些图像的速度。但它有两个明显的缺点:一是占空间大。因为缓存了解码后的位图到磁盘,位图是很大的,宽高100*100的图像在2x的高清屏设备下就需要200*200*4byte/pixel=156KB,这也是为什么FastImageCache要大费周章限制缓存大小。二是接口不友好,需预定义好缓存的图像尺寸。FastImageCache无法像SDWebImage那样无缝接入UIImageView,使用它需要配置ImageTable,定义好尺寸,手动提供的原图,每种实体图像要定义一个FICEntity模型,使逻辑变复杂。
FastImageCache已经属于极限优化,做图像加载/渲染优化时应该优先考虑一些低代价高回报的优化点,例如CALayer代替UIImageVIew,减少GPU计算(去透明/像素对齐),图像子线程解码,避免Offscreen-Render等。在其他优化都做到位,图像的渲染还是有性能问题的前提下才考虑使用FastImageCache进一步提升首次加载的性能,不过字节对齐的优化倒是可以脱离FastImageCache直接运用在项目上,只需要在解码图像时bitmap画布的bytesPerRow设为64的倍数即可。
分类: Tags:
缩减iOS安装包大小是很多中大型APP都要做的事,一般首先会对资源文件下手,压缩图片/音频,去除不必要的资源。这些资源优化做完后,我们还可以尝试对可执行文件进行瘦身,项目越大,可执行文件占用的体积越大,又因为AppStore会对可执行文件加密,导致可执行文件的压缩率低,压缩后可执行文件占整个APP安装包的体积比例大约有80%~90%,还是挺值得优化的。下面介绍一下在研究可执行文件过程中发现的可以优化的点。研究的过程使用了linkmap,linkmap的介绍跟生成可以参考另一篇文章—。
1.编译器优化级别
Build Settings-&Optimization Level有几个编译优化选项,release版应该选择Fastest, Smalllest,这个选项会开启那些不增加代码大小的全部优化,并让可执行文件尽可能小。
2.去除符号信息
Strip Linked Product / Deployment Postprocessing / Symbols Hidden by Default 在release版本应该设为yes,可以去除不必要的调试符号。Symbols Hidden by Default会把所有符号都定义成”private extern”,详细信息见。
这些选项目前都是XCode里release的默认选项,但旧版XCode生成的项目可能不是,可以检查一下。其他优化还可以参考官方文档—
第三方库统计
项目里会引入很多第三方静态库,如果能知道这些第三方库在可执行文件里占用的大小,就可以评估是否值得去找替代方案去掉这个第三方库。我们可以从linkmap中统计出这个信息,对此写了个node.js脚本,可以通过linkmap统计每个.o目标文件占用的体积和每个.a静态库占用的体积,(需翻墙)。
有人提出用ARC写的代码编译出来的可执行文件是会比用MRC大的,原因大致是ARC代码会在某些情况多出一些retain和release的指令,例如调用一个方法,它返回的对象会被retain,退出作用域后会被release,MRC就不需要,汇编指令变多,机器码变多,可执行文件就变大了。还有其他细节实现的区别,先不纠结了。
那用ARC究竟会增大多少体积?我觉得从汇编指令的增多减少去算是很难算准确的,这东西涉及细节太多,还是得从统计的角度计算。做了几个对比试验,统计了几个同时支持ARC/MRC的开源项目在开启/关闭ARC的情况下__TEXT代码段的大小对比。只对比__TEXT代码段是因为:
ARC对可执行文件大小的影响几乎都是在代码段
可执行文件会进行某种对齐,例如有些段在不足32K的时候填充0直到对齐32K,若用可执行文件大小对比结果可能是对齐后的,不准确。
实验数据:
MBProgressHUD
SDWebImage
结果是ARC大概会使代码段增加10%的size,考虑代码段占可执行文件大约有80%,估计对整个可执行文件的影响会是8%。
可以评估一下8%的体积下降是不是值得把项目里某些模块改成MRC,这样程序的维护成本上升了,一般不到特殊情况不建议这么做。
在项目里新建一个类,给它添加几个方法,但不要在任何地方import它,build完项目后观察linkmap,你会发现这个类还是被编译进可执行文件了。
按C++的经验,没有被使用到的类和方法编译器都会优化掉,不会编进最终的可执行文件,但object-c不一样,因为object-c的动态特性,它可以通过类和方法名反射获得这个类和方法进行调用,所以就算在代码里某个类没被使用到,编译器也没法保证这个类不会在运行时通过反射去调用,所以只要是在项目里的文件,无论是否又被使用到都会被编译进可执行文件。
对此我们可以通过脚本,遍历整个项目的文件,找出所有没有被引用的类文件和没有被调用的方法,在保证没有其他地方动态调用的情况下把它们去掉。如果整个项目历时很长,历时代码遗留较多,这个清理对可执行文件省出的空间还是挺可观的。
类/方法名长度
观察linkmap可以发现每个类和方法名都在__cstring段里都存了相应的字符串值,所以类和方法名的长短也是对可执行文件大小是有影响的,原因还是object-c的动态特性,因为需要通过类/方法名反射找到这个类/方法进行调用,object-c对象模型会把类/方法名字符串都保存下来。
对此我们可以考虑在编译前把所有类和方法名进行混淆,跟压缩js一样,把长名字替换成短名字,这样做的好处除了缩小体积外,还对安全性有很大提升,别人拿到可执行文件对它class-dump出来的结果都是混淆后的类和方法名,就无法从类和方法名中猜出某个方法是做什么的,就难以挂钩子进行hack。不过这样做有个缺点,就是crash堆栈反解出来的堆栈方法名会是混淆后的,需要再加一层混淆-&原名的转换,实现和使用成本有点高。
实际上这部分占用的长度比较小,中型项目也就几百K,对安全性要求高的情况可以试试。
冗余字符串
代码上定义的所有静态字符串都会记录在在可执行文件的__cstring段,如果项目里Log非常多,这个空间占用也是可观的,也有几百K的大小,可以考虑清理所有冗余的字符串。另外如果有特别长的字符串,建议抽离保存成静态文件,因为AppStore对可执行文件加密导致压缩率低,特别长的字符串抽离成静态资源文件后压缩率会比在可执行文件里高很多。
最后把缩减iOS安装包大小的各种方法列出来做了张CheckList图:
又到了12月31日,第八篇年终博客。
房子这事跟2013年的总结连接上了,写2013时就在忙买房的事,今年1月1日就去跟房东签约交定金了,接下来是一阵折腾,手续繁多,出现各种难题一一解决,还好没出什么意外,顺利完成交易。接下来刷墙,买家具,5月份就先入住了,算是很快,接着陆陆续续把家里需要的东西买齐,花了不少时间精力,钱也哗啦啦流走,到最近几个月才止住。入住后没多大特殊感觉,我对这方面不太敏感,相对于租房最明显的差别就是可以随意买家具,随意放置装扮自己的家,当然环境是比之前晚上楼下烧烤档吵闹又有烟味是好得多了,但离地铁站也比之前远了,特别是现在每天上下班,上地铁下地铁两段路程要走二十多分钟,但也没找到什么好办法,走着走着就习惯了。地铁发展这么久,还没出现一种方便的地铁接驳工具,真是遗憾。
我挺希望我的房子可以做到简洁和实用,但不太容易,发现简洁是奢侈品,怎样才显得简洁?大量留白。怎样才能做到大量留白?一是东西少,二是房子大。我们一是东西多,二是房子小,简洁恐怕是实现不了了。即使东西少房子大,若想实用,也很难实现简洁,简洁和实用往往是一对矛盾的需求,例如你想方便地为手机充电,家里就会充满杂乱的充电线,你想家里能方便地拿到纸巾,家里就要放很多纸巾盒,在各种需求推动下慢慢变杂乱。要解决这个问题估计得等智能家居发达了,我说的不是现在的智能家居,现在的智能家居没看到有任何用处,没有让人拥有的欲望。理想中的智能家居是让家里的东西我不需要的时候隐藏,需要的时候马上出现,能远程执行机械控制,例如拉个窗帘,倒杯水之类的,而不是开关空调电灯电视这样画蛇添足的数码操控,悲观目测这一天还挺远的。
结婚咯,一下就变成已婚人士,至今对这身份的转变还没缓过神来╮(╯▽╰)╭ (开玩笑)。年初就领了证,年中拍了婚纱照,8月在家乡小小摆了下酒,弄得比较简单,只是请一下爸妈那代人的一些亲戚朋友喝个喜酒,再加几个好友庆贺,没有弄婚礼,但一些传统仪式还是有简化地做了。虽然仪式和酒席简单,但操办起来还是有很多繁琐事情,这些家里人都包办了,真是辛苦了。
摆完酒就去度蜜月了,想当年小学时看漫画IQ博士,第一次接触到度蜜月这几个字,当时还不知道啥意思,一晃就轮到我实践度蜜月了。去了马尔代夫瑞喜登岛玩了四五天,相当不错,第一次出国,第一次去海岛度假,第一次看到浅蓝色的海滩,第一次浮潜,总的来说时光就是在浮潜-吃饭-懒洋洋躺着-游泳-逛小岛中度过,舒适惬意。感觉四天的时光很长,过得很慢,可能因为过的日子不是重复的,是新颖的,人脑就会活跃起来感知外界,从而感知到的时间就越长。这么说来经常去体验不同的东西,常外出游玩,会让自己的精神寿命更长,感受到更多活着的时间。可惜正儿八经地工作后,能外出游玩的时间少了。
年初回腾讯工作,为什么回大公司工作我说了,小公司若没有高速发展,对技术人员的成长很不利,若走另一条路自己创业,也没萌生什么值得尝试的想法,社交能力和忽悠能力也是我的大短板。迷茫了好一阵子,在一段时间里还有能力停滞焦虑症,觉得还是先回大公司锻炼技术能力。工作了九个多月,刚开始一段时间比较困难,后面适应了就好了,现在状态不错,想认真投入做好每件事,在技术上做好一个产品。
人总是生活在一个个框架下,公务员生活在国家体制这个框架下,创业者生意人生活在市场经济这个大框架下,而大公司也形成了自己一套框架,在一个框架下待久了,可能会习惯这个框架,依赖于这个框架给予的利益,导致遵守规则追求框架内的利益成了最重要的事,眼界变窄收缩于公司内部,把自己其他的追求都抛之脑后。大公司提供了大平台和良好的成长环境,同时也可能有这样的局限,需要警惕。
今年不断在下滑,不太好受,不过也没更多办法,一是我没放多少精力在上面,没有精力去研究竞争对手,研究AppStore新规则,更新APP功能,做热点APP,解决投诉等,二是主要APP受冲击,在私密文件保存这个领域这一年多出了非常多模仿和竞争对手,有些除了名字跟我的一样,界面配色都被拙劣地模仿了,有些看起来是做得还不错,很多还是免费下载,在这一大波同类产品冲击下,销量下滑也是情理之中。
今年时间有限,只做了三个小APP,年初的,年中的和年末的。StockProfit每天自动计算已购股票的收益,但因为各国股票市场货币不同,不清楚世界各个股票市场对应的货币单位,至今还没做好汇率转换问题。WifiKeyboard是通过Wifi连接电脑的键盘,可以像蓝牙键盘一样用键盘在iPhone/iPad上打字,iOS8出来之前只能在APP内同步打字,打完后复制到其他地方,iOS8以后做了键盘扩展,可以在任意地方打字了,个人感觉还是挺不错的。Fitastic用于记录每日运动项目,很早就想做这个项目,因为做俯卧撑/plank这种运动时,挺想记录下知道我历史上一共做了多少个/多少分钟,不过觉得这个需求太小众,一直没做,这次在老婆的推动下完成了这个APP,每项运动的图标都是老婆设计的,算是合力做的一个APP。后面为APP的名字犯愁,最后还得感谢小木舟和老罗赐名。Fitastic刚好就在今天通过审核上架了~
还是挺喜欢做各种完整的小东西,这个爱好持续了十几年,搞得我对设计/产品/技术一直都有不小的兴趣,但时间和能力有限,目前只能选择发展技术能力,但时不时还是会有一些矛盾的想法。
今年开始买点股票玩玩,可惜碰上形式不好的一年,跟2013年完全无法比,2013年随便持有几个互联网股,放一年,不翻番至少也有50%的收益,今年要是买个互联网股放一年,不跌就偷笑了。结果我的战绩是不盈利不亏损,没仔细算过,估计比把钱放余额宝收益低。我只买了自己熟悉的互联网公司的股,没去分析各种股票市场走势,感觉比较复杂,有各种内幕,各种操盘博弈,A股在没内幕消息的情况下更是跟赌博没两样,一直没碰它。作为一个门外汉,买卖股票的时候心理活动挺有意思的,持有时看它涨了不舍得卖,觉得形势大好了应该还会继续涨,看着它跌了揪心,又不甘心冒着亏损把股票卖了,也一直等,导致买入的股票都不怎么卖出,到最后还是亏了。
今年没看什么书,因为把看书的时间都贡献给了听电台。年初发现逻辑思维很振奋,用上下班坐车走路时间把他过去一年的节目都给听了。接着年中发现晓松奇谈和晓说,更是喜欢,把他两年多100多期的节目听了,期间还听了不少一席的讲座。
逻辑思维大部分讲的是互联网思维/创业/小团队/市场经济,偶尔讲讲历史,但大部分是通过历史来说现代的一些道理。刚开始听觉得能获得挺多知识的,后来感觉说教味太浓了,道理说着说着还有点成功学的味道,最近几期听着老是有反驳的冲动,没那么有说服力,也不怎么喜欢听了。
高晓松说的题材比较纯粹,都是纯文科,历史/地理/政治/军事/民族/美国,都非常有意思,有点帮人睁眼看世界的感觉,有很多自己个人的观点,但没有说教的成分,只是跟大家分享下自己的想法,感觉很舒服。挺佩服高晓松庞大的知识面,可能因为我文科知识太匮乏,很多观点听起来都很有意思,例如上次博客提到的历史在马尔塞斯陷阱中不断循环,又如2012年最后一期说的科技和人文的交替前进。
听了这么多,说下一点小感触,世界很大,也很复杂,很多事情的发生都无法用几个因素推导出结果,但人脑最容易理解的就是因果关系,而且比较容易赞同逻辑简单的因果关系,所以在向其他人分析一件事的原因时,只需要列出众多原因中的一两条,就能把因果关系串起来,如果听众对这件事情的了解不是那么多,不知道影响事情发生的各种因素,很容易就会信服他对这件事因果关系的分析,而偏离事实真相,知识的作用之一就是减少这样的事情出现,它可以帮助你不被人或少被人忽悠,你知道得越多,别人偏离真相的推导就越不容易说服你,在信息爆炸的年代各种观点乱飞的时候帮助你尽量看清真相,不被洗脑,不轻易相信任何人的话,独立思考。
身体好了挺多,这一年没生什么病,就牙齿问题麻烦了点。
自律能力没多大提升,还是有大量时间照常浪费,注意力也经常分散,话说这事连续好几年的年终总结都是这样说,还有没有救……
从2010年开始在Google Calendar写每日做的事情,今年开始全部转到DayOne这个APP上,变成真正的写日记了,看上它的配图/搜索/同步功能,一天配一张图还挺有意思,不过生活单调后经常没有拍照。
据豆瓣统计,今年只读了区区七本书……是去年的1/6,上面说了因为业余时间都被电台占据了,工作时间又长,就没读什么书了。电影看了21部,倒不算少,但没什么值得特别推荐的电影。
2015年,好好过~
QQ邮箱4.0增加了漂流瓶功能,记录一下做漂流瓶过程中碰到的问题。
分类: Tags:
毕业之前一直在做前端开发,毕业后就转成做iOS开发,这两者有很多挺有意思的对比,尝试写下我能想到的它们的一些相同点和不同点。
前端和终端作为面向用户端的程序,有个共同特点:需要依赖用户机器的运行环境,所以开发语言基本上是没有选择的,不像后台想用什么就用什么,iOS只能用object-c,前端只能javascript,当然iOS还可以用RubyMotion,前端还能用GWT/CoffeeScript,但不是主流,用的人很少,真正用了也会多出很多麻烦。iOS还可以用苹果新出的swift语言,后面可能用于取代object-c,还处于起步阶段,先不讨论。
objc和js这两者有个有意思的对比:变量/方法命名的风格正好相反。苹果一直鼓吹用户体验,写代码也不例外,程序命名都是用英文全称并且要多详细有多详细,力求看变量和方法名就能知道是干嘛的,例如application:didFinishLaunchingWithOptions:。而js因为每次都要从网络下载,要力求减少代码体积,所以变量方法名是尽量用缩写,实际上有代码压缩工具,无论变量名写多长最终上线的效果是一样的,但大家也都习惯了用短的命名,例如上述objc的application:didFinishLaunchingWithOptions:方法在js里习惯的命名是:$()。
objc与js都是动态语言,使用起来还蛮像,但objc是编译型,速度快,很多错误也能在编译过程中被发现,js是解释型,性能依赖于解释引擎,即使在强劲的v8引擎下性能也赶不上编译型语言,语言太动态,变量完全没有类型,写起来爽,debug起来稍微费点劲。一直感觉js轻巧灵活放荡不羁充满各种,objc中规中矩没c++ java那么严肃也没有js那么灵活。

我要回帖

更多关于 6666是什么意思 的文章

 

随机推荐