FengJian's Blog

iOS Golang node.js Developer


  • Home

  • About

  • Archives

iOS cache系统和自定义http代理

Posted on 2014-10-08 | Edited on 2016-05-04

面试的时候,我喜欢问一些关于iOS http cache的问题,遇见不少开发者,第一反应都是自己编码实现整个cache存取流程,思路是没有问题的,但是这并不是我想要的答案。对于大部分使用场景,iOS自带的cache系统,就已经足够了,官方文档里也描述的很清楚,可以直接看 Understanding Cache Access。

流媒体播放的缓存需求

在做一个音乐播放器app的过程中,有一个需求是可以边播放边缓存,并且不能浪费流量(不能播放时有一次http下载,离线缓存时又有另一次http下载)。

技术方案

查了一大堆文档,做了很多原型代码,最终还是发现 AVQueuePlayer 并不是基于 URL Loading System 实现的,所以不可能直接使用其中的cache系统,只能想其他的办法,比如:

  1. 使用 CFNetwork、Audio File Stream Services 以及 Audio Queue Services 分别实现http下载、多媒体解码和播放过程,因为所有的流程都是自己控制,自然也就可以实现边播边存的效果。
  2. 使用 ffmpeg 第三方多媒体框架来实现。
  3. 仍然使用iOS自带的 AVQueuePlayer 进行http流媒体播放,但是在app内运行一个支持cache的 http proxy,让 AVQueuePlayer 通过这个 proxy 请求多媒体文件。

方案对比

第一种方案是比较容易想到的,但是其实实现的难度比较大,三个模块都要使用偏底层的接口,学习成本很高。第二种方案其实也是学习成本很高,而且只能使用 ffmpeg 的软解码器,功耗也会很大。前两个方案还有一个共同的问题,就是需要自己考虑如何和iOS系统做整合,比如如何优雅的实现后台播放功能等等。综合考虑后,还是选择了第三种方案,虽然也有一定的学习成本,而且需要修改标准的 http proxy 协议,但是大部分模块都是用iOS的SDK实现的,和操作系统贴合的最紧密。

实现 http proxy

自己完整的实现一个 http proxy,也是一件很复杂的事情,所以还是偏向于找开源方案,对比了几个开源方案后,最终选择在 Polipo 的基础上进行修改。

有一个技术细节需要说明一下,当使用浏览器、不使用代理的时候,通过抓包可以看到浏览器发出的http请求如下(忽略了无关内容):

1
2
3
GET / HTTP/1.1
Host: www.example.com
Connection: keep-alive

当使用浏览器并且使用代理的时候,通过抓包可以看到浏览器发出的http请求如下(忽略了无关内容):

1
2
3
GET http://www.example.com/ HTTP/1.1
Host: www.example.com
Proxy-Connection: keep-alive

最重要的一个区别就是 GET 请求后面的路径信息,前者是相对路径 /,而后者是绝对路径 http://www.example.com/。因为浏览器本身支持设置代理,所以浏览器会拼接合适的路径信息并且发送。但是iOS的 AVQueuePlayer 并不支持 http proxy 功能,无法和标准的代理服务器协同工作,所以只能同时在 AVQueuePlayer 和 Polipo 上做一些小的修改。

使用 AVQueuePlayer 的时候,需要做一些 magic trick,关键代码如下:

1
httpProxyUrl = [NSURL URLWithString:[NSString stringWithFormat:@"http://127.0.0.1:%d/http://%@/music_new/%@", httpProxyPort, kKYMediaServiceManagerRemoteServerIpAndPort, mediaInfo.fileName]];

可以这样来理解这段代码,假设app内的 http proxy 的地址为 127.0.0.1:9258,实际的多媒体文件的地址为 http://media.test.com/xxx.mp3,那么 AVQueuePlayer 请求的最终地址就应该是:

1
http://127.0.0.1:9258/http://media.test.com/xxx.mp3

在 Polipo 中,解析得到的最终目的服务器的地址(相对路径)是 /http://media.test.com/xxx.mp3,需要稍微修改一下源代码,去掉最左侧的 / 字符。具体就是在 client.c 文件的 httpClientHandlerHeaders 函数中添加一小段代码,也就是 #ifdef POLIPO_KUYQI_VERSION 和 #endif 之间的那一段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
int
httpClientHandlerHeaders(FdEventHandlerPtr event, StreamRequestPtr srequest,
HTTPConnectionPtr connection)
{
HTTPRequestPtr request;
int rc;
int method, version;
AtomPtr url = NULL;
int start;
int code;
AtomPtr message;

start = 0;
/* Work around clients working around NCSA lossage. */
if(connection->reqbuf[0] == '\n')
start = 1;
else if(connection->reqbuf[0] == '\r' && connection->reqbuf[1] == '\n')
start = 2;

httpSetTimeout(connection, -1);


#ifdef POLIPO_KUYQI_VERSION
char *pch;
pch = strstr (connection->reqbuf, "/http://");//TODO: if the client use url encoding??
if (pch == NULL) {
//fprintf(stderr, "###########this is normal http request\r\n");
} else {
//remove the first '/'
pch = strstr (connection->reqbuf, "/");
pch[0] = ' ';
}
#endif



rc = httpParseClientFirstLine(connection->reqbuf, start,
&method, &url, &version);
if(rc <= 0) {
do_log(L_ERROR, "Couldn't parse client's request line\n");
code = 400;
message = internAtom("Error in request line");
goto fail;
}

do_log(D_CLIENT_REQ, "Client request: ");
do_log_n(D_CLIENT_REQ, connection->reqbuf, rc - 1);
do_log(D_CLIENT_REQ, "\n");

if(version != HTTP_10 && version != HTTP_11) {
do_log(L_ERROR, "Unknown client HTTP version\n");
code = 400;
message = internAtom("Error in first request line");
goto fail;
}

if(method == METHOD_UNKNOWN) {
code = 501;
message = internAtom("Method not implemented");
goto fail;
}

request = httpMakeRequest();
if(request == NULL) {
do_log(L_ERROR, "Couldn't allocate client request.\n");
code = 500;
message = internAtom("Couldn't allocate client request");
goto fail;
}

if(connection->version != HTTP_UNKNOWN && version != connection->version) {
do_log(L_WARN, "Client version changed!\n");
}

connection->version = version;
request->flags = REQUEST_PERSISTENT;
request->method = method;
request->cache_control = no_cache_control;
httpQueueRequest(connection, request);
connection->reqbegin = rc;
return httpClientRequest(request, url);

fail:
if(url) releaseAtom(url);
shutdown(connection->fd, 0);
connection->reqlen = 0;
connection->reqbegin = 0;
httpConnectionDestroyReqbuf(connection);
connection->flags &= ~CONN_READER;
httpClientNewError(connection, METHOD_UNKNOWN, 0, code, message);
return 1;

}

微信web页面开发代码规范以及最佳实践

Posted on 2014-09-26 | Edited on 2016-04-10

本来专职是做iOS开发的,对 node.js 感兴趣,所以也学习过服务器开发的一些皮毛。前段时间公司的一个产品,要覆盖iOS、Android和微信web这3个前端,微信web页面的第一个版本,是服务器端同事用最传统的web技术做出来的,也就是使用后端模板渲染的技术组装页面,前端js则使用了 jQuery 做简单的操作。东西做出来后,体验特别的不好,尤其是每次页面跳转都要加载一个新页面,会有延迟。后来狠下决心重做一遍,完全采用前端渲染的技术。当时本来就缺人手,好歹我也算是会用JavaScript,义不容辞的自然也就把这个项目接手过来了。

做的过程很累,特别的赶进度,而且是摸着石头过河,未知的因素很多。还好iOS做的很熟悉了,很多经验或问题,可以直接照搬到web端,借助Google和Stack Overflow,记录了一大堆笔记出来。前两天另一个团队启动新项目,微信web端也是要做的,同事便找到我,想要一些经验分享,琢磨了一下,笔记写的比较凌乱,毕竟主要是给自己看的,符合自己的思维模式习惯,但是并不适合给别人看,干脆整理一份出来,方便别人查看,对自己也是再次清理一下思路。

CSS中如何命名class

这个话题没有唯一答案,而且特别的松散,风格很多。

目前团队里使用 http://webuild.envato.com/blog/how-to-scale-and-maintain-legacy-css-with-sass-and-smacss/ 这种方案,但是并不完全遵照它的代码模板,比如我们不使用 Sass 语法,而是用 SCSS 语法。

另外还推荐阅读 http://blog.jobbole.com/47702/ 这个里面提到了几乎所有的技术、框架、思路,比如 Sass、BEM、OOCSS、SMACSS、OrganicCSS等等。

使用 Sass 编写CSS

这个没啥多说的,语法并不难。引入了编程语言中的一些思路和技巧,对于iOS和Android开发者来说,很容易上手。

CSS书写规范 && 顺序

参照 http://markdotto.com/2011/11/29/css-property-order/ 以及 http://codegeekz.com/standardizing-css-property-order/

如何计算 CSS Box Model 中的 width 和 height

我们使用 CSS3 中的 border-box 模型,这个就和iOS中的 UIView 的尺寸模型保持了一致,也更直观,容易理解。

使用 Flexbox 做 Layout

项目的前期,为了实现一些手机上常见的布局,我们大量使用了CSS中的 float 、table-cell 等等,但是代码会比较复杂,而且可读性不好。直到发现了神器 Flexbox。

另外,还有一个基于 Sass 的工具 https://github.com/mastastealth/sass-flex-mixin.git,帮助我们更好的进行编码。

使用 Yeoman 实现工作流

不懂得使用工具的web开发者,不是好前端,嘿嘿 ^_^

MV* 框架 Or jQuery 类型的库

  • 单纯从技术角度来看,AngularJS 是最合适的框架,而且针对mobile,还有基于 AngularJS 的 Ionic 框架。Ionic 是对手机适配的最好的框架(没有之一)。但是 Ionic 的体积比较大,官方宣传时定义其为 framework for developing hybrid mobile apps。如果是对网速不敏感的使用场景,或者网速很快的场景,其实 Ionic 是可以做 web app 的。
  • 百度开源的 gmu,是类似于 jQuery UI 的库,但是是基于 zepto 的,很轻量级,而且也提供了不少的 widget。但是为了轻量,并没有套用 MV* 模式,所以应用场景复杂的时候,代码通常会组织的比较凌乱。交互界面复杂的时候,还会暴露出各种各样的坑,比如click事件穿透,就让我们大吃苦头。
  • 我个人已经不太愿意继续使用 gmu 了,如果真正只需要开发轻量级的页面,我宁愿直接用 Sass + zepto 或 http://minifiedjs.com/ 来实现。
  • Backbone 是相对轻量级的 MVC 框架,在体积大小和功能上有合理的舍取,但是框架本身只注重设计模式的引入,并不包含一套完整的针对mobile的 widget,所以我们还整理了另外一种思路,就是基于 Backbone,再加上各种各样小的lib,根据需求组合起来使用。这种方案可能存在的问题就是这些lib各自为政,不像 AngularJS 这种框架一样都在一个体系内协同工作,所以开发的时候也许会有很多坑,得做一遍才会有深刻的体会。有两个例子,非常值得学习参考,https://github.com/ccoenraets/directory-backbone-ratchet 和 http://n12v.com/2-way-data-binding/。

常见widget

按照iOS平台上的开发经验,针对mobile,常见的 widget 包括这些(有官方SDK自带的,也有大量第三方开源的,iOS平台现在很完善,有很多 widget 可以拿来即用)

  • button,textinput,slider,progress bar,image view,switch等等,这些是最常见的 widget
  • 全屏HUD,比如iOS上的 https://github.com/jdg/MBProgressHUD
  • 全屏的菜单选择类 widget,比如iOS自带的 UIAlertView 和 UIActionSheet
  • popview,比如iOS上的 https://github.com/chrismiles/CMPopTipView.git
  • 免干扰式的下拉信息提示框,比如iOS上的 https://github.com/toursprung/TSMessages.git
  • 系统级的页面切换方式,比如iOS自带的 UINavigationController 和 UITabBarController,以及第三方开源的 https://github.com/ECSlidingViewController/ECSlidingViewController.git

mobile web平台上,widget 的生态环境并不好。相对而言,Ionic 自带的widget是最完善的,而且有框架的支持,也更容易实现自定义的 widget。唯一的问题就是 Ionic 框架比较大。

Backbone 或 gmu 中,除了最常见的 widget 外,其他的通常都只能自己实现,比如在使用 gmu 的时候,我们就只能自己编写ActionSheet、全屏HUD、免干扰式的下拉信息提示框、以及系统级的页面导航控制器。由于缺少框架级的支持,除了 ActionSheet 外,其他几个 widget 的代码实现都很粗暴,而且遇到了各种各样的bug。

实际项目中,最好从一开始做交互设计的时候,就考虑 widget 的问题,尽量使用最常见的 widget,舍弃一些复杂的交互方式。

iOS输入法性能优化

Posted on 2014-09-26 | Edited on 2016-04-10

一年前指导同事开发了一款andriod版的输入法(非中文),其中的词库引擎,是我做的技术选型并且在iOS平台上做了原型验证,采用的是 Ternary Search Tree。一年后,也就是最近,随着iOS8的发布,我们也要发布一款iOS版的输入法。

遇到的主要问题

  1. Ternary Search Tree 实现 prefix match 的速度很快,但是因为只是一个纯粹的内存数据结构,所以输入法词库的容量是一个瓶颈。在android平台上,考虑多方面因素后,我们的词库中只有3万左右的单词量。iOS平台上做原型验证的时候,词库容量也只能做到10万左右。但是实际的业务需求中,是希望词库容量可以进一步增大的。
  2. iOS版本输入法的开发过程中,遇到了另外一个问题,就是键盘页面的加载速度和切换速度有点慢,用户能够感觉出来。

解决办法

词库容量

词库容量扩充这个问题,其实一直是一个难题,在 Ternary Search Tree 上也做过一些优化,但是变化并不明显。反而是在做server端开发,学习 LevelDB 的时候,碰巧发现 LevelDB 是一个很好的替代。首先 LevelDB 支持 prefix search,而且搜索速度也很快,测试数据表明完全满足我们的业务需求,其次 LevelDB 是将数据存储在文件系统上的,没有了内存大小的限制,词库的容量很轻松就可以扩充100倍以上,而且有了这种近乎无限的词库容量后,之前有一些需要复杂算法甚至很难实现的业务需求,现在也可以在超大词库的基础上,用“简单但是粗暴”的算法实现出来。

iOS平台上的 Core Data 是一套相当好用的数据持久化存储框架,唯一可能存在的问题就是性能,因此有些开发者在某些场景中,还是愿意去选择使用 SQLite 。有了这次的开发经验后,相信在某些应用场景中, LevelDB 也将会是一个很好的替代方案,比如 Square 开源的 Viewfinder 中的客户端,就是用 LevelDB 实现的数据存储。LevelDB 的核心是 LSM-Tree,其实 SQLite4 的核心,也是 LSM-Tree,小伙伴们,你们知道吗 :-)

页面加载和切换速度

说实话,页面加载速度这个问题,挺出乎意料的,以前我们团队也做了这么多iOS应用了,从来没有在页面速度上遇到过问题,用 Instruments、 NSLog 对比分析了一遍,测量出来的页面加载时间,也和其他应用中页面加载消耗的时间差不多。大家讨论了一下为什么用户会觉得慢,得出的结论是,输入法本来就是一个效率型的工具app,用户心理的期待之一,就是键盘的速度要快,而普通类型的app,用户对速度不会这么敏感。

问题已经出来了,还是得想办法去优化,吭哧吭哧写代码调试,从3个方面压缩了页面加载切换时消耗的时间:

  1. 键盘的view,是分了好几个层次的,当作为container的UIView加载完成后,就立刻让键盘先显示出来,然后再触发加载真正的keyboard view,这样给用户的一个心理感觉就是键盘弹出的速度很快。
  2. 键盘切换的时候,不再每次都重新从xib中加载对应的view,而是将view缓存在cache里面,用空间换时间。
  3. 移除了keyboard view中每个key view上的 Auto Layout 约束条件,直接在 layoutSubviews 方法中设置subview的 frame,关于这个优化思路,可以看看 Optimising Autolayout。需要强调的是,我们并不是否定 Auto Layout ,实际上我们团队现在采用的思路是 Auto Layout 和 Manual Frame Layout 一起使用,代码布局和xib布局一起使用,根据页面的需求做出更合适的选择。
  4. 2014-10-16更新,借助Facebook出品的神器https://github.com/facebook/AsyncDisplayKit.git,又抠了一些性能出来:]

这款输入法app,我们还全面切换到使用 ReactiveCocoa 这个框架进行开发,当时也怀疑过是不是因为这个框架造成了性能的损失,从 Instruments 的测量数据来看,我们的顾虑是多余的, ReactiveCocoa 虽然使得整个函数调用栈的层次增加了不少,但是,这不是性能瓶颈。

博客搬家 && 重新开博

Posted on 2014-09-26 | Edited on 2016-04-10

和小伙伴一起创业一年半了,投入了很大的精力,也舍弃了个人的一些事情,写博客便是其中之一。这一年多来,其实还是写了很多个人笔记,觉得还是有必要整理一些出来,所以便有了这个新的站点。

12
FengJian

FengJian

14 posts
22 tags
RSS
GitHub
© 2018 FengJian
Powered by Hexo v3.7.1
|
Theme — NexT.Pisces v6.3.0