现在的位置: 首页 > 综合 > 正文

移动互联网服务客户端开发技巧

2013年10月01日 ⁄ 综合 ⁄ 共 12929字 ⁄ 字号 评论关闭

 

(一)

 

随着微博、LBS 等移动互联网服务的火爆普及,越来越多的苹果开发者希望制作各网络服务的 iPhone 客户端。CocoaChina 会员 “qdvictory” 的《基于网络的客户端开发技巧》系列文章详细介绍了 iPhone 客户端应用的开发思路和注意事项,希望对苹果开发者们有所帮助。

    一、做一个程序之前,要明确你要做的是什么。这之间道道不少,不仅仅要实现功能,还要考虑到程序功能之间是如何链接,用户如何交互。在这个基础上,才能和美工一起配合,把程序的设计做好,而仅仅的 UI,已经不能称得上是完整的设计了。利用 API 扩展是必需的,Open API 不是你自家的 API,所以肯定是有局限性的,如何将 API 与产品良好的结合,是重点,抓细节,是重点。每条微博就是一个少量信息,所以客户端要做的就是快,清楚,友好,稳定。

  • 快,不用说,加载速度快,浏览的速度快。
  • 清楚,UI 布局的可识别性,一个页面,不同的排版,字体的颜色,都将影响到用户看这个页面的速度。
  • 友好,不需要太炫的效果,也不需要很酷的动画,就算再简单的 UI,也足以满足用户的需求,就看你如何做。不要烂加,有必要再加。
  • 稳定,这个要好好说说了。客户端,毕竟不是单方面数据了,很有可能客户端缓存与数据库信息不统一,或者网络问题导致接收数据出错,这些都是需要做处理,多 测试,否则 crash 很杯具。前几天同事给我看 Weico 在官方清空了短信息,然后再回来 weico 客户端,直接导致 weico 持续 crash,必须重装,不知目前修正没有。总得来说,多测试,尤其是在数据差异的方面。   

    二、就目前而言,主流的微博特效就不再多说了,以后有可能细讲,基本上都有开源的了。

    三、目前大多展示都是用的 tableview。这里就要多说说了。tableview 的使用方式有好多,下拉刷新,即可以将展示放到 tableview 的 subview 上,也可以放到 tableview cell 上,这 2 种有啥区别呢?目前没有发现实质上的区别,只是在计算的时候放在外面比较好算。
正常来说,展示的步骤为:

  • 访问api,获取数据
  • 将数据存入数据
  • 在 tableview 上展示数据,并载入相关的默认图片。(像新浪微博客户端是用的 Webview 来展示的图片,这个又可以叨叨一阵了,下次再说。。。)
  • 自己封装也好,用线程也罢,将下载的图片保存到本地,并刷新 tableview cell。这里又要注意2点:
        1、保存到本地的文件名,像这种公共数据,建议存放在同一文件夹中,像头像,可以以用户 id 为文件名存放,这样做多账号的时候可以直接利用到。关于与当前账号 有关的数据存于单独文件夹(例如以 dir_ID 为名的文件夹)。以当前账号为文件名我认为是不太安全的。像 weico,上次无意中发现,他的缓存都是用我的登录名,不知最新版本修改了没有。
        2、更新 cell 的时候,有不少方法是利用 path 和 tag 来确定修改哪个 imageview,button,但是当 tag 被占用时就相当郁闷,这时就要继承一个子类。所以我比较建议直接用 tableview cell 的 reload cell 方法来刷新,这样还可以刷新行高之类,相当有用。
  • 微博一般都是多行,sizeWithFont 确实比较慢,我能做的也只有在获取了数据之后把所有的行高算出来保存,不知有没有更好的方法会效率高一些。

    四、特色功能。想做出与众不同的产品,目前在我看来,只有特色功能。像微博的展示,lbs的签到,再怎么做,也已经大体定型,就算你做的再认真,也是别的程序换一套皮肤,在我看来,投入成本还是换一个方面的好。想一些实用的功能,扩展一下,在完成基本功能的同时,可以给用户一个使用自己程序的理由。

    五、照片处理特效。现在基本都是用开源库,主流的开源库不说了,基本都是 C、C++的,只要能编译成功,基本没啥使用难度了。问题在于处理图片的效率上。

    六、安全性。像我们公司为了加强用户密码的安全性,特意买了SSL,为的就是让用户的账号更安全。而 Open API 使得我们没法控制,所以最好使用 oauth 认证,以及不要将用户的账号暴露在缓存之中,这些都会增加风险。

 

 

(二)Webview及正则

 

 

 随着微博、LBS 等移动互联网服务的火爆普及,越来越多的苹果开发者希望制作各网络服务的 iPhone 客户端。CocoaChina 会员 “qdvictory” 的《基于网络的客户端开发技巧》系列文章详细介绍了 iPhone 客户端应用的开发思路和注意事项,继第一章以后,今天发布第二章:Webview及正则

    目前微博客户端基本都使用Webview,这篇就详细讲解一下使用利用Webview展示微博的技巧。当然,主要以代码为主。Demo暂时就不提供了。之前也有几篇博文提到相关的,这里简单总结一下。

    为什么要用webview来展示呢,就我目前的经验来看,为了文字链接图片混排比较方便。所以既然要混排,又不能让人觉得这是webview,首先就要禁止bounces,也就是边缘滚动。方法如下:

[(UIScrollView *)[[webview subviews] objectAtIndex:0] setBounces:NO];

    webview的各种复制,全选禁止方法:

document.documentElement.style.webkitTouchCallout = “none”; //禁止弹出菜单

document.documentElement.style.webkitUserSelect = “none”;//禁止选中

    具体代码可以看下

http://www.minroad.com/?p=275

http://www.minroad.com/?p=266

    webview使用起来注意的问题有:

  1. loadhtml相当慢,所以要做到良好的展示效果,想办法预加载吧。当然用JS也可以。
  2. 使用js的时候千万要外部导入js,不然会调试到头大都调试不好。。各种转义字符
  3. 对于iPhone4,记得图片尺寸要双倍。
  4. webview的html里面尽量用/”而不是’ 比如<img width=/”20/”>而不是<img width=’20′>

    下面说一下正则,将链接替换的方法,用的RegexKitLite:

- (NSString *)replacePersonLink:(NSString *)_str

{

NSString *regexString       = @"@//b(//w+)//b";

NSString *replaceWithString = @"<a href=/"at:$1/">@$1</a>";

NSString *replacedString    = NULL;

replacedString = [_str stringByReplacingOccurrencesOfRegex:regexString withString:replaceWithString];

NSLog(@"%@",replacedString);

return replacedString;

}

    提取链接的方法:

NSString *linkString = [NSString stringWithFormat:@"%@ %@",[dic getWBText],[dic getWBRetweetedText]];

NSString *regexString  = @"//bhttps?://[a-zA-Z0-9//-.]+(?::(//d+))?(?:(?:/[a-zA-Z0-9//-._?,'+//&%$=~*!():@////]*)+)?";

NSArray *splitArray = [linkString componentsMatchedByRegex:regexString];

    解析一个字符串的方法

NSString *regexString  = @"<a href=/"([^>]+)/" rel=/"([^>]+)/">([^>]+)</a>";

NSDictionary *urlDictionary = [searchString dictionaryByMatchingRegex:regexString

withKeysAndCaptures:@"url", 1, @"followed",2,@"source",3, NULL];

if (![[urlDictionary allKeys] containsObject:@"source"]) {

NSString *regexString  = @"<a href=/"/" rel=/"([^>]+)/">([^>]+)</a>";

urlDictionary = [searchString dictionaryByMatchingRegex:regexString

withKeysAndCaptures:@"followed",1,@"source",2, NULL];

}

    source string为”<a href=/”http://t.sina.com.cn/” rel=/”nofollow/”>新浪微博</a>” 

    利用这些,基本可以完成微博上的需求。同时可以利用JS实现渐近渐现等效果,用CSS将布局处理的更合理。不会用CSS就td吧。

 

 

(三)上下拖动

 

 这次来说说像 Reeder 那样上下拖动切换的效果是如何做的。如何触发这里就不多说了,靠 contentofset 也可以,按钮也可以。这里主要介绍一下这种效果的3种做法,各有好处。

    第一种:如上图,提前预加载好 3 个 view,比如往下翻的时候,就将 view C 移到 view B,view B 移到 view A,,view A 重新加载。这样做的好处就是展示的效果比较好,可控性高。代码见 http://www.minroad.com/?p=286

    第二种方法:利用 tableview 来做。在 tableview 的一层中套入一个 view,同样预加载 view A B C,每一次都只要把要显示 view 加入到 tableview 中的 view 上,并利用 reload cell 来刷新一下。这种做的好处就是简单,对代码水平要求不高,但是与第一次效果相比略显粗糙,可扩展性不强。比如要将下拖的时候,将 cell2 中的 view B 删除,加入 view C,然后调用

NSIndexPath *path = [NSIndexPath indexPathForRow:1 inSection:0]; //指向cell2的path
[self.tableview reloadRowsAtIndexPaths:[NSArray arrayWithObject:path] withRowAnimation:
UITableViewRowAnimationBottom];//刷新

    第三种方法:同样是 tableview,但是这次不需要预加载,优点就是对于 tableview 相当实用,处理也比较简单;缺点同方法二。比如要将 cell1 cell2 cell3 向前滚动,用 cell4,cell5,cell6 代替。可以执行以下代码

[self.tableview beginUpdates];
NSIndexPath *path1 = [NSIndexPath indexPathForRow:0 inSection:0];
NSIndexPath *path2 = [NSIndexPath indexPathForRow:1 inSection:0];
NSIndexPath *path3 = [NSIndexPath indexPathForRow:2 inSection:0];
//更新tableview delegate中的数据,cell数量等于删除之后的数量相同,否则会出错
[self.tableview deleteRowsAtIndexPaths:[NSArray arrayWithObjects:path1,path2,path3,nil] withRowAnimation:UITableViewRowAnimationTop];
//更新tableview delegate datasource,cell的数量是添加之后的,同时数据是新的
[self.tableview insertRowsAtIndexPaths:[NSArray arrayWithObjects:path1,path2,path3,nil] withRowAnimation:UITableViewRowAnimationTop];
[self.tableview endUpdates];

    这样相当于把原来该是cell4,cell5,cell6中的数据从下向上到cell1,cell2,cell3的位置。

 

(四)多帐号

 

多账号无非就是可以让你登录 N 个账号,我个人比较倾向于将个人信息存在 nsuserdefault 中,所以先讲讲我 nsuerdefault 中的结构。

    nsuserdefault 中,nsuserdefault->accounts(nsarray)->任一 account(nsdictionary)->key:username,userpwd,userallinfo(获取用户信息时所返回的数据,主要保存 ID,name 等)

    nsuserdefault->current account(nsdictionary)表示当前使用的账号

再说说缓存

    就像之前所说,公共信息可以放在同一个文件夹,像个人信息就放入以 id 为标识的文件夹中方便存储。比如我现在的账号名为 aaa,密码 bbb,用户 ID 为 1001,那么必然要选择用户 id 来命名文件夹名,当然也可以选用表的,选择原则是无重复,不私密。

    比如我的 documents 里面的目录结构 documents-> public(存用户头像,图片) dir_1001(ID 为 1001 的缓存信息),dir_1002(ID 为 1002 的缓存信息)

最后再说说如何处理多账号

    切换账号不必多说了,根据UI设定来搞吧。这里主要提几个比较需要注意的地方:

  1. api 的封装,最外层一定是错误处理,否则你的用户名在非本客户端修改之后,必然会出问题。增加错误检测的好处还有可以明确知道是哪的问题,不要以为只有开发者需要,用户也同样需要。
  2. 在写程序之前就要提前划分好哪一些是公共数据,哪一些是个人账号私有数据,提前封装好,这样不管是修改,还是以后作扩展,都会快很多。
  3. 缓存分账户存放是有一个好处的,不必每次启动程序都去检测密码,每次调用API的时候有前端错误检测,所以可以完全放心由于api引起的 crash问题。同时每个账号进入的时候都是会有缓存信息的,不会太空,既然你的密码已经不一致。坏处嘛,就是你就算修改了密码,还是能看到之前缓存过的数据。

    总之一句话,多账户主要就是信息存储,存储的明确了,做起来那是相当容易。

 

(五)细节

 

1、本来这篇名字想叫“一根线”的,但是发现要说的细节还是比较多的,所以改了下名。我要说的这一根线,当然就是tableview的cell分隔线。相当现在iPhone应用,cell的默认分隔线已经满足不了多数开发人员的要求了,所以制作分隔线再所难免。那么,这根分隔线如何去做呢?

    至少我相信,基本很少人会用画的方式把线画到 Cell 上,多数还是用图片。那么图片怎么做,将是非常值得讨论的。把线合到背景上,虽然可以解决问题,但是明显是不可取的,对于现在的 iPhone 内存,也许不算什么,但是对于将来 UI 变化,实在是相当之不方便,而且效果不能说是完美。

    那么如何去处理这一根线呢,可以利用 1 像素的线与 - (UIImage *)stretchableImageWithLeftCapWidth:(NSInteger)leftCapWidth topCapHeight:(NSInteger)topCapHeight 配合拉伸实现,也可以单独做一个图片,当然这 2 种方法效率谁快我还真没做过测试。。。

    2、既然是基于网络服务的客户端,那么网络传输是必然少不了的。所以状态栏的网络状态必不可少。@property(nonatomic, getter=isNetworkActivityIndicatorVisible) BOOL networkActivityIndicatorVisible 千万不要忘记哈~同时不要太频繁的 set Yes,太频繁,你会发现状态栏的 activityIndicator是一直在闪的。

    3、UINavigationItem 的左右按钮,虽说已经给定好了位置,但是对齐还是相当必要的。这就要求图片对齐,或者用代码控制image contentoffset。同时还有一点需要注意,按下状态也是要对齐一下的,不要以为手按下去看不见就无所谓,细节真的可以决定成败。

    4、每个 cell 中的排版,间距一定需要注意,该对齐的地方一定要对齐,这样看起来才舒服。有条件最好研究一下视觉范围。

    5、每个页面加载的速度。open api 必然会有限制发挥的地方,要合理利用 api,而不要盲目的使用 api 来达到预先的效果。尽可能的节省流量,为用户节约每一分钱。

    6、比较杯具的 iPhone 4 的高清屏,想要显示清楚就必然要缩小图片在屏幕上的 size,这方面只能将就了。

    7、在 textview 中不管是转发,还是 @ 以及 ##,记得尽量处于用户的角度想一下,移动好光标的位置。移动方法是修改textview的@property(nonatomic) NSRange selectedRange

    8、在手势冲突的时候,设置好优先级。

    9、使用尽量简单,尽量隐蔽的动画,来达到最好的效果。

    10、没有摄像头的设备,记得不要弹出来自摄像头。。。

    11、textfield/textview 的键盘类型,返回键类型

    12、注重细节并不是意味着到处加效果。

    13、程序员也是设计师,也有义务和美工一起搞好 UI。要重构的时候必须重构,就算你把制作程序当成工作,当成赚钱的手段,那么你也必须要和美工一起达到目前能达到的最好效果,这是对你工作的交代。当然,我一直认为这样的人不适合做一名程序员,至少我一直认为我从事的是我的兴趣,我完成的是我的创作。

    14、activityindicator 如果是 IB 拖出来,一直是转动状态,而代码只是控制显示和隐藏,记得测试后台返回的时候 activityindicator 是否还在转。

    15、尽量避免出现由于加载而导致用户无法操作的情况。

    16、整体风格的统一。

    17、使用gps时,需要的时候再打开,用完关闭。

    18、字体大小特别要注意,可以设有几套字号,但是同类微博的展示位置的字号要统一。

 

(六)透过现象看本质

 

想说这一问题好久好久了,总是想说,但是又是没啥说的。今天就算是开发技巧之中的一点点心得来说吧。

老掉牙的故事:当你难以做决定的时候就抛硬币,不是让硬币决定命运,而是在你抛出的一瞬间你就知道了你想要的。

是的,人就是这样。但是,现实,是一场没有回放的电影,每个人都是主角。如果你抛出的硬币一直在转,那你想要的答案是什么呢?

同样的,作为开发者,你需要明确知道自己想要的是什么。

就拿新浪微博来说事吧。一个新浪客户端,是给谁用,怎么用,以及有什么用。从最基本的说起,客户端,基本功能就是与服务器交互。这个在之前的几篇中说的已经差不多了,虽说不能靠几篇文章就能把微博做的出神入化,但是入门应该还是勉强够了的。现在主要说说现象与本质。

现象——基于网络的客户端。

本质——技术?功能?营销?

每一个客户端,都有自己的独道之处。像新浪官方的客户端,它作为官方的客户端,要做到最全的功能,所以作为新浪微博控,官方客户端就必装(再没有更好的出现之前)。这也就决定了官方的客户端不能更精简,连一个功能都不能少。

而在开发的时候,目的到底是什么?

有的人,就是为了做一个新浪客户端,好了,想特色功能,最大程度的精简,最佳的用户体验,至少要提供必备的功能。然后去和各种新浪客户端比拼。

有的人,看中的是新浪的用户群体,好了,开始做新浪同步,给新浪微博带来流量,其实还是想达到双赢。

有的人,想借助微博的走势,做一些小工具,来打响自己的名气。

所以在借鉴一款产品的时候,先要认清这款产品是否是适合自己的。天天看总结,总结也是人总结的,与人有关,就与人的经历有关,并不是所有人的经历都是一样的,所以也并不是所有人的总结都是适合自己的。

想做好,难。想达到目的,更难。先认清自己的实力,优势,才能去竞争。模仿,也要找对人。

所以对于准备,或已经开始做类似客户端的朋友们,我有几条建议,当然是我经过没多久的工作经验,以及极少的经历总结出来的,有不好的地方还请别见怪。

1.问自己一个问题,你想要什么。

2.第二个问题,如果你还剩一天的时间,你会去做什么。

前两道题,我的答案是,我想要做一个人人都喜欢用的客户端。第二个问题是,我会用剩下所有的时间去做这个客户端。

3.坚定的信心,好的产品,不需要朝三暮四。

4.要有取舍。冲准了一个点钻,远比你哪个点都要做强的多。

5.善于自己总结。别人的总结,总是会有个人色彩在里面,借鉴可以,完全听取,注定了你将在几十亿人口中找与自己相同经历的人。要是有数据分析,你不看,那么你就是在等和你一样经历的人找你。

6.不要小看任何对手。也不要神化对手。

7.技术永远都不是最大的难题,难题在于你如何使用技术。

8.时间永远是最宝贵的,不要浪费。

9.正确的认识gui designer,叫美工不适合,叫美术也不适合,叫设计还是不适合,反正记住一点,设计,并不是只是会画画的,程序一样是设计。

10.在评价别人的作品之前,要给别人足够的尊重。这样,才能与人形成良好的交流,三人行,必有我师啊。

11.保持良好的状态,注意身体健康,佣有足够的睡眠,拼,要拼的有技巧,耗,是解决不了问题滴。。

12.不管是个人还是公司,一个人作战真的很累,你需要共同拼搏的伙伴。

13.平日里要做的3件事:学习,学习,还是学习。

 

 

 

 

(七)后台上传

 

这里说的后台上传当然不是真的后台上传,只是在开启程序的情况下不影响操作的上传。基本的思想就是开启一个线程,不断的处理上传操作。这里用队列比较方便一些。

主要做以下几步:

第一步:实现一个自定义类,继承NSObject

@interface MinroadOperation : NSObject {
NSOperationQueue *operationQueue;
}
- (void)addOperation:(NSDictionary *)_dic;
- (void)reStartOperation:(NSDictionary *)_dic;
@property (retain,nonatomic) NSOperationQueue *operationQueue;
@end
@implementation MinroadOperation
@synthesize operationQueue;
- (id)init
{
self = [super init];
if (self) {
self.operationQueue = [[NSOperationQueue alloc] init];
[self.operationQueue setMaxConcurrentOperationCount:2];//设置同时进行的线程数量,建议为2。
}
return self;
}

- (void)addOperation:(NSDictionary *)_dic
{
//保存上传列表到nsuserdefaults
NSUserDefaults *userDefault = [NSUserDefaults standardUserDefaults];
if ([userDefault valueForKey:@"upload"] == nil) {
NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithObjectsAndKeys:[NSMutableArray arrayWithObject:_dic],@"upload",nil];
[userDefault registerDefaults:dic];
[userDefault synchronize];
}
else {
NSArray *array = [userDefault valueForKey:@"upload"];
NSMutableArray *arr = [[NSMutableArray alloc] initWithArray:array];
[arr addObject:_dic];
[userDefault setObject:arr forKey:@"upload"];
[arr release];
[userDefault synchronize];
}

//添加到队列
NSInvocationOperation* theOp = [[NSInvocationOperation alloc] initWithTarget:self
selector:@selector(myTaskMethod:) object:_dic];

[self.operationQueue addOperation:theOp];
[theOp release];

//更新UI
}

- (void)reStartOperation:(NSDictionary *)_dic
{
//从userdefaults读取数据,并添加到队列
NSUserDefaults *userDefault = [NSUserDefaults standardUserDefaults];
NSArray *array = [userDefault valueForKey:@"upload"];
NSMutableArray *arr = [[NSMutableArray alloc] initWithArray:array];
int i = (int)[arr indexOfObject:_dic];
if (i == NSNotFound) {
return;
}
NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithDictionary:[arr objectAtIndex:i]];
[dic setValue:[NSNumber numberWithInt:0] forKey:@"state"];//0 等待上传 1 上传中 2 失败
[arr replaceObjectAtIndex:i withObject:dic];
[userDefault setValue:arr forKey:@"upload"];
[userDefault synchronize];

NSInvocationOperation* theOp = [[NSInvocationOperation alloc] initWithTarget:self
selector:@selector(myTaskMethod:) object:dic];

[self.operationQueue addOperation:theOp];
[theOp release];

//更新UI
}

- (void)myTaskMethod:(id)_obj
{
//实现上传方法
//成功调用
[self performSelectorOnMainThread:@selector(taskMethodDidFinish:) withObject:nil waitUntilDone:YES];
//失败调用
[self performSelectorOnMainThread:@selector(taskMethodDidFailed:) withObject:nil waitUntilDone:YES];
}

- (void)taskMethodDidFailed:(id)_obj
{
//失败的任务更改状态之后保存
NSDictionary *tdic = [_obj retain];

NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithDictionary:tdic];
[dic setValue:[NSNumber numberWithInt:1] forKey:@"state"];

NSUserDefaults *userDefault = [NSUserDefaults standardUserDefaults];
NSArray *array = [userDefault valueForKey:@"upload"];
NSMutableArray *arr = [[NSMutableArray alloc] initWithArray:array];
int _index = (int)[arr indexOfObject:dic];
if (_index != NSNotFound) {
NSMutableDictionary *tmp = [NSMutableDictionary dictionaryWithDictionary:[arr objectAtIndex:_index]];
[tmp setValue:[NSNumber numberWithInt:2] forKey:@"state"];//0 等待上传 1 上传中 2 失败
[arr replaceObjectAtIndex:_index withObject:tmp];
}
[userDefault setObject:arr forKey:@"upload"];
[arr release];
[userDefault synchronize];

//更新UI
}

- (void)taskMethodDidFinish:(id)_obj
{
//成功的任务从userdefaults中删除
NSDictionary *tdic = [_obj retain];

NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithDictionary:tdic];
[dic setValue:[NSNumber numberWithInt:1] forKey:@"state"];

NSUserDefaults *userDefault = [NSUserDefaults standardUserDefaults];
NSArray *array = [userDefault valueForKey:@"upload"];
NSMutableArray *arr = [[NSMutableArray alloc] initWithArray:array];
int _index = (int)[arr indexOfObject:dic];
if (_index != NSNotFound) {
[arr removeObjectAtIndex:_index];
}
[userDefault setObject:arr forKey:@"upload"];
[arr release];
[userDefault synchronize];

//更新UI
}

- (void)dealloc
{
[operationQueue release];
[super dealloc];
}
@end

    第二步:在appdelegate中初始化一个MinroadOperation对象,添加的时候用addOperation(),重新开始的时候用reStartOperation()。

    第三步:在程序重新开启的时候启动队列。同时有一点不确认,就是从后台返回貌似队列会继续进行,也就是说进入后台的时候队列处在等待状态。

    需要注意的几点:

  1. NSInvocationOperation的cancel 方法只是设置一个标识,在进入队列的时候判断是否为 true,从而判断是否进行。如果想取消 NSInvocationOperation,可以尝试继承它。
  2. 避免无限重试。

 

 

 

抱歉!评论已关闭.