事先声明:

  1. 能用傻瓜式的API还是去用傻瓜式吧,MediaPlayerFramework基本都能满足正常的音频播放需求。本文所涉及的内容用这些是实现不了的。

  2. 如果你指望本文是一个关于标题中提到的东西的教程,那我建议你还是关掉这个标签去翻文档吧,我看过的几个市面上容量仅有一篇博文那么多的关于这些方面的教程都没有什么实际意义,真的想学就踏踏实实去看文档,没有比文档对这方面讲的更清楚的了。除了文档建议补一下WWDC 2010 2011的相关Session,此外就是几个官方Sample。遇到问题了尤其是设计底层的API时,基本只有一个地方回答的比较靠谱,就是Apple Mailing List,另外StackOverflow也不错。本文仅仅回提供一些思路和链接,给后来的人指一个大概的方向。

以下进入正文:

记得小时候mp3刚开始流行那阵,那时候WINAMP称霸一时,虽然当时对音乐还没有什么感觉,但对WINAMP自带的众多可视化效果甚是着迷,直到今天终于有能力自己实现一个了,做了个Demo录了个视频发出来,效果较挫,嫌弃不好的就当听歌了,原曲来自niconico,sm18028378。youku视频如下:(也有u2b新浪

想实现视频中的效果有一种简单的方法和一种困难的方法

先说简单的:首先你要有音频的源文件(这不是废话么),压缩非压缩均可,格式要在iOS设备支持的范围内,另外就是对AudioQueue工作方式比较熟悉,如果你能成功用AudioQueue播放音频的话基本就成功一半了,其实从一定程度上来说AudioQueue也算是半傻瓜式的API,接下来就是获取level meter,这里提示点关键东西,自己看下就明白了:

  • AudioQueueGetProperty, kAudioQueueProperty_CurrentLevelMeterDB
  • Sample Code, Speak Here,MeterTable.h

这样就可以在每次OpenGL刷新的时候获取level meter值,
关于最后的OpenGL展示集中在后面讲

比较困难的方法就是用Audio Unit了,说困难其实不是API多么难用,而是越到底层我们做的工作就越多,真的要实现一个播放器的话我们就要人工处理音乐播放器的各种逻辑,时间控制,播放列表控制等等,实现功能以后还有一段很长的路要走。

进入正题,要求音频文件最好是liner PCM,也就是无损无压缩的,因为Audio Unit只能接受liner PCM的数据,如果是压缩格式还需要转换,转换格式可能就需要用到ExtAudioFile,或者AudioFile+AudioConverter,前者比较方便,后者相当复杂,尤其是要考虑对各种格式的兼容问题,需要很多判断,很多分支,对音频格式的知识要求也相当高,参考Sample Code,ConvertFile,这是一个控制台应用,代码很多,耐心看。如果格式的问题回避或解决了,就可以进入AudioUnit部分了,这部分其实也不难,基本就是设置一大堆属性,然后在回调函数里面提供数据就可以了,看官方文档会对你有很大帮助。至于获取level meter,可以让音频数据通过一次MultiChannleMixer Unit(这个AudioUnit的使用可以参考下Sample Code,Mixer Host),不过我们只用一个输入口,不真正mix,只是为了借用其kMultiChannelMixerParam_PostAveragePower,获取和kAudioQueueProperty_CurrentLevelMeterDB相同的东西,然后沿用MeterTable.h,转换成更直观的数据,最后显示出来。

如果做更复杂显示效果,可能就需要fft了,也就是将时域转换成频域,根据频率数据来实现比如做那种频谱图(Spectrometer),可参考的资料:

FFT部分:

  • 理论基础,随便找个教程看吧,这方面有很多
  • 实际一点的看下Create a FFT Analyzer这个系列的前半部分,这个系列从头到尾讲了下Mac上一个显示频谱的AudioUnit的实现,但是iOS目前(5.x.x)并不支持自制的Audio Unit,所以即使你照着这个教程做出来东西也不能用到iOS平台上,不过对实现过程还是能有一个比较清晰的认识的。另外该教程也对学习路线有一明确指引。

AudioUnit部分:

  • Sample Code, AurioTouch2, Aurio Touch;前者用了AccelerateFramework,这个Framework里面有个硬件加速版的FFT,以及一些iOS 5才出现的API,但仅仅FFT部分还是能支持到iOS 4的,至于后者是纯用CPU算FFT,可以兼容iOS 4之前的系统。

接下来是显示部分:

这部分实在没什么可说的,对OpenGL ES我还是个小白,现在只是用了一个现成的Particle System,在每次draw view之前根据level meter改变了下各点的尺寸,然后就没有然手了。我也跪求高手指导如何才能做出漂亮的效果。

最后说下我的学习路线:

  1. WWDC 2010 2011关于Audio的所有Session,对Audio部分有一个直观的认识,
  2. 官方文档,从Multimedia Programming Guide开始,所有关于Audio的分支文档,从表层到底层都有个了解,明确实现某种功能需要从哪一层入手,这部分内容很多,拖拖拉拉看至少4天吧。
  3. 期间参杂之前提到的所有Sample Code,文章,以及StackOverflow,Mailing list中的各种问题。

其实有一些关于CoreAudio的书也不错,但是太大部头了,短期纯为了实现功能看书太慢了。

本文Demo的代码我就不放出来了,没有太大价值,只能起到急功近利的效果,对学习没有什么帮助,另外就是代码都是拼凑了,结构混乱,还是不要拿出来让人喷了。

PS.题外话,关于Mac向Youku,新浪传视频,Mac下的转码软件基本都是个悲剧,前面用的视频是用Video Converter-Clone2Go这个转换的,这个软件虽然UI非常悲剧,但至少能设置下音频,视频的bit率,这对于要保证音频质量来说是必不可少的(youku没爆音真是很幸运)。另外就是对比新浪和youku,新浪的审核转码都比youku快不少,音质也明显比youku高,至于u2b各方面都非常好,也就是对于我们这些国内的人上传超费劲。

祝各位玩得愉快。

–以上–

Tagged with:  

WWDC 2012 小结

On 06/13/2012, in Apple, Development, by ultragtx

低清晰度的keynote刚看完,高清1080p那个7G下了个开头就放弃了,作为一个iOS开发者简要谈下体会。

Retina MacBook Pro

之前就预测过Retina Mac几乎没有可能出现,理由就是市面上所有显卡包括台式机显卡,支持的单屏最高分辨率为2560×1600,也就是13寸MBP分辨率的Retina版,15寸的2880×1800想都不要想。可惜失算了,不知道什么时候nVidia的GTX 600M系列最高分辨率居然支持到了3840×2160,也就是Full HD的Retina版,更奇葩的是nVidia的台式机最强显卡最高分辨率依旧为2560×1600,因此不得不怀疑苹果跟nVidia在私下又合作了一次。另外最近又有新闻说iMac和Mac Pro要在明年有重大更新,在此也可以大胆预测一下,21.5寸iMac(1920×1080)上Retina从目前技术角度上来看是可能的,27寸(2560×1440)没戏,最终能否实现主要靠那两家做显卡的。

还有一点疑虑就是Reina MBP集成个Intel的显卡到底有什么用,分辨率的问题导致其不能驱动主屏幕,双屏或三屏的时候正常也是靠独显支持的,希望有条件的人去拿gfxCardStatus切换下试试,如果真的支持2880的分辨率,上面那一段就当我没说把。
如果Intel的显卡不能驱动主屏幕,那也可以看出Air和13寸MBP在以后的一两代内也不可能支持Retina,除非苹果给上独显,就算是Intel的下一代集成显卡支持高分辨率,性能上估计也很难达到要求。

Moutain Lion

Lion如其名可以算是我用过的最烂的操作系统,烂在慢,烂在各种Bug,跟SL比起来简直不在一个层面上,相信SL用户都能感受出来,都到了第四个版本了,虽然修复了很多问题,但是遗留的东西依旧不少。个人感觉,之所以没有太多人喷,是因为对于不少人来说Lion是他们用过的第一个Mac操作系统,另外Air的SSD也很大程度上掩盖了慢的缺点。综合Lion的悲剧表现我对于ML就比较期待,但以苹果最近的情况来看实在不赶抱太大希望,至于介绍的那几个新功能真正有很大用处的就是那个Power Nap了。 Game Center以但前iOS平台的游戏水准来说就是个笑话,那几个iOS游戏也就能在手机平台拿来耍耍,桌面系统玩这种级别的东西不感觉可悲么。至于未来能有什么发展就不是我等屁民可以预测的了。iCloud等附属的一系列软件还是不错的,对于用户来说只需设置个帐号,其他工作都是自动完成的,透明性非常好,但这些难道不应该在Lion中就附带么,由此可见Lion就是一个纯粹的过渡产品。

iOS 6

从升级幅度上看,iOS 6也就是iOS 5.3-5.4这个级别的,估计Bug不能多,所以很放心地升级了,主要功能新闻里都提了,我就主要谈一下他们没涉及的部分吧。

系统的整体UI有一定变化,Navigation Bar和Tool Bar的渐变方式有了改变,不算好看也不算难看,换个样子总还是有些新意的。

系统自带软件比如两个商店UI大幅改变,更美观了,自带的TODO也多了一些内容,值得一提的是Music/iPod,UI向iPad版靠拢,Cover Flow流畅了许多。

新地图对于中国区来说简直是个悲剧,AutoNavi的数据从各方面都不及Google Map,而且没有国外数据,卫星模式下全世界只有一个国家以及一个大大的红五星边上写着帝都。想用TomTom的数据对于iPhone来说也很麻烦,关键就是要开启飞行模式防止基站定位,另外一个国外的ip可能也是必要的,如果还是不行就用XCode模拟个米国地址调试一下。3D地图没条件测试,从keynote里的演示看还是很赞的,

Safai的分享按钮点击后弹出来一堆图标我就忍了,“添加到阅读列表”这个选项居然到了第二页这不是有病么?

设置>>开发者 这里多了一些内容,其中Network Link Conditioner真是相见恨晚啊,使用这个功能就可以模拟多种网络情况比如100%丢包等等,前一阵子还考虑要不要写一个这方面的程序来方便调试,iOS 6就实现了。

总结:Mac和iOS融合得好了,苹果的生态系统也更加完善了。Google和微软未来的道路又会如何呢?Google的眼镜能带来怎样的变革呢?至于iOS 6,Moutain Lion到底能带来多大变化还要看WWDC的Session才知道。

ps. 一开头Siri同学将用那蛋疼的发音讲了几个冷笑话,我是多么希望能换成GLaDOS啊!

–以上–

Tagged with:  

关于C++和Objective-C混编

On 05/23/2012, in Development, iOS Dev, Mac Dev, by ultragtx

Objective-C在大部分情况下足够满足我们的需求,但是还是会有一些情况必须要使用C++,比如:

  1. 使用C++的库
  2. 当Objective-C不够快的时候

第一点自然不必多说,至于第二点,Objective-C的消息机制比起函数调用还是比较慢的,当对性能有极高要求的时候,就需要C/C++来替代。

C++与Objective-C混编只要注意将包含C++代码的.m文件改为.mm即可,XCode就会自动判断该使用何种编译器来编译。

这看起来简单,但实际操作中还是很容易出现令人费解的编译问题,比如最经典的"Unknow type name ‘class’; did you mean ‘Class’?“。究其原因就是我们没有遵守这两条规则:

  • .m文件不能含有C++代码
  • .m文件所import或include的.h文件中不能直接或间接包含C++代码

在保证你的C++代码是正确前提下,如果发现相关编译错误可以通过如下几个方法修复

  1. 将.h文件中的c++代码转移到其他地方
  2. 阻段include或import链
  3. 将相关的.m文件后缀改成.mm

显然第三种方法相对于前两种实施起来更方便,但如果你使用XCode 4以及之后的版本所包含的模板建立项目的话有可能会忽视一个问题:

当你在AppDelegate.h中include或import一个C++的头文件时,当然你一定不会忘记修改AppDelegate.m为AppDelegate.mm,如果只做了这点儿还不够,我们还忽略了隐藏在Supporting Files组内的一个文件的存在–main.m

在以前的XCode模板中main.m默认是这样的

1
2
3
4
5
6
7
8
9
#import <UIKit/UIKit.h>;
 
int main(int argc, char *argv[]) {
 
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    int retVal = UIApplicationMain(argc, argv, nil, nil);
    [pool release];
    return retVal;
}

而新的模板是这样的:

1
2
3
4
5
6
7
8
9
10
#import <UIKit/UIKit.h>;
 
#import "AppDelegate.h"
 
int main(int argc, char *argv[])
{
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

看出问题来了吧,main.m新增了#import “AppDelegate.h",这就导致了main.m作为一个Objective-C源文件却引入了C++代码,而Xocde会使用Objective-C的编译器进行编译,从而产生编译错误,因此我们还需要将main.m的后缀改成.mm。

总结+吐槽:我在发现main.m这个问题之前对编译错误纠结了好久,一度修改代码,也一度怀疑是XCode本身的问题(XCode 4出奇的不稳定让人不得不对其产生质疑,虽然这次是我错了),希望同样使用Objective-C++的同学不要像我一样栽在main.m上。

–以上–

Tagged with:  

GSBookShelf是我参照iBooks的操控实现的一个书架界面(或者说是控件)。项目放在github上了,大家可以去参考。

由于前两天GSBookShelf被Cocoa Controls收录了(不知道谁给我提交上去了,或者他们编辑自己发现的),而且还获得了Control of the week称号,所以有必要提供一个github之外的反馈平台吧。

演示:

效果图(图片比较大,请耐心等待):

imageimageimage

视频演示(感谢Cocoa Controls制作):

u2b

简介:

看过上面效果图并且用过iBooks的同学应该清楚这个控件的基本功能了吧,就是实现书籍拖拽(支持拖拽的同时ScrollView滚动),批量添加,批量删除,以及这些动作的相应动画。

部分功能实现细节:

  • 整体采用了类似UITableView的cell,重用等机制
  • 在拖拽的同时ScrollView滚动是通过NSTimer每隔0.xx秒触发,让ScrollView的contentOffset加1实现的,这种方法看起来可能不太靠谱,但实测性能上没有问题,2代touch上滚动速度略慢。

其他说明:

  • 别忘了开ARC
  • demo中的“书”(bookView)是UIButton,可以替换成任意UIView,只要你别拦截长按手势。
  • 为你自定义的bookView,和cell添加GSBookView和GSBookShelfCell协议来支持重用,这个对性能很重要。
  • 超出书架拖动范围,在书架顶部之上可以添加一个自定义的UIView来实现ibooks中那个苹果图表的效果,在书架底部之下也可以添加一个自定义的UIView来显示出更多层,防止直接显示scrollview的背景的突兀。
  • 在书架顶部可以加一个自定义UIView,demo中我添加了一个UISearchView,可以用来查询
  • “书”(bookView)的尺寸是固定的,如果你希望有所变化,可以把一个透明的UIView设置成bookView,然后在这个view之上添加你的内容,但是手势识别仍旧以透明的UIView的范围为基准,这个问题主要是为了权衡bookView的可定制性,以及实现的难度所做出的妥协。
  • 横屏的图片我没做,原因一是比较懒,二是如果你用这个控件应该会自己作图,三是考虑到下面TODO中提到的内容。

TODO:

  • 在之前一版的demo中虽然支持了横竖屏切换,但是只是简单地reload了一次,而且跳到了书架顶部,细节不太完美,而iBooks在旋转过程中还有一些动画等细节功能,这个会在以后添加,但肯定不是最近(目前比较忙)。

总结:

感谢Cocoa Controls收录,欢迎各位批评指正,如有问题请回复或者通过各种可能的方式联系我。

PS:我无节操地在github项目那里加了个Donate,欢迎支持!

–以上–

Tagged with:  

好久没更新了,最近在鼓捣一些Core Audio的有关内容,加上各种事儿,没有时间写东西,先把最近发现的一些好东西分享出来吧。

文章

项目

Tagged with:  

iPad 3 图片解压缩测试

On 03/16/2012, in Development, iOS Dev, by ultragtx

之前翻译过Avoiding Image Decompression Sickness[iOS]如何避免图像解压缩的时间开销,现在iPad 3出了,原文也有了后续iPad 3 Image Decompression Benchmarked,所以我也就简单再翻译个后续,推荐先看一眼之前那篇,以下内容都是建立在之前的基础上的。

此次原文没有太多重要内容,挑重点简要翻译,括号内大部分是我的注释,少部分原文,很好辨别。

iPad !!!(iPad 3,这是跟k-on!!或者working!!学的么?),根据GeekBench测试,CPU几乎没有变化(因为A5X只是显卡4核心,CPU还是和iPad 2一样),根据GLBenchmark的结果,苹果对iPad 3做了很大优化,是的在Retina屏幕上的祯数几乎和iPad 2相同。

一位Australia的同学帮忙在他的iPad 3上帮我们运行了benchmark,并把测试结果发给了我们。(之前的结果参考之前的那篇文章,我就不贴图了)

我们预想iPad 3解压缩速度将是iPad 2的2-4倍,iPad 2默认的launch image需要100ms来解压缩并显示,如果在iPad 3上也有相同的速度就可以了。

测试结果如下

PNG Crushed, 1024*768 (init+decode+draw)

  • iPad 2: 5 ms + 89 ms + 18 ms = 113 ms
  • iPad 3: 1 ms + 50 ms + 18 ms = 69 ms

-47%

JPG 80%, 1024*768 (init+decode+draw)

  • iPad 2: 2 ms + 32 ms + 18 ms = 52 ms
  • iPad 3: 2 ms + 32 ms + 17 ms = 51 ms

-2%

PNG Crushed, 2048*1536 (init+decode+draw)

  • iPad 2: 5 ms + 266 ms + 96 ms = 368 ms
  • iPad 3: 2 ms + 171 ms + 66 ms = 238 ms

-33%

JPG 80%, 2048*1536 (init+decode+draw)

  • iPad 2: 1 ms + 121 ms + 69 ms = 192 ms
  • iPad 3: 1 ms + 122 ms + 66 ms = 189 ms

-2%

粗略看下我们发现iPad 3的速度并没有我们预想的那样有4倍的提升。退一步讲,iPad 3的效率的确像Apple描述的那样有2倍的提升(-50%)(A5X比A5显卡快了一倍),但这仅仅针对处理PNG Crushed的情形(-47%),在处理JPEG的时候速度仅仅提高2%,这点儿提升也许是来自SSD读取速度的提升,看起来苹果的开发人员似乎都将JPEG遗忘了。

至于"感觉上" iPad 3是不是比iPad 2快了呢?

恐怕不是,iPad 2显示launch image要113ms,iPad 3上却要消耗238ms,如果没有任何优化的话,一个应用启动要多消耗125ms(记住CPU性能是相同的)。

因此最大的问题就是,杂志类应用开发者更期望这种“感觉上”的速度提升,而实际上即使使用80%全屏大小的JPEG也只能得到5-19(128ms-52ms)的祯数,这就导致了如果在主线程绘制图片,从一个页Scroll到下一个页时就会产生一个明显的卡顿。

总结

现在我们知道为什么新的核心命名为A5X了,这仅仅是与A5相同的CPU配上了一个性能稍高点的显示核心。这个“X”我们计算出来大概为1.4,但仅仅针对PNG,对于JPEG,也许在下一个iOS更新中体现吧。

目前发生这种情况的原因很可能是这样:针对JPEG图片的硬件加速处理模块因为某种情况在iOS 5.1中被遗忘了,或者干脆没有准备好。如果事实果真如此,那么对于苹果来说这真的很尴尬。

最后一段原文打广告。大概就是建议用大图的人暂时只留下必须的小图片,使用他们做的DTCoreText来渲染富文本(蛋疼核心文本?LOL)。(要我说按苹果这么个折腾法矢量图才是王道啊!)

–以上–

Tagged with:  

Obj-C用起来真是各种happy,比如现在有这样一种情况:有一个类,我们希望它能响应一个消息(message),但是这个类没有相应的方法(method),而你又偏偏不能重写/继承这个类。这时我们可能会想到,能不能动态地给类添加一个方法呢?感谢Obj-C,仅需简单几步就能实现。

先看一段代码

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
#if TARGET_IPHONE_SIMULATOR
#import <objc/objc-runtime.h>
#else
#import <objc/runtime.h>
#import <objc/message.h>
#endif
 
@interface EmptyClass:NSObject
 
@end
 
@implementation EmptyClass
 
@end
 
void sayHello(id self, SEL _cmd) {
    NSLog(@"Hello");
}
 
- (void)addMethod {
    class_addMethod([EmptyClass class], @selector(sayHello2), (IMP)sayHello, "v@:");
 
    // Test Method
    EmptyClass *instance = [[EmptyClass alloc] init];
    [instance sayHello2];
 
    [instance release];
 
}

我们首先定义了一个EmptyClass,继承NSObject,没有任何自带方法,接着定义了一个函数。这里提一句,Obj-C的方法(method)就是一个至少需要两个参数(self,_cmd)的C函数,这个函数仅仅输出一句Hello。接下来在addMethod方法中,我们调用class_addMethod()为EmptyClass添加方法,class_addMethod()是这样定义的:

BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)

参数说明:

cls:被添加方法的类

name:可以理解为方法名,这个貌似随便起名,比如我们这里叫sayHello2

imp:实现这个方法的函数

types:一个定义该函数返回值类型和参数类型的字符串,这个具体会在后面讲

接着创建EmptyClass的实例,调用sayHello2,运行,输出Hello,添加方法成功。

接下来说一下types参数,
比如我们要添加一个这样的方法:-(int)say:(NSString *)str;
相应的实现函数就应该是这样:

1
2
3
4
int say(id self, SEL _cmd, NSString *str) {
    NSLog(@"%@", str);
    return 100;//随便返回个值
}

class_addMethod这句就应该这么写:

1
class_addMethod([EmptyClass class], @selector(say:), (IMP)say, "i@:@");

其中types参数为"i@:@“,按顺序分别表示:

i:返回值类型int,若是v则表示void

@:参数id(self)

::SEL(_cmd)

@:id(str)

这些表示方法都是定义好的(Type Encodings),关于Type Encodings的其他类型定义请参考官方文档

最后调用say:方法:

1
2
int a = [instance say:@"something"];
NSLog(@"%d", a);

输出something和100。

关于本文所涉及内容的详细信息请参考Objective-C Runtime Reference

本文参考了:

推荐去看看
—以上—

Tagged with:  

这是一篇译文,(原文"Avoiding Image Decompression Sickness"在此),原文是我看过的非常不错的一篇关于iOS图片显示的一些文章,解决了我的一些疑惑和问题,因此翻译过来分享,为保证一定的通顺性其中一部分内容与原文有些许出入,但我尽量保证了意思的一致性,欢迎指正批评,横线之间为译文,略挫,见谅:


当开始iCatalog.framework的工作时,我发现使用大尺寸图片会引起一些恼人的问题,“大”意味着这个图片有足够大的分辨率(1024×768)来覆盖iPad的整个屏幕,或者覆盖未来Retina Display iPad(如果有的话)的双倍分辨率(2048×1536)屏幕。

想像一个杂志类型的App,一个分页的UIScrollView,每页显示一个UIImageView,一旦某一页进入屏幕区域你就要为这个页创建或者重用一个UIImageView并把它放到scrollView的当前显示区域,即使这个页只有一个像素进入到屏幕区域,你还是要做这些工作。这在模拟器上运行得非常好,但在真机上进行测试,你会发现每次进入下一页时都会有一个明显的延迟。这个延迟来自于将图片从文件解压缩并渲染到屏幕上这一系列的工作。不幸的是UIImage仅在图片将要显示的时候做这个解压工作。

因为添加一个view到当前的view层次结构中必须在主线程上进行,所以图片的解压缩和之后渲染到屏幕上的工作也在主线程进行,这就是这个延迟产生的原因,这个问题也可以在store里的其他有类似这种效果的app中发现。

一般我们使用的图片有两种主要格式,jpeg和png。Apple通常推荐你使用png作为用户界面的图片格式,这些图片会被一个叫pngcrush开源的工具优化(译者注:这个工具就在/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin/pngcrush ),这样对于iOS设备就可以在显示时更快地进行解压和渲染。iPad平台上首批出现的杂志应用,比如Wired,就曾用过png作为杂志内容图片的格式,这导致了这个应用的某一版本大小超过了500MB(link)[http://www.cocoanetics.com/2010/05/saturday-morning-breakfast-wired-emag/]

虽然png格式的图片会被事先优化好,但是这并不意味着在所有情况下png都是最佳的图片格式,png对于那些app中自带的图片来说非常好,但是对于要从internet上down下来的图片来说又会怎样呢。png和jpeg这两种格式都有各自的优缺点:

png格式的图片有alpha通道,jpeg则没有。png无损压缩,jpeg允许你选择0-100%的压缩质量。如果需要alpha通道(透明),就只能用png格式。但是如果你不需要一个完美的图片,就可以使用jpeg格式,jpeg格式会忽略那些你看不到的信息,对于大部分的图片可以使用60-70%的压缩质量而不对图片造成明显的影响,对于比如文字那样有"sharp pixels"的图片就可能需要较高的压缩质量,对于照片可以使用较低的压缩质量。

来看一下一个图片的空间消耗:

  1. 磁盘空间或者通过internet传输所消耗的空间
  2. 解压缩空间,通常是长X宽X高X4字节(RGBA)
  3. 当显示在一个view中时,view本身也需要空间来存储layer

对于这里的第一个问题,有一个可能的优化方法:将压缩的文件拷贝到内存中不如映射到内存中,NSData有能力来假设一块磁盘空间是在内存中的,这样当访问这个图片时实际上就是从磁盘访问而不是从内存。据说CGImage知道哪种访问方式是最高效的,UIImage只是将CGImage封装了一下。

对于“将这些像素显示到屏幕上最快要多久?”这个问题,显示一个图片所消耗的时间由以下三个因素决定:

  1. 从磁盘上alloc/init UIImage的时间
  2. 解压缩的时间
  3. 将解压缩后的比特转换成CGContext的时间,通常需要改变尺寸,混合,抗锯齿工作。

要逐一解答各个问题,我们需要一个benchmark来测量。

测试环境和测试内容

Continue reading »

Tagged with:  

编译你自己的MobileTerminal

On 12/28/2011, in Development, iOS Dev, by ultragtx

先扯点题外话,没兴趣看的直接跳过这一段:话说iOS 5.0.1完美越狱终于出来了,虽然还有一些Bug(比如ibooks),以及一些4.x时代的越狱软件不能用,但是终归是出来了,最近还有weiphone的好心人分享4s的siri认证,就是那个Spire一直下不下来很蛋疼。再吐槽下Sbsettings,虽然放到通知栏里了,但是也太影响加载速度了吧,你就不能在viewdidappear里面加载那几个图标么(也许通知栏的控件没有这个方法,但是那个天气股票啥的都挺流畅的啊),搞得通知栏没有一回是流畅拉下来的。依赖的Activator显然已经没什么用了,就应该老实儿做个通知栏版的Sbsettings,也不能费多少劲。

吐完槽进入正题:MobileTerminal是一个写的很蛋疼的app,为什么很蛋疼,原因有很多:

  • 删除(backspace/delete)操作总是有历史字母残留,这个不知道为什么,对终端的实现不是很了解
  • 左下角那个快捷菜单不能添加ESC这种常用的快捷键
  • swipe left right up down 的手势识别率太低,就不应该用这种不好识别的手势,本来终端就是个没什么触摸交互的东西,手势操作看不到结果。
  • 等等…

这里先说下环境:Xcode 4.2,iOS SDK 5.0, 有开发者帐号证书

幸好编译MobileTerminal还算简单,编译脚本该有的都有了,不用去操心这些实在是很方便,大概步骤如下:

  • 找个目录 svn checkout http://mobileterminal.googlecode.com/svn/ mobileterminal-read-only 这个目录路径最好全英文,无空格,否则编译deb时有问题
  • check下来的东西有好几个project,真正有用的在branches/applesdk/MobileTerminal 打开这个project
  • Target选择成MobileTerminal,编译到真机
  • 编译遇到错误 Command ./build_svnversion.sh failed with exit code 1 问题,这个svnversion似乎是用来生成版本号的,我们也不需要就可以无视。打开项目配置文件,选择Targets下面的MobileTerminal,BuildPhases >> Target Dependencies里面删除svnversion(MobileTerminal)
  • 继续编译,多了一个错误:TerminalGroupView.m 文件里#import部分,找不到 Perferences/Settings.h;直接改成#import "Settings.h"
  • 继续编译,可能有签名问题,对于有开发者帐号的就直接设置好签名就OK了,没有帐号的如果之前已经做好XCode免证书调试的各种工作了这个错误应该不会出现,具体没有条件尝试,大家自行解决吧,不是大问题。
  • 继续编译,第一次遇到的错误没有了,现在在 MobileTerminalAppDelegate.m 文件里提示找不到 svnversion.h;删除这句import,修改applicationDidFinishLaunching:里面的SVN_VEFRSION为任意数字,比如999
  • 过程中还可能遇到找不到gen_entitlements.py的问题,这个估计是XCode3.x版本免证书调试遗留下来的东西(参考这里),解决方法,终端中输入如下命令(要sudo):

    mkdir /Developer/iphoneentitlements312
    cd /Developer/iphoneentitlements312
    curl -O http://www.alexwhittemore.com/iphone/gen_entitlements.txt
    mv gen_entitlements.txt gen_entitlements.py
    chmod 777 gen_entitlements.py

经过以上修改,编译就应该成功,在你的机器上就应该会出现沙盒版的MobileTerminal,运行会提示在沙盒里不能fork,这就需要将App安装到/Application/下,直接将编译好的terminal.app拖过去运行会秒退,需要编译deb包,像Cydia那样安装。

Target选择deb,编译,可能会提示缺少dpkg-deb;直接下载这个二进制文件dpkg-deb-fat(参考这里),放到/usr/local/bin/下面(或者/usr/bin/),重命名为dpkg-deb,在终端直接敲dpkg-deb命令检查以下,接着编译成功,编译好的deb在~/Library/Developer/Xcode/DerivedData/MobileTerminal-xxxxxxxxxx/Build/Products/Debug-iphoneos/ 下面,这个路径可以通过在项目Products文件夹右键点击已经编译好的Terminal.app >> Show in Finder来找到

至于Deb包的安装我就不用多说了,iFile傻瓜式还是命令行dpkg,再或者蛋疼地架个repo,方法很多了。

至于Terminal的修改我还没有研究,最近也比较忙没那闲工夫折腾,有兴趣的自己看看吧,牛人可以挑几个bug啥的上传到code上以后也可以作为炫耀的资本(开完笑),以后Cydia上的那个版本就可能时你编译的了。

PS.其实MobleTerminal可以直接在模拟器上运行,没有沙盒问题,证书问题,也方便调试

最后感谢一下为MobileTerminal作出贡献的我不认识的大牛们,虽然我之前吐了很多槽,幸好你们看不懂中文。

—以上—

Tagged with:  

Session 121

Understanding UIKit Rendering


个人感觉是WWDC 2011里比较精品的Session,强烈建议各位同学反复看几遍视频,可以了解到许多iOS的实现机制

提要
  • UIView and CALayer
  • CATransaction and when views get rendered
  • Quality and Performance

     

    • Clipping and masking
    • Edge anti-aliasing
    • Group opacity
    • Shadows

UIView and CALayer

Geometry

iPhone 4
UIKit 左上为原点,320 x 480, 横屏原点不变 CoreAnimation 左上为原点,640 x 960

iPad 2
UIKit 左上原点 768 x 1024 CoreAnimation 左下为原点 1024 x 768

关于坐标问题直接参看官方文档就可以了

Drawing
  • view.layer.contents
  • UIImageView
  • drawRect

参考Practical Drawing for iOS Developers

UIImageView 与 drawRect 对比

放大一个图片,drawRect会占用更多内存,UIImageView只占原始图片所占内存,没有额外开销 用一个基础图片(tile)填充一个更大的区域,drawRect同样占用更多内存,UIImageView会在性能与最小内存占用之间选择一个平衡点(将多个tile拼成一个大一些的tile)

CATransaction

转换在runloop结束时生效

Performance
Avoid Offscreen Rendering

“正常"的渲染都直接在当前可视区域进行 屏幕外渲染,让显卡指向一个屏幕外的内存,alloc那段内存并开始绘图,渲染结束后,显卡指向主屏幕,使用之前在屏幕外渲染来在屏幕内绘图,这个过程需要消耗额外的内存,显卡从一个buffer切换到另一个buffer也需要消耗额外的时间,还需要刷新缓冲区,所以在屏幕内的和屏幕外的buffer之间切换代价是很昂贵的。 由于CoreAnimation是逐帧渲染的,所以屏幕外的渲染将会带来极大的开销, 每次屏幕刷新都会有屏幕外的渲染发生,比如一个scrollView,每次scroll的时候,都会在屏幕内进行一些渲染,屏幕外进行一些渲染,然后又将屏幕外的绘制到屏幕内。

Layer Rasterization

这个算是offscreen rendering的一种解决方法吧,屏幕外的渲染会被缓存起来,这样就不用每次都重新绘制一下屏幕外的buffer. ([CALayer setShouldRasterize:])
使用这个特性要保证你的内容是固定不变的,因为如果是个变化的内容,那么每一帧的内容都会与前一帧不同,缓存不能被重复利用,反而影响了性能。


比如要实现图中所示效果
CuriousFrog

Clipping and Masking

这里有两个技术点,一是圆角的实现,我们可以直接用CALayer的cornerRadius来达到这个效果,虽然很方,但是效率很低。二是倒影的实现,实现倒影的方法是首先取得上半部分原始View hierarchy的一个副本,y轴翻转绘制在下面,然后根据剃度来设置mask,但是这个mask开销是很大的,一方面要对整个View hierachy进行操作,另一方面需要进行Offscreen Render。

下面的方法效率都比较低,虽然很方便 * [CALayer cornerRadius] * [CALayer mask] * [UIView clipsToBounds] 或 [CALayer masksToBounds]

一些Trick * [CALayer contentsRect] 设置需要渲染的范围,本例中并不适用。 * [UIView drawRect:] 预先渲染好来防止每一帧都进行渲染 * Transparent overlay 比如圆角就可以通过覆盖黑色来让它看起来是圆的,但是本例中还要看到圆角后面的内容,所以不采用

Group Opacity

设置Group Opacity和没设置的区别

GroupOpacity

可以通过一下几个方法实现: * 在Info.plist里设置UIViewGroupOpacity键值 这样就又会产生offscreen rendering * 在drawRect:里预先渲染 * 设置shouldRasterize为YES

Shadowed text

使用CALayer的shadow属性很方便,但是开销也很昂贵

Demo

首先给出检测offscreen rendering的方法:使用Instruments的Core Animation模板,选中Color Offscreen-Rendered Yellow就可以让所有的Offscreen rendering都染成黄色

首先使用

1
2
3
UIBezierPath *capsulePath = [UIBezierPath bezierPathWithRoundedRect:myBounds cornerRadius:myBounds.size.height / 2.0];
[[UIColor grayColor] set];
[capsulePath fill];

来绘制圆角的背景,免去setCornerRadius的开销。

接着使用

1
2
3
4
5
CGContextSetShadow(context, CGSizeMake(3.0f, 3.0f), 10.0f);
CGContextSetShadowWithColor(context, CGSizeMake(3.0f, 3.0f), 10.0f, [UIColor colorWithWhite:0 alpha:.4].CGColor);
 
[[UIColor blackColor] set];
[_labelTitle drawInRect:titleFrame withFont:[UIFont boldSystemFontOfSize:28]];

来代替setShadowOffset: 和 setShadowColor:,这里注意一点,CoreGraphics在你执行绘制的代码后立即就会进行绘制,所以要在绘制之前先把属性设置好;而Core Animation在Transaction commit之后才开始渲染,所以属性可以在之后设置。

倒影的绘制,预先绘制成一个图片来防止逐帧渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (_isReflected) {
    // create mask image
 
    UIGraphicsBeginImageContext([self bounds].size);
    CGColorSpaceRef deviceGray = CGColorSpaceCreateDeviceGray();
    CGFloat locations[2] = {1.0, 0.0);
    NSArray *colors = [NSArray arrayWithObjects:(id)[[UIColor colorWithWhite:0.0 alpha:0.0] CGColor], (id)[[UIColor colorWithWhite:0..0 alpha:0.5] CGColor], nil];
    CGGradientRef gradient = CGGradientCreateWithColors(deviceGray, (CFArrayRef)colors, locations);
 
    CGContextDrawLinearGradient(UIGraphicsGetCurrentContext(), gradient, CGPointMake(0, 0), CGPointMake(0, [self bounds].size.height), 0);
    CGGradientRelease(gradient);
    CGColorSpaceRelease(deviceGray);
    CGImageRef maskImage = [UIGraphicsGetImageFromCurrentImageContext() CGImage];
    UIGraphicsEndImageContext();
 
    CGContextClipToMask(context, myBounds, maskImage);
}
Edge Antialiasing

抗锯齿的实现有下面两个方法

  • Info.plist里设置UIViewEdgeAntialiasing键值 开销很大
  • 用一个像素的边来模拟

代码如下:

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
- (UIImageView *)createImageViewForImage:(UIImage *)image
{
    UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
    [imageView setContentMode:UIViewContentModeScaleAspectFit];
 
    //Add a shadow around the image.
    [[imageView layer] setShadowOffset:CGSizeMake(10, 10)];
    [[imageView layer] setShadowColor:[[UIColor blackColor] CGColor]];
    [[imageView layer] setShadowOpacity:.5];
 
    //Rotate the image by some small random angle.
    [imageView setTransform:CGAffineTransformMakeRotation((((float)random() / RAND_MAX) * MAX_ROTATION) - MAX_ROTATION / 2)];
 
    //Rather than using Core Animation's edge antialiasing, which requires an off-screen rendering pass, we'll draw our image int own image with an empty 1px border on each side. That way, when it's rotated, the edges will appear smooth because the outermost pixels of the original image will be sampled with the clear pixels in the outer border we add. This sampling is much faster but not as high-quality.
 
    CGSize imageSizeWithBorder = CGSizeMake([image size].width + 2, [image size].height + 2);
    UIGraphicsBeginImageContext(imageSizeWithBorder);
    // The image starts off filled with clear pixels, so we don't need to explicitly fill them here.
    [image drawInRect:(CGRect){{1,1}, [image size]}];
    [imageView setImage:UIGraphicsGetImageFromCurrentImageContext()];
    UIGraphicsEndImageContext();
 
    // We no longer need CA's edge antialiasing on this layer
    [[imageView layer] setEdgeAntialiasingMask:0];
 
    return imageView;
}

阴影绘制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (void)layoutImageView:(UIImageView *)imageView inFrame:(CGRect)celFrame withAnimationDuration:(NSTimeInterval)duration
{
    // The image view may be rotated, so set its frame in terms of the identity transform and then reapply the transform.
    CGAffineTransform imageViewTransform = [image View transform];
    [imageView setTransform:CGAffineTransformIdentity];
    [imageView setFrame:CenteredRectInRect(CGRectMake(0, 0, [imageView image].size.width, [imageView image].size.height), celFrame)];
    [imageView setTransform:imageViewTransform];
 
    // Save Core Animation a pass to figure out where the transparent pixels are by informing it explicitly of the contents's shape.
    CGPathRef oldPath = CGPathRetain([[imageView layer] shadowPath]);
    [[imageView layer] setShadowPath:[[UIBezierPath bezierPathWithRect:[imageView bounds]] CGPath]];
 
    // Since the layer's delegate (its UIView) will not create an action for this change (via the CALayerDelegate method actionForLayer:forKey:), we must explicitly create the animation between these values.
    CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:@"shadowPath"];
    [pathAnimation setFromValue:(id)oldPath];
    [pathAnimation setToValue:(id)[[imageView layer] shadowPath]];
    [pathAnimation setDuration:duration];
    [pathAnimation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
    [pathAnimation setRemovedOnCompletion:YES];
 
    [[imageView layer] addAnimation:pathAnimation forKey:@"shadowPath"];
    CGPathRelease(oldPath);
}

以上就是这个session中个人觉得比较精髓的部分了,一些地方用中文表达的也不是太好,代码都是手打的,难免有错误,如有问题还请留言指正 对这篇文章涉及的内容感兴趣的同学也看看这篇文章(在墙外,如有需要我可以帮忙做个pdf传上来)

—以上—

Tagged with: