分类 猪在写代码 下的文章

善用iOS App中webview

iOS开发中webview和native code写这是一件纠结的事。我写这篇文章, 介绍一下我做iOS两年来总结的一些在webview和native code的配合上的一些经验和技巧,当然,都是基于互联网App的,希望对大家有所帮助。

首先提两句两者的优劣。webview与运维成本低, 更新几乎不依赖App的版本;但在交互和性能上与跟native code有很大差距。native code与之对应。

注,我这里不说HTML5,因为我认为,HTML5确实给web带入了一个新时代。这个时代是什么,web app。也就是说,只有脱离native的这个前提,在浏览器的环境下,HTML5的意义才能显现,而我们讨论iOS App的时候,HTML5显然没什么意义。

不管是用webview还是native code,我有两个原则:

1,用户体验不打折
2,运维成本低

注,为什么不提开发成本。因为做web开发和iOS开发根本就是两回事。当然,web开发发展了这么多年,对于某些功能实现是要比native app快。但多数情况,同一个功能,对于iOS开发者和web开发者,用各自擅长的方式开发成本都最低,所以说某个功能开发成本低,往往是一个伪命题。

刚刚说了,webview的优势在于更新不依赖版本,那么在一款App中,只有会频繁更新的界面考虑webview才有意义。那么哪些界面会频繁更新,这就要因App而异了,我只说两年来,我接触到的一些。

首页。首页资源可谓必争之地,内容一天一换是正常现象,一天几换也不稀奇。而如果仅仅是内容的更换,非要上个webview就显得有些激进了。而事实上首页的变化千奇百怪,逢年过节变个脸,特殊情况挂个公告,偶尔还要特批强推一把某个业务,等等。
此前,我在设计App首页的时候,把首页配置设计的非常复杂。App端要处理n种情况,n各参数,server端要记住n种规则,直到一天,我崩溃了,把首页完全换成webview,才豁然开朗。

活动页。做互联网都知道,活动,是一个最常见的运营手段。特点是,周期短,功能少,但基本不能复用。这些特点都标识了活动不适合做native,要用webview实现。
即使有人告诉你说,我的活动是一个长期活动而且形式不变,也不要相信他。因为在第二期,第三期,第四期他会分别加上一些非常诡异,却有很合理的小变更,而这些变更是你在那个版本根本无法实现的。

试水的新功能。这种界面,往往设计不成熟,需要在运行过程中不断收集用户反馈,更新升级,甚至决定去留。所以,只有webview才能hold住如此不稳定的功能。
切记在一个功能还没有确定之前,不要大张旗鼓单位开发native code,要知道,你写的这些代码,三天后就要改一遍,而且要发布上线。

富文本内容。这个不用多说了吧,按照HTML的常用标签做一个webtext可不是小工程。而且富文本的变化太多了,一点无法匹配,都会导致整个界面巨丑。

OK,上边说了我认为最该使用webview的4个界面,分别带有不同的特点,用这些特点去描述一个界面,如果符合了,那么别犹豫了,做成webview吧。说了哪些界面做成webviev,再说说怎么做webview才能避其短处。

刚刚说了,webview的交互是个短板,因此webview在一个App中,只能作为界面,不允许在界面中出现动作。

而一个webview的界面如何跟native code结合起来呢,我的答案是,超链接。在webview上点击超链接,会调用webview delegate的shouldload方法,自这里拦截请求,进行处理。这里附上这种方法的说明,此前的一文:http://pingguohe.net/2011/06/25/webview_to_nativeview/

由于webview的链接都是URL,因此我建议,把整个App的界面都用URL管理起来。从320框架对VC的管理中获得灵感,构建一个新的应用,总是先制定一套协议,封装一个方法,每一次VC的切换都通过URL。如此一来,server与App的交互就简单了许多,webview也如是。

最后就是长相问题,webview很难长成native的view。而我的方案是,长不成也要装成。在一些情况下,禁用webview滚动,使用滚动框架(iScroll不错)去实现。webview上下留出200pixel的空白背景,y从-200开始。否则大家知道,webview上下会有阴影的背景,不藏起来会很丑。等等,还有很多其他的方法去伪装webview,是要视情景而用。
目前,就想到这些,一定有很多纰漏,只求抛砖引玉,能让webview在App中发挥优势,有助于开发者。欢迎交流讨论,问题可移步http://segmentfault.com

Bash Shell 里的各种括号

今天在 SegmentFault 上看到又有人问起关于Shell里各种括号的问题。对于很多玩Shell的人,括号是个很尴尬的问题,用起来没问题,说起来不明白,我在这里总结一下Bash Shell几种括号的用法和区别,别的Shell我不会。。。

(( )) :一对圆括号有两个地方用到。

1,for循环,

for (( expr1 ; expr2 ; expr3 )) 

这里一对双括号里边的表达式,GNU的文档指出,expr1支持 Shell Arithmetic;expr2不为0时,expr3被赋值且语句执行。说的很麻烦,还要花时间搞清楚什么是Shell Arithmetic。其实一言以蔽之,支持数字条件。比如:

for (( a=0 ; a<10 ; a++ )); do echo $a; done

会输出 0 1 2 3 (带换行哦~~~)

2,数学表达

(( )) 和 $(( ))

(( )) 的用法与let一样,就不用多解释了吧~~~

$(( ))就是把计算结果拿出来,可以用在双引号里边,比如:

echo "1+2=$(( 1 + 2 ))"

会输出 1+2=3

( ):一个圆括号

在for循环里,跟C语法一样一样的。

或者是子程序,返回整个里边表达的返回值。里边的变量都是局部的,修改不会带到外边。举例子

a=1

(a=3; echo $a)

echo a

结果是 3 1

还有个就是圈数组。。。这个就没神马意思了

[ ]:一个方括号,是bash的命令,查man手册是可以查到的,跟test一样,在手册里可以看到很多用法。比如-b -c -gt -eq 什么的很多,还有用-a表示与,-o表示或等等

[[ ]]:一对方括号是一个方括号的加强版,则是Shell的保留字,里边支持了 || && 等等这些符号。一般我喜欢用这个

还有相对复杂的 { }

几个用处,区分变量,如:

var=abcd; echo ${var}EFG;

这样,Bash就不会认为变量是varEFG了

还有用来截取字符串的 ${ }语法比较灵活多变,这里不做多解释,大家有兴趣可以自己去搜搜资料,一般我脚本用到的字符串处理,这个都能搞定了。

代码块。用来区分代码的,但是跟( )有个区别,就是在末尾要加上 ;

目前看到常用的用法也就是这样,肯定不全,欢迎补充,欢迎交流。

iOS sqlite控件 Sqlight

记:上一篇文章介绍了Kache这个缓存控件,可以用来配合这次iCloud升级,这篇是关于一个sqlite的封装,Sqlight,一个很轻量级的封装,也是为了配合iCloud。模仿了PHP mysql扩展PDO的一些做法。把数据库操作封装成方法,通过NSDictionary和NSArray传递参数。源码:http://jiajun.org/g/Sqlight.zip

Sqlight真是没什么可介绍的,主要的常用方法有4个,select, update, insert和delete,还有一个create table在初始化时调用。用法示例在 Sqlite.h 文件的注释中。

简单介绍一下原理,Sqlight的数据文件是放在Documentation目录的,也就是说会被备份到iCloud。跟cache不同,我建议在数据库里存放一些比如,用户信息,登录状态等等。

以下把用法示例贴一下:

 

SqlightAdapter *sqlight = [[Sqlight alloc] initWithDatabase:@"test_database" AndTable:@"test_tb"];

if (nil == sqlight) {

sqlight = [[Sqlight alloc] initWithDatabase:@"test_database"];

 

[sqlight createTable:@"test_tb" Info:[NSArray arrayWithObjects:@"f1", @"f2", nil]];

sqlight.tableName = @"test_tb";

}

[sqlight insertData:[NSDictionary dictionaryWithObjectsAndKeys:

@"value1",@"f1",

@"value2",@"f2",

nil]];

// this is a wrong condition, there is no f0 field.

sqlight_result_t res = [sqlight selectFields:[NSArray arrayWithObjects:@"f1, f2", nil]

ByCondition:@"f0=?"

  Bind:[NSArray arrayWithObjects:@"value1", nil]]; 

NSLog(@"%d -- %@ -- %@", res.code, res.msg, res.data);// will out put 1 -- no such column: f0 -- ()

res = [sqlight selectFields:[NSArray arrayWithObjects:@"f1, f2", nil]

ByCondition:@"f1=?"

Bind:[NSArray arrayWithObjects:@"value1", nil]]; 

NSLog(@"%d -- %@", res.code, res.data);// will out put 101 -- ( value1, value2 )

[sqlight updateData:[NSDictionary dictionaryWithObjectsAndKeys:@"value_new", @"f1", nil]

ByCondition:@"f1=?"

  Bind:[NSArray arrayWithObjects:@"value1", nil]];

res = [sqlight selectFields:[NSArray arrayWithObjects:@"f1, f2", nil]

ByCondition:@"f1=?"

Bind:[NSArray arrayWithObjects:@"value1", nil]]; 

NSLog(@"%d -- %@", res.code, res.data); // will out put 101 -- ()


 

iOS缓存控件 Kache

记:昨天(Sep. 29)想必很多 iOS 开发者都收到Apple的邮件和苹果中国的工作人员的电话了,要求开发者升级应用,调整本地文件的存储位置,以适应即将发布在iOS5中启用的iCloud同步应用数据功能。恰好我这里有n久以前写的一个缓存控件(Kache的源码:http://jiajun.org/g/Kache.zip,现在再拿出来,希望在这次大规模的升级中,能给大家带来方便。

Kache 的基本思路是:用一个单例对象hold住整块内存,每次set都把数据set到这块内存里,get也从这里拿。在需要持久化的时候,例如程序退出之类的,调用一个saveToStorage方法,把这块内存持久化,存到iCloud文档中提到的Caches目录,这个目录是不会被iCloud备份的。在需要从本地存储载入时调用一个loadFromStorage方法,把整块内存load出来,照常使用。注意:Kache只能缓存 NSData, NSDictionary, NSArray, NSString等这些基本数据结构,不能缓存其他对象。

Kache提供两个基本功能:

1,普通缓存,允许设定有效时长,如果用户不指定被缓存数据的有效时长,则默认为86400秒,也就是一天过期。

2,缓存队列,Kache提供一个队列,用户可指定队列长度,默认为100,当新数据进入缓存队列导致队列长度超过100时,最先进入的数据会被删除。普通缓存不影响队列。

特殊需求:

之前提到,Kache使用的是一个单利hold住整块内存,那么一个单例中只允许有一个缓存队列。如果需要多个缓存队列,那么可以使用Kache提供的实例方法,开发者自己创建单例对象来hold住不同的内存。

基本用法:

用法非常简单,只要 #import "KCH.h"后,就可以按照 KCH.h 文件注释的例子来调用缓存了。

阅读源码:

Kache的源码非常简单,只有6个文件加1个KCH.h。

KCH.h基本上是一个README作用的文件。

KCHObject.h/KCHObject.m 定义了缓存的原子对象,这里记录了某个数据的过期时间和具体数据等内容。

KCHHolder.h/KCHHolder.m 定义了整个缓存的基本操作方法,包括对过期的处理,对队列的处理,持久化,重置等等。

Kache.h/Kache.m 是对KCHHolder的封装,提供了所有操作的实例/静态方法,以及对一直说到的单例的定义。

最后,是Kache的源码:http://jiajun.org/g/Kache.zip

使用Three20

记:Three20(简称:TT)是Facebook维护的一个开源iPhone应用框架。框架封装了一系列视觉控件,网络组件,和工具方法。最近使用TT重构了一个app,这个app在1万行规模使用的是原生代码,架构非常简单。增长到2万行规模,这个原生的架构已经疲于应付迅速变化的业务需求,因此我们采用TT进行了重构。这里,简单介绍一下使用TT开发的app采用了怎样的架构,以及开发过程中的经验和教训。

app基于TTTableViewController的架构进行设计,主要分为三层:ViewControlelr,DataSource和Model。另外,平行于这三层设有Service和Util,封装一些通用逻辑和工具,例如:登陆,URLEncode等。最底层是Manager,封装网络控件,缓存控制等。除了这些还有独立封装的组件和对系统组件的扩展,如SegmentedControll,滚动图片等。这个架构不做赘述,说一说在这个架构下遇到的几个问题,以及解决方案。

1、TT对系统控件的封装无法满足个性化需求。

TT对很多系统控件进行封装,拿之前提到过的TTTableViewController举例,所有的cell都被封装,对框架使用者透明,而TT封装的cell类型无法完全满足需求,我们往往需要格式更加丰富的cell样式。

在这种情况下,我们选择对TT方法进行重写。Objective-c提供指定类指定方法的重写,因此集中把需要个性定制的TT控件进行重写,完全不修改TT本身的代码。这样操作,既满足了需求,又使在日后对TT框架进行升级变的非常方便,几乎不需要考虑升级造成的不兼容。

2、Cell的默认操作过分单一

TT是使用Navigator和ULR的策略(欲了解该策略请参考TT官方网站http://three20.info)来管理整个应用的ViewController的。

在TableView中没一个cell带有URL,这个URL表明了点击cell后要跳转到的viewController,同时TT还认为如果一个cell没有URL那么他就是不可点击的,而往往存在这样的需求,cell可点击,但点击操作却不是跳转到某个ViewController。

针对这种情况,我们定义空URL,空URL不指向任意ViewController的类,而是指向nil。带有空URL的cell既可点击,又不会跳转到任意ViewController。

3、构造ViewController的URL不支持中文

之前说了,TT使用URL管理ViewController。有些ViewController的参数是中文,而且需要通过URL传递,而Navigator不支持汉字URL。

增加URLEncode方法,对每一个配置到URL中的参数编码,生成编码后的URL就可以正常使用。另外,TT会自动Decode的URL,无需开发者处理。

但TT的在URL策略中“/”无法使用,即使进行Encode之后,放入URL仍无法使用。这就需要开发者在构造URL过程中,检查每一个参数,确保不出现“/”。

4、稳定版TT(v1.0.5)不支持ios3.1及以下版

根据我们对客户端使用ios版本的统计,3.1及以下版的用户仍然占一定比例,还不能放弃支持。因此,考虑到支持3.1及以下版本的ios设备,需要使用v1.1 TT进行开发。

开发过程中遇到的问题很多,以上是比较明显的几个,接下来聊一聊开发经验。

1、一个界面一个ViewController

在一个应用中,ViewController往往通过简单的配置就可以复用,可以控制多个界面,但我建议ViewController不复用。复用ViewController必然导致在类中出现用于区分不同界面的逻辑,如果界面上的逻辑稍有变动,这个被复用的Controller要跟着修改,随着发展,代码会越来越复杂,因此,保证一个界面一个ViewController。

对于那些确实可以复用的逻辑,可以采用继承的方式。把可以的复用逻辑封装在一个类中,每一个直接控制界面的ViewController继承自这个父类,针对各自的个性逻辑重写相应的方法。

2、不过分使用URL

之前我们提到多次通过URL控制ViewController,Controller中的参数也可以通过URL传进去,但过度使用URL构造Controller可能会埋下隐患。URL不仅是初始化的时候使用,在运行过程中可能还需要使用这个URL在池中取出该对象。如果在URL中定义了多个参数,在获取对象的过程中,必须拿到这些参数值才能准确定位到相应对象,往往这些参数都不是全局的,所以这个过程就会非常麻烦。

因此,在某些非终端的类中,尽量不使用URL构造对象,需要传递的参数使用ApplyQuery的方式,使用一个字典构造Query,使用URLAction构造对象。

3、封装两个网络控件,带缓存/不带缓存。

TT封装的网络控件叫TTURLRequest,在TTURLRequest中允许使用缓存,默认缓存1天。在应用中,有些请求要求实时性,不允许使用缓存,尤其是一些写操作的请求。

应用本身也要对TTURLRequest进行一层封装,就是之前提到过的Manager,在NetworkManager中封装两个方法,使用缓存/不缓存。

4、使用延迟加载操作

所谓延迟操作,指的是在某个界面上加载某个组件的时候,如果直接调用addSubview方法可能会出现加载失败等诡异问题。是由于iPhone渲染一个界面需要时间,加载自己的组件需要在渲染界面之后,而调用viewLoad,甚至是viewDidLoad方法在TT框架下不能保证在渲染完成之后。因此在加载个人组件时,可能需要延迟加载,即,延迟0.3或0.5秒后再加载。TT框架本身也采用了许多延迟加载,使用[NSTimer scheduledTimerWithTimeInterval:(NSTimeInterval) invocation:<#(NSInvocation *)#> repeats:<#(BOOL)#>]方法。

 

以上介绍了8个经验和教训,希望能对大家在使用TT进行开发过程中提供一些帮助,少走弯路。