iOS APP可执行文件的组成

2014-7-30 评论(30) 分类:技术文章 Tags:

iOS APP编译后,除了一些资源文件,剩下的就是一个可执行文件,有时候项目大了,引入的库多了,可执行文件很大,想知道这个可执行文件的构成是怎样,里面的内容都是些什么,哪些库占用空间较高,可以用以下方法勘察:

1.XCode开启编译选项Write Link Map File
XCode -> Project -> Build Settings -> 搜map -> 把Write Link Map File选项设为yes,并指定好linkMap的存储位置
linkmap

2.编译后,到编译目录里找到该txt文件,文件名和路径就是上述的Path to Link Map File
位于~/Library/Developer/Xcode/DerivedData/XXX-eumsvrzbvgfofvbfsoqokmjprvuh/Build/Intermediates/XXX.build/Debug-iphoneos/XXX.build/

这个LinkMap里展示了整个可执行文件的全貌,列出了编译后的每一个.o目标文件的信息(包括静态链接库.a里的),以及每一个目标文件的代码段,数据段存储详情。

1

以伊书项目为例,在LinkMap里首先列出来的是目标文件列表:

# Object files:
[ 0] linker synthesized
[ 1] /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator7.0.sdk/usr/lib/crt1.o
[ 2] /Users/bang/Library/Developer/Xcode/DerivedData/yishu-eyzgphknrrzpevagadjtwpzzeqag/Build/Intermediates/yishu.build/Debug-iphonesimulator/yishu.build/Objects-normal/i386/TKPFileInfo.o
...
[280] /Users/bang/Downloads/yishu/yishu/Classes/lib/UMeng/MobClick/libMobClickLibrary.a(UMANJob.o)
[281] /Users/bang/Downloads/yishu/yishu/Classes/lib/UMeng/MobClick/libMobClickLibrary.a(UMANWorker.o)
[282] /Users/bang/Downloads/yishu/yishu/Classes/lib/UMeng/MobClick/libMobClickLibrary.a(MobClick.o)
[283] /Users/bang/Downloads/yishu/yishu/Classes/lib/UMeng/MobClick/libMobClickLibrary.a(UMANLaunch.o)
...

前面中括号里的是这个文件的编号,后面会用到,像项目里引用到静态链接库libMobClickLibrary.a里的目标文件都会在这里列出来。

2

接着是一个段表,描述各个段在最后编译成的可执行文件中的偏移位置及大小,包括了代码段(__TEXT,保存程序代码段编译后的机器码)和数据段(__DATA,保存变量值)

# Sections:
# Address   Size     Segment   Section
0x00002740 0x00273890 __TEXT __text
0x00275FD0 0x00000ADA __TEXT __symbol_stub
0x00276AAC 0x00001222 __TEXT __stub_helper
0x00277CCE 0x00019D9E __TEXT __objc_methname
0x00291A70 0x00012847 __TEXT __cstring
0x002A42B7 0x00001FC1 __TEXT __objc_classname
0x002A6278 0x000046A7 __TEXT __objc_methtype
0x002AA920 0x000061CE __TEXT __ustring
0x002B0AF0 0x00000764 __TEXT __const
0x002B1254 0x000028B8 __TEXT __gcc_except_tab
0x002B3B0C 0x00004EBC __TEXT __unwind_info
0x002B89C8 0x0003662C __TEXT __eh_frame
0x002EF000 0x00000014 __DATA __program_vars
0x002EF014 0x00000284 __DATA __nl_symbol_ptr
0x002EF298 0x0000073C __DATA __la_symbol_ptr
0x002EF9E0 0x000030A4 __DATA __const
0x002F2A84 0x00000590 __DATA __objc_classlist
0x002F3014 0x0000000C __DATA __objc_nlclslist
0x002F3020 0x0000006C __DATA __objc_catlist
0x002F308C 0x000000D8 __DATA __objc_protolist
0x002F3164 0x00000008 __DATA __objc_imageinfo
0x002F3170 0x0002BC80 __DATA __objc_const
0x0031EDF0 0x00003A30 __DATA __objc_selrefs
0x00322820 0x00000014 __DATA __objc_protorefs
0x00322834 0x000006B8 __DATA __objc_classrefs
0x00322EEC 0x00000394 __DATA __objc_superrefs
0x00323280 0x000037C8 __DATA __objc_data
0x00326A48 0x000096D0 __DATA __cfstring
0x00330118 0x00001424 __DATA __objc_ivar
0x00331540 0x00006080 __DATA __data
0x003375C0 0x0000001C __DATA __common
0x003375E0 0x000018E8 __DATA __bss

首列是数据在文件的偏移位置,第二列是这一段占用大小,第三列是段类型,代码段和数据段,第四列是段名称。

每一行的数据都紧跟在上一行后面,如第二行__symbol_stub的地址0x00275FD0就是第一行__text的地址0x00002740加上大小0x00273890,整个可执行文件大致数据分布就是这样。

这里可以清楚看到各种类型的数据在最终可执行文件里占的比例,例如__text表示编译后的程序执行语句,__data表示已初始化的全局变量和局部静态变量,__bss表示未初始化的全局变量和局部静态变量,__cstring表示代码里的字符串常量,等等。

3

接着就是按上表顺序,列出具体的按每个文件列出每个对应字段的位置和占用空间

# Address Size File Name
0x00002740 0x0000003E [ 1] start
0x00002780 0x00000400 [ 2] +[TKPFileInfo parseWithDictionary:]
0x00002B80 0x00000030 [ 2] -[TKPFileInfo fileID]
...

同样首列是数据在文件的偏移地址,第二列是占用大小,第三列是所属文件序号,对应上述Object files列表,最后是名字。

例如第二行代表了文件序号为2(反查上面就是TKPFileInfo.o)的parseWithDictionary方法占用了1000byte大小。

使用

这个文件可以让你了解整个APP编译后的情况,也许从中可以发现一些异常,还可以用这个文件计算静态链接库在项目里占的大小,有时候我们在项目里链了很多第三方库,导致APP体积变大很多,我们想确切知道每个库占用了多大空间,可以给我们优化提供方向。LinkMap里有了每个目标文件每个方法每个数据的占用大小数据,所以只要写个脚本,就可以统计出每个.o最后的大小,属于一个.a静态链接库的.o加起来,就是这个库在APP里占用的空间大小。

写了个nodejs版统计程序可供使用:https://gist.github.com/bang590/8f3e9704f1c2661836cd

一个CoreText排版性能问题

2014-6-19 评论(2) 分类:技术文章 Tags:

伊书的阅读器是用CoreText排版的,在中文字体的选择上,自带的字体中只有黑体,后来发现日文字体Hiragino也是可以用于中文,而且显示效果比黑体好很多,于是选用了这个字体。但在CoreText排版中遇到个问题,用Hiragino字体排版速度非常慢,几乎是默认黑体的100倍,以下是用Instrument Time Profiler查看排版同一章内容的时间消耗:

Hiragino:

1

黑体:

2

时间都消耗在CTFramesetterCreateWithAttributedString里,CoreText又不开源,看不到内部实现,在TimeProfiler里再继续展开,到最后只能看到时间分别耗在两个TRunGlue::ComputeEndIndex()里,看不出什么头绪。

后来用Instrument Allocation查看APP的内存分配时,发现用Hiragino字体排版生成的CTRun非常多,一章内容有2万多个CTRun。按理说,若一篇文章字体格式全部一样,渲染到frame之前CTRun只会有一个,渲染到frame后一行一个CTRun,怎样都不应该出现这么多CTRun。

3

再测了用黑体排版生成的CTRun次数,这次符合预期,只有70多个:

4

可以猜测到用Hiragino字体速度慢就是因为生成了大量的CTRun,那为什么会出现这样的情况?断点进入渲染处,打出其中一行CTLine数据,如下:

5

发现一些文字自动被转为黑体了。原因是Hiragino只支持部分中文字体,而那些不支持的中文字体就要用黑体代替,又因为一篇文章里会交错出现很多Hiragino支持和不支持的字,于是无法用一个CTRun表示一整段文字,每个交错都用一个新的CTRun,导致CTRun非常多,排版处理逻辑变复杂,耗时变高。

终于找到问题的原因,但如果想用Hiragino字体显示中文,这个问题还是无解的。其实在iOS4.x里是没有这个问题的,估计当时CoreText还没有为字体问题拆分多个CTRun,在5.0才开始这样做。除了我使用Hiragino字体会遇到这个问题外,在中英混杂的文字里用英文字体也有这样的问题,因为英文字体不支持中文显示,还是会被拆分成很多个CTRun,用CoreText注重性能的人可以注意下这个问题。

[iOS]ARC下循环引用的问题

2013-8-30 评论(6) 分类:技术文章 Tags:

最初

最近在开发应用时碰到使用ASIHttpRequest后在某些机器上发不出请求的问题,项目开启了ARC,代码是这样写的:

@implement MainController
- (void) fetchUrl{
    ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:currUrl]];
    [request setCompletionBlock:^{
        NSLog(@"completed");
    }];
    [request startAsynchronous];
}
@end

后来发现原因是request这个变量在退出这个函数后就被释放了,自然发不出请求。因为用了ARC,没法手动调用[request retain]让这个变量不被释放,所以只能把这个变量变成实例变量,让Controller实例存在的过程中一直持有这个变量不释放。

@interface MainController {
     ASIHTTPRequest *request;
}
@end

@implement MainController
- (void) fetchUrl{
    request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:currUrl]];
    [request setCompletionBlock:^{
        [self complete];
    }];
    [request setFailedBlock:^{
          NSLog(@"failed");
    }];
    [request startAsynchronous];
}
@end

问题一

这下发送请求没问题了,但出了另一个问题,XCode编译后提示[self complete]这一行可能会导致循环引用。因为MainController实例持有request, request持有completionBlock,completionBlock又持有MainController,导致循环引用,MainController实例在外界引用计数为0时仍无法被释放,因为自身的变量request里持有MainController实例的引用,其引用计数永远大于1。

导致这样循环引用的原因是在completionBlock里调用的self是一个strong类的引用,会使self引用计数+1,可以保证在调用过程self不会被释放,但在这里不需要这样的保证,可以声明另一个__weak变量指向self,这样在block使用这个变量就不会导致self引用计数+1,不会导致循环引用。

(更多…)

伊书web排版解析

2012-3-3 评论(8) 分类:技术文章 Tags:

伊书是针对iOS的电子书WebApp,在web上排版不像原生APP有底层排版函数支持,限制很大,勉强实现了一些特性,在此分享。

内容左右对齐

给内容的DOM加上这个CSS属性即可:text-align:justify;
现代桌面浏览器几乎都支持,没详测。iOS5.0以上才开始支持这个属性,似乎若没有这个属性,左右对齐无解。

分页

web无法做到一页页定量渲染文字,分页需要一点hack才能做到。
我的做法是,一章的内容放在一个dom里,复制出一个一样的dom,限制显示一页范围的内容,根据页码调整显示位置。
一图声千言:

坏处是这样的形式要精确算准行高,就算字体大小不一样也要是跟正文一样的行高或是它倍数,一个算不准就会出现只显示半截文字的情况。也因为这样字体大小调整比较麻烦。

标点外挂

标点外挂是指一行中若第一个字符是标点符号,就把它移到上一行的末尾。具体实现方法是:

  1. 给所有标点加标签,就是字符串替换,把。?等替换成<b>。</b> <b>?</b>
  2. 遍历内容里的所有b标签,判断它们的位置是否在当前行的起始位置,若是则把它移到上一行末尾,具体代码:
$(".page_content b").each(function(i, b){
    if (b.offsetLeft == 0) {
        $(b).css({
            "position" : "absolute",
            "left": "256px",
            "top": b.offsetTop - lineHeight + "px"
        })
    }
});

内容缩进、信件落款左右对齐特殊处理

内容数据我是手工格式化后存到json里的,在这些特殊的地方加了标签,在显示时替换成dom按这些不同的类型给内容以不同的缩进/对齐。

注释

同上,在需要注释的地方用自定义的标签把内容包在标签里,在显示内容时把这个标签替换成dom,用js控制这个dom的touch事件显示内容就行了。需要判断这个注释的位置,以决定注释内容的显示位置。

[iOS]给UIWebView头尾插入自定义View

2012-2-25 评论(4) 分类:技术文章 Tags:

src与demo:https://github.com/bang590/iOSPlayground/tree/master/TWebview

经常有这样的需求:需要给webview头部加个自定义view,这个view不是固定的而是要跟webview一起滚动。例如iMail里的读邮件页面,头部有收件人等信息,下面是一个webview。如浏览器,顶部的地址栏要随页面滚动。如阅读类软件,需要给正文上面加一个标题。

实现方法可以通过设webview里scrollView的contentInset,在头部留好自定义view的空间,再把view添加进来,随着webview的滚动缩放细调这个view的位置。这里把这些行为封装了起来做成一个扩展的UIWebView,主要解决的问题是:

1.直接通过设headerView和footerView完成头尾view的插入。
2.webView滚动和缩放时自动调整headerView和footerView的位置,使之看起来自然。
3.提供接口实现头部高度改变时进行动画

有个问题未解决:

设置contentInset后,webview顶点的Y坐标不是0,而是小于0的一个数(负的headerView的高度) 。
在webview跳转页面时,会自动滚动到(0,0)位置,也就是跳到webview内容的顶部,也就是这时候headerView被隐藏了。

从调用堆栈来看这次跳转是私有的UIWebDocumentView调起,无法截获阻止。只能在触发scrollViewDidScroll时让它跳回去。但还没找到方法判断什么时候该跳什么时候不该跳。

[iOS]自动添加NavigationController自定义按钮动画

2012-2-21 评论(0) 分类:技术文章 Tags:

问题

UINavigationController顶部的返回按钮在切换视图时有左右滑动的动画(效果见iMail),但在自定义了这个按钮后(通过设self.navigationItem.leftBarButtonItem自定义),切换视图时按钮没有了动画。

最初解决方法

在每个viewController的viewWillAppear和viewDisappear方法上手动让按钮左右动。由于每个视图出现/消失的方向都有左/右两种,在这里难以判断,需要各种变量辅助,实现十分恶心,中间需要新加view时逻辑还要重新修改,被折腾。

最终解决方法

继承UINavigationController,重写pushViewController和popViewController方法,在这两个方法里面获取NavigationController里的view栈,因为push和pop总是操作最前面的View,所以可以从栈里知道哪两个view是要做动画的。调用这些view相应的方法使他们让自己的按钮动。这些方法可以写在UIViewController的扩展里,不用修改每一个viewController。

换言之,把下面的代码加入项目里,把项目里的UINavigationController换成下面的类,那些自定义的返回按钮就会自动做动画了。

代码

https://github.com/bang590/iOSPlayground/tree/master/NavigationButtonAnimate
(更多…)

[iOS]iOS开发初学者记录

2012-2-6 评论(4) 分类:技术文章 Tags:

做完了项目,想总结下,不知怎么写合适,只列出一些iOS开发的基础点,开发前因为不知道这些点,导致走了一些弯路,希望对初学者有帮助。

基础篇

为什么对一个变量release后还要设为nil

对一个变量release后,这个变量指向的内存释放了,但这个变量本身没变,仍指向原来的内存地址。若这个变量在释放后被访问,或者被重复release,就会导致应用崩溃。设为nil后这个变量指向0x00,可以保证程序以后访问不到原先的内存地址,对nil进行release也没任何问题。

使用类成员时,前面加不加self.有什么区别

不加self.调用的是成员本身,加self.后实际上调用了其成员的get set方法。
例:

//.h
@property (nonatomic, retain) NSString *name

//.m
name = @"bang";  //没有retain,随时会被释放
NSString *str = self.name  //等于NSString *str = [self name];
self.name = @"bang"; //等于[self setName:@"bang"]; 这时在set方法里retain了这个字符串

技巧篇

内存泄漏

可以通过xcode的编译工具Product-Analyze检查函数块范围内可能的泄漏点(外带会提示一些可能有的错误)。

用leaks工具监测出来的泄漏查找方法是跟踪其代码提示中出现的变量,经常这个变量是在提示的调用堆栈以外的地方泄漏的。若实在查不到,最终办法是重写这个变量的retain和release方法,debug,从调用堆栈看是谁retain了它而没有release。

要注意的是,用CFXXCreate(例如CFArrayCreate)生成的变量要用CFRelease释放。

数据存储

如无搜索需要,可以将一个数据对象直接序列化后存到sqlite,取出时直接反序列化为对象使用。序列化需要数据类实现NSCoding协议,实现encodeWithCoder和initWithCoder两个方法就行,若有多个数据对象,可以写个基类实现这两个方法,并在这里面利用反射枚举自身所有变量去encode和decode,一劳永逸,具体实现网上找找就有了。

组件篇

UINavigationController头尾显示隐藏

在用NavigationController去管理view的push和pop时,需要根据不同的view设置是否显示NavigationBar和ToolBar,一开始在错误的地方设置了,导致有时该显示NavigationBar和ToolBar时不显示的情况,后来发现在viewWillAppear上设置万无一失。别笑我土鳖,没好好去理解它整个流程,一直没发现。

- (void) viewWillAppear:(BOOL)animated{
    [super viewWillAppear:animated];
    [self.navigationController setToolbarHidden:NO];
    [self.navigationController setNavigationBarHidden:NO];
}

UITableView游标式渲染

tableView的机制大概是:先定好总行数,某一行滚入视图范围时,回调一个函数去取view出来显示。这一行滚出视图再滚入时仍会继续回调这一函数取view。有这样的机制就是说无论你table里的数据有多少,都可以全部放入table中不用分页,因为不用一次性把所有数据都取出来,只在需要显示的时候根据游标去取对应的数据就行了。

可能这是APP组件很自然的方式不用说明,但在web上页面上的数据和元素都是要一次性载入内存的,做久了web,一开始没想到它这样的实现机制,导致我们走了不少弯路。

UIWebView渲染范围

UIWebView不是根据可视范围决定每次的渲染范围,而是根据自身控件的frame大小决定。

曾尝试webview嵌在tableview里,为了让webview跟tableview一起滚动,把webview的大小设为webview里的内容大小,让webview不出滚动条,从而能跟着tableview的滚动条一起滚。这样做的后果是每次webview都一次性渲染整个页面,内存占用多性能很差,而且在放大缩小这个webview时,渲染放大的整个页面更吃力,出现不能忍受的性能。解决办法是让webview定住高度为一整屏iphone的高度,限制了webview每次的渲染范围为可视范围,性能大好。带来的问题是无法随tableview滚动,但可以以其他方式优化体验。最近看到新版的ZAKER也是这样做的。

个人感觉篇

界面布局调整非常麻烦,让人怀念web了。界面描述方法XIB感觉晦涩难学,至今不会,没有CSS+HTML来得方便。

有编译器把关,少了像写js时多写or写错一个字符查半天的问题。

Object-C写起来各种变量函数和变量调用很长,没有js的短小精悍来得爽。

第一次编写涉及手动内存管理的程序,挺有意思,没想象中难,但有些内存管理导致的bug很难查。

虽然APP不像web那样随时更新,但也不像传统PC客户端升级那么麻烦,用户更新意愿更强,还是适合快速迭代的。

细节是可以决定成败,但得看你把什么定成细节。

最后,0bug的程序不存在,极致是把最主要的事做好。done is better than perfect。

iOS5 innerHTML插入内联touch事件的问题

2011-10-19 评论(2) 分类:技术文章 Tags:

iOS5一出来,很多对safari的溢美之声,那些新增的特性确实好,但这个版本的safari很有问题。

document.getElementById("test").innerHTML = '<div ontouchstart="alert(\'touchstart\')"></div>'

一般,这样的代码是没问题的,一段HTML字符串赋给DOM的innerHTML后,自动生成DOM,并且上面的内联事件都是有效的。在至今所有浏览器都可以这样,但现在在iOS5的Safari不行,生成的DOM中ontouchstart事件无效,目前测试ontouchstart/ontouchmove/ontouchend都不行,若改成onmousedown,onclick等都是可以的。这个问题导致了jquerysencha的一些应用都挂了。

目前的解决办法就是,先把HTML加入一个新建的DOM里,再用cloneNode把创建好的DOM取出来加入目标位置,这样才可以。如果不用cloneNode而是直接把创建好的元素塞进去还是不行的。

var dom = document.createElement("div");                                    
dom.innerHTML = '<span ontouchstart="alert(\'touchstart\')">touchstart dom create</span>    ';                                                                          
document.getElementById("testTSdom") .appendChild(dom.childNodes[0].cloneNode(true));

可以在这里 http://bangswork.googlecode.com/svn/trunk/lab/ios5touchstart/index.html 看到效果

这应该属于一个bug,居然就带着这么明显的bug发布出来了,要折腾死做移动版的前端了。

此外,还发现iOS5下Safari的一些其他问题,例如缓存很奇怪js excution timeout,还有一些已经碰到但未明原因的,总之,这个safari很有问题。

Android和iOS的体验差异

2011-1-3 评论(11) 分类:互联网 Tags:

最近试玩Nexus one,对比我自己的iTouch2,显得生硬,卡卡的感觉,不太流畅,之前也试玩过milestone,都差不多,为什么这些配置高级的Android机器使用流畅程度上都不如配置过时的iTouch2?本来我以为是硬件问题,Android手机屏幕灵敏度不够,现在觉得,应该是软件问题,Android不重视这种流畅UI体验。

例如,Android浏览器缩放页面时是边缩放边渲染页面。处理页面渲染和响应手指交互是同时进行的,平级的两个事件,结果是,在元素稍微多的页面上移动和缩放都会显得很卡,有时还会忽略了交互事件,因为浏览器忙于渲染页面/处理脚本。

iOS上是在手指交互事件结束后才渲染页面,页面的渲染不会跟交互争抢资源,在复杂的页面上拖动,如果拖动得太快,iOS也会马上响应你拖动到的位置,并且动画效果保持流畅,只是在你拖动过程中那个位置是空白的,在不用响应交互事件的时候才渲染页面。

速度永远是产品体验的第一要素,看看整个iOS系统,响应交互的优先级都是最高的,一般情况下手指对屏幕做出的交互命令都能得到最快最流畅的反应,在硬件不给力的情况下它也可以通过动画或其他各种方式告诉你已经接收到命令了,并最优先处理你的命令。说白了就是iOS把你的命令当作最高指令,Android则认为你的命令跟机器内部的命令是平等的。正如之前在网上看的评论,iOS充满人性化,Android就是一部机器。

上面的举例只是冰山一角,再仔细体验可以挑出很多iOS体验上细致的优化,例如页面到边界时直接撞墙,iOS则有缓冲,双击页面时iOS总能放大到合适的大小,Android不灵。

苹果有这么多粉丝不是盖的,用户体验也不是吹出来的。也只有它有能力把一件产品做得如此细致,因为硬件软件UI全是由它们设计,在世界第一偏执狂乔布斯手下又能把每件都做好,还对每一款产品提供完整的产业链一条龙服务,我想我没成为苹果粉丝是因为我没钱~

不过iOS相对Android还是有劣势的,一条龙服务做到了,各种体验都完美,但代价是不个性化,例如你不能往屏幕上添加widget,永远是那一排排整整齐齐的APP图标,永远只能左右翻动,这也是另外一种生硬。

很多模仿iPhone的手机都是形像神不像,本来以为魅族M9可以做得好一点,我觉得M9该做的就是给Android套上细致流畅的UI体验,但看了网上演示M9的视频,跟其他Android无差别,不知道是不是技术原因做不到。