一只在路上的iOS程序猿
故事是这样开始的,刚开始我一直想找可以延长App后台存活方法的,后来找到如下代码
1 | //程序进入后台后继续运行10分钟 |
使用中发现,App会在3分钟左右强制kill。经查询发现写法有问题。
1 | UIBackgroundTaskIdentifier backgroundUpdateTask; |
文档上说有10分钟的执行时间,但从打印的backgroundTimeRemaining时间来看,只有180秒。
注意:测试此功能不能用Xcode直接debug运行,因为在调试器链接到app的进行的情况下,app是不会在后台被挂起的,也就是说即使backgroundTimeRemaining =0了,timer里的代码依然能够继续执行。
所以要测试运行态的情况,要么用文件日志(总是要导出比较麻烦),要么用本地通知来查看。
2、是否能递归调用此方法来持续获得执行时间
在beginBackgroundTaskWithExpirationHandler里最后再递归调用[self startTask];
经尝试此方法无效,180秒超时后再次申请,会立刻回调超时的block,并且backgroundTimeRemaining时间一直都是0。
并且由于一直不停的在递归创建和终止后台任务,当Expiration真正到来的时候,一个还有一个创建的任务没有关闭。从而导致违背begin和end成对调用的原则,app被系统强制kill。所以此方法不但不能延长执行时间,还会导致app在180秒后台执行时间到达后,被系统kill的情况。
3、beginBackgroundTaskWithExpirationHandler多次被调用的情况
didEnterBackground每次调用都会触发beginBackgroundTaskWithExpirationHandler来创建新的后台任务,并用backgroundUpdateTask保存任务id,但如果第一次的任务还没有endBackgroundTask之前,应用回到前台,然后再次进入后台,就会重新创建一个新的后台任务,并且backgroundUpdateTask之前保存的id会被覆盖,这就违背了beginBackgroundTaskWithExpirationHandler与endBackgroundTask成对调用的原因。因为前一个后台任务超时的block回调的时候,其实是end了后一个taskId对应的后台任务,并且把taskId赋值为UIBackgroundTaskInvalid。而后一个后台任务超时的block回调的时候,taskId已经变成了null,对其进行end调用已经无效了,所以相当于没有成对调用begin和end,导致的结果就是:后一个后台任务超时的时候,app被系统强制kill。
所以每一次创建的后台任务都要有一个独立的变量来维护其taskId,如果只有一个后台任务,但是有重入的可能,那么应该在willEnterForeground回调中,把前一个后台任务进行endBackgroundTask操作,这样就不存在taskId被覆盖的问题了。或者是每次didEnterBackground的时候,检查taskId == UIBackgroundTaskInvalid,若不满足该条件,说明taskId已经引用了一个正在进行的后台任务,还没有完成,由于这个后台任务重进前台又切换回后台的情况下,backgroundTimeRemaining会被重置为180秒,所以在这种需求下,关闭前一个任务再重新建议一个相同的后台任务没有必要,所以应该直接
1 | if(backgroundUpdateTask != UIBackgroundTaskInvalid){ |
4、后台任务expiration后,app被系统kill的问题
按照文档里的说法,只要begin与end在真正expiration之前成对调用,就不会导致系统强制kill app,而是app从后台执行状态切换到suspend状态,但实际测试中,每次expiration之后,app都会被kill掉,根据是app从launch页面重新进入。但我在willTerminate通知里的回调中加了一个local notification,并没有触发这个本地通知。(从app switcher强制退出应用的时候会触发本地通知,说明本地通知有效)。只能认为是app从后台状态切换到suspend状态后,立刻被系统kill掉了,但不知道为什么会这样。
5、参考另一个文章中的实现,可以在任务结束后不被kill
参考
http://www.cnblogs.com/lyanet/archive/2013/03/26/2983079.html
1 | //程序进入后台后延长活动时间,一般180s内,添加这段代码是为了避免App推入后台,线程暂停,NSTime停止计时(验证码)。 |
beginBackgroundTaskWithExpirationHandler 和 endBackgroundTask 是成对出现。
beginBackgroundTaskWithName:expirationHandler:方法标识了一个后台任务的开始,并用过超时处理的回调来结束此任务。那么,超时时间具体是多少?可以通过UIApplication的只读属性backgroundTimeRemaining来获取当前后台任务执行的剩余时间,它不是具体的数字(大约180s内),而是iOS根据当前系统环境综合考量后估算出来的。然后执行expirationHandler回调完成一个后台任务的执行周期。
当开启后台任务后,结束前block回调,回调中就要设置关闭后台任务的代码,比如缓存数据等等。并且调用endBackgroundTask。关闭后台任务,否则系统会任务后台任务超时仍未完成,系统直接kill app。导致再次打开挂起的App发现,应用得重新启动。
之前的问题就因为没有调用endBackgroundTask 导致App提前kill。
工作中上传后重新加载头像的时候,发现头像并没有改变。这个问题以前也遇到过,这次不能忍了必须查个水落石出。
SDWebImage options设置为:SDWebImageRefreshCached,但是并没有效果。
汇总资料后:
使用
curl url --head
例如:
curl http://123.126.109.38:18081/AreaAppFile/nurse_head_thumb/402887bb62f58c250162f5bf4a840006.jpg --head
返回:
curl http://123.126.109.38:18081/AreaAppFile/nurse_head_thumb/402887bb62f58c250162f5bf4a840006.jpg --head
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Accept-Ranges: bytes
ETag: W/"48681-1525950947149"
Last-Modified: Thu, 10 May 2018 11:15:47 GMT
Content-Type: image/jpeg
Content-Length: 48681
Date: Thu, 10 May 2018 11:16:31 GMT
对比抖音头像链接:
curl https://p1.pstatp.com/aweme/100x100/729c00380329e728f710.jpeg --head
HTTP/1.1 200 OK
Expires: Thu, 18 Apr 2019 06:10:07 GMT
Date: Wed, 18 Apr 2018 06:10:07 GMT
Server: nginx
Content-Type: image/jpeg
Content-Length: 2689
Accept-Ranges: bytes
Cache-Control: max-age=31536000
Last-Modified: Tue, 27 Mar 2018 15:18:20 GMT
X-Mosaic-Namespace: aweme-online
X-Response-Date: Tue, 27 Mar 2018 15:22:24 GMT
X-Xxoo-Time: Tue, 27 Mar 2018 15:22:24 GMT
Access-Control-Allow-Origin: *
Age: 1
X-Via: 1.1 lf163:4 (Cdn Cache Server V2.0), 1.1 menxiazai39:3 (Cdn Cache Server V2.0), 1.1 fangwangtong49:4 (Cdn Cache Server V2.0)
Connection: keep-alive
X-Dscp-Value: 0
> 对比发现没有Cache-Control: 字段
#### 好奇Cache-Control:如何实现图片验证,资料如下
客户端从服务器请求数据经历如下基本步骤:
1. 如果请求命中本地缓存则从本地缓存中获取一个对应资源的"copy";
2. 检查这个"copy"是否fresh,是则直接返回,否则继续向服务器转发请求。
3. 服务器接收到请求,然后判断资源是否变更,是则返回新内容,否则返回304,未变更。
4. 客户端更新本地缓存。
* no-cache的作用是:强制客户端跳过步骤2,直接向服务器发送请求。也就是说每次请求都必须向服务器发送。
* must-revalidate:作用与no-cache相同,但更严格,强制意味更明显。但这只是理论上的描述,根据我在ff6上的测试,它几乎不起作用:只要请求的频率加快到一定程度,服务器就接收不到请求。
* no-store:缓存将不存储response,包括header和body。测试结果表明,除每次请求都必发送到服务器外,响应代码均是200,且request并没有发送"If-Modified-Since"和"If-None-Match"头,这意味着缓存的确没有存储response。
以上三者都是要求客户端每次请求都必须到服务器进行revalidate,此功能还可以通过max-age实现:
Cache-Control:max-age=0
测试结果证明了这一点,每次都请求了服务器,且状态码是304。
明天让服务端增加该值,再试试看。
开场白
语言本地化,又叫做语言国际化。是指根据用户操作系统的语言设置,自动将应用程序的语言设置为和用户操作系统语言一致的语言。往往一些应用程序需要提供给多个国家的人群使用,或者一个国家有多种语言,这就要求应用程序所展示的文字、图片等信息,能够让讲不同语言的用户读懂、看懂。进而提出为同一个应用程序适配多种语言,也即是国际化。
info.plist 中修改Bundle name即可设置各种语言下的App名字
main.storyboard 中的文本修改
然后我们只需要在Localizable.strings下对应的文件中,分别以Key-Value的形式,为代码中每一个需要本地化的字符串赋值,如下图:
代码中使用方法 [NSLocalizedString(<#T##key: String##String#>, comment: <#T##String#>)]
注意 文件名必须是 Localizable.strings 不然无法识别,上面截图有误
[!] The `PuHuaHospital [Release]` target overrides the `GCC_PREPROCESSOR_DEFINITIONS` build setting defined in `Pods/Target Support Files/Pods-PuHuaHospital/Pods-PuHuaHospital.release.xcconfig'. This can lead to problems with the CocoaPods installation
- Use the `$(inherited)` flag, or
- Remove the build settings from the target.
解决办法:
在Build Settings
Header Search Paths
Other Linker Flags
Preprocessor Macros
在这三个地方都加上$(inherited),然后重新pod install就可以了。
* 为什么要优化NSDateFormatter?
今天逛论坛看到NSDateFormatte很耗性能,一查发现确有其事。
下面是官方描述
Creating a date formatter is not a cheap operation. If you are likely to use a formatter frequently, it is typically more efficient to cache a single instance than to create and dispose of multiple instances. One approach is to use a static variable
优化方式有哪些?
a.延迟转换
即只有在UI需要使用转换结果时在进行转换。
b.Cache in Memory
根据NSDateFormatter线程安全性,不同的iOS系统版本内存缓存如下:
如果直接采用静态变量进行存储,那么可能就会存在线程安全问题,在iOS 7之前,NSDateFormatter是非线程安全的,因此可能就会有两条或以上的线程同时访问同一个日期格式化对象,从而导致App崩溃。
+ (NSDateFormatter *)cachedDateFormatter {
NSMutableDictionary *threadDictionary = [[NSThread currentThread] threadDictionary];
NSDateFormatter *dateFormatter = [threadDictionary objectForKey:@"cachedDateFormatter"];
if (!dateFormatter) {
dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setLocale:[NSLocale currentLocale]];
[dateFormatter setDateFormat: @"YYYY-MM-dd HH:mm:ss"];
[threadDictionary setObject:dateFormatter forKey:@"cachedDateFormatter"];
}
return dateFormatter;
}
在iOS 7、macOS 10.9及以上系统版本,NSDateFormatter都是线程安全的,因此我们无需担心日期格式化对象在使用过程中被另外一条线程给修改,为了提高性能,我们还可以在上述代码块中进行简化(除去冗余部分)。
static NSDateFormatter *cachedDateFormatter = nil;
+ (NSDateFormatter *)cachedDateFormatter {
// If the date formatters aren't already set up, create them and cache them for reuse.
if (!dateFormatter) {
dateFormatter = [[NSDateFormatter alloc] init];
[dateFormatter setLocale:[NSLocale currentLocale]];
[dateFormatter setDateFormat: @"YYYY-MM-dd HH:mm:ss"];
}
return dateFormatter;
}
如果缓存了日期格式化或者是其他依赖于current locale的对象,那么我们应该监听NSCurrentLocaleDidChangeNotification通知,当current locale变化时及时更新被缓存的日期格式化对象。
In theory you could use an auto-updating locale (autoupdatingCurrentLocale) to create a locale that automatically accounts for changes in the user’s locale settings. In practice this currently does not work with date formatters.
c.利用标准C语言库
如果时间日期格式是固定的,我们可以采用C语言中的strptime函数,这样更加简单高效。
- (NSDate *) easyDateFormatter{
time_t t;
struct tm tm;
char *iso8601 = "2016-09-18";
strptime(iso8601, "%Y-%m-%d", &tm);
tm.tm_isdst = -1;
tm.tm_hour = 0;//当tm结构体中的tm.tm_hour为负数,会导致mktime(&tm)计算错误
/**
//NSString *iso8601String = @"2016-09-18T17:30:08+08:00";
//%Y-%m-%d [iso8601String cStringUsingEncoding:NSUTF8StringEncoding]
{
tm_sec = 0
tm_min = 0
tm_hour = 0
tm_mday = 18
tm_mon = 9
tm_year = 116
tm_wday = 2
tm_yday = 291
tm_isdst = 0
tm_gmtoff = 28800
tm_zone = 0x00007fd9b600c31c "CST"
}
ISO8601时间格式:2004-05-03T17:30:08+08:00 参考Wikipedia
*/
t = mktime(&tm);
//http://pubs.opengroup.org/onlinepubs/9699919799/functions/mktime.html
//secondsFromGMT: The current difference in seconds between the receiver and Greenwich Mean Time.
return [NSDate dateWithTimeIntervalSince1970:t + [[NSTimeZone localTimeZone] secondsFromGMT]];
}
相关资料:
Date Formate Patterns :
根据苹果的介绍,iOS设备中的Keychain是一个安全的存储容器,可以用来为不同应用保存敏感信息比如用户名,密码,网络密码,认证令牌。苹果自己用keychain来保存Wi-Fi网络密码,VPN凭证等等。它是一个在所有app之外的sqlite数据库。
如果我们手动把自己的私密信息加密,然后通过写文件保存在本地,再从本地取出不仅麻烦,而且私密信息也会随着App的删除而丢失。iOS的Keychain能完美的解决这些问题。并且从iOS 3.0开始,Keychain还支持跨程序分享。这样就极大的方便了用户。省去了很多要记忆密码的烦恼。
Keychain内部可以保存很多的信息。每条信息作为一个单独的keychain item,keychain item一般为一个字典,每条keychain item包含一条data和很多attributes。举个例子,一个用户账户就是一条item,用户名可以作为一个attribute , 密码就是data。 keychain虽然是可以保存15000条item,每条50个attributes,但是苹果工程师建议最好别放那么多,存几千条密码,几千字节没什么问题。
Keychain 可以包含任意数量的 keychain item。每一个 keychain item 包含数据和一组属性。对于一个需要保护的 keychain item,比如密码或者私钥(用于加密或者解密的string字节)数据是加密的,会被 keychain 保护起来的;对于无需保护的 keychain item,例如,证书,数据未被加密。
跟keychain item有关系的取决于item的类型;应用程序中最常用的是网络密码(Internet passwrods)和普通的密码。正如你所想的,网络密码像安全域(security domain)、协议、和路径等一些属性。在OSX中,当keychain被锁的时候加密的item没办法访问,如果你想要该问被锁的item,就会弹出一个对话框,需要你输入对应keychain的密码。当然,未有密码的keychain你可以随时访问。但在iOS中,你只可以访问你自已的keychain items;
extern CFTypeRef kSecClassGenericPassword
extern CFTypeRef kSecClassInternetPassword
extern CFTypeRef kSecClassCertificate
extern CFTypeRef kSecClassKey
extern CFTypeRef kSecClassIdentity OSX_AVAILABLE_STARTING(MAC_10_7, __IPHONE_2_0);
大多数iOS应用需要用到Keychain, 都用来添加一个密码,修改一个已存在Keychain item或者取回密码。Keychain提供了以下的操作
+ (NSMutableDictionary *)keyChainQueryDictionaryWithService:(NSString *)service{
NSMutableDictionary *keyChainQueryDictaionary = [[NSMutableDictionary alloc]init];
[keyChainQueryDictaionary setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass];
[keyChainQueryDictaionary setObject:service forKey:(id)kSecAttrService];
[keyChainQueryDictaionary setObject:service forKey:(id)kSecAttrAccount];
return keyChainQueryDictaionary;
}
+ (BOOL)addData:(id)data forService:(NSString *)service{
NSMutableDictionary *keychainQuery = [self keyChainQueryDictionaryWithService:service];
SecItemDelete((CFDictionaryRef)keychainQuery);
[keychainQuery setObject:[NSKeyedArchiver archivedDataWithRootObject:data] forKey:(id)kSecValueData];
OSStatus status= SecItemAdd((CFDictionaryRef)keychainQuery, NULL);
if (status == noErr) {
return YES;
}
return NO;
}
+ (id)queryDataWithService:(NSString *)service {
id result;
NSMutableDictionary *keyChainQuery = [self keyChainQueryDictionaryWithService:service];
[keyChainQuery setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];
[keyChainQuery setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit];
CFDataRef keyData = NULL;
if (SecItemCopyMatching((CFDictionaryRef)keyChainQuery, (CFTypeRef *)&keyData) == noErr) {
@try {
result = [NSKeyedUnarchiver unarchiveObjectWithData:(__bridge NSData *)keyData];
}
@catch (NSException *exception) {
NSLog(@"不存在数据");
}
@finally {
}
}
if (keyData) {
CFRelease(keyData);
}
return result;
}
+ (BOOL)updateData:(id)data forService:(NSString *)service{
NSMutableDictionary *searchDictionary = [self keyChainQueryDictionaryWithService:service];
if (!searchDictionary) {
return NO;
}
NSMutableDictionary *updateDictionary = [[NSMutableDictionary alloc] init];
[updateDictionary setObject:[NSKeyedArchiver archivedDataWithRootObject:data] forKey:(id)kSecValueData];
OSStatus status = SecItemUpdate((CFDictionaryRef)searchDictionary,
(CFDictionaryRef)updateDictionary);
if (status == errSecSuccess) {
return YES;
}
return NO;
}
+ (BOOL)deleteDataWithService:(NSString *)service{
NSMutableDictionary *keyChainDictionary = [self keyChainQueryDictionaryWithService:service];
OSStatus status = SecItemDelete((CFDictionaryRef)keyChainDictionary);
if (status == noErr) {
return YES;
}
return NO;
}
在工程中 Target -> Capabilities -> Keychain Groups。打开这个选项。
Keychain通过provisioning profile来区分不同的应用,provisioning文件内含有应用的bundle id和添加的access groups。不同的应用是完全无法访问其他应用保存在Keychain的信息,除非指定了同样的access group。指定了同样的group名称后,不同的应用间就可以分享保存在Keychain内的信息。
首先要在Capabilities下打开工程的Keychain Sharing按钮。然后需要分享Keychain的不同应用添 加相同的Group名称。Xcode6以后Group可以随便命名,不需要加AppIdentifierPrefix前缀,并且Xcode会在以entitlements结尾的文件内自动添加所有Group名称,然后在每一个Group前自动加上$(AppIdentifierPrefix)前缀。虽然文档内提到还需要添加一个包含group的.plist文件,其实它和.entitlements文件是同样的作用,所以不需要重复添加。 但是每个不同的应用第一条Group最好以自己的bundleID命名,因为如果entitlements文件内已经有Keychain Access Groups数组后item的Group属性默认就为数组内的第一条Grop。
需要支持跨设备分享的Keychain item添加一条AccessGroup属性,不过代码里Group名称一定要加上AppIdentifierPrefix前缀。 [searchDictionary setObject:@“AppIdentifierPrefix.UC.testWriteKeychainSuit” forKey:(id)kSecAttrAccessGroup];如果要在app内部存私有的信息,group置为自己的bundleID即可,如果entitlements文件内没有指定Keychain Access Groups数组。那group也可以置为nil,这样默认也会以自己的bundleID作为Group。
3.
Keychain内部的数据会自动加密。如果设备没有越狱并且不暴力破解,keychain确实很安全。但是越狱后的设备,keychain就很危险了。
通过上面的一些信息我们已经知道访问keychain里面的数据需要和app一样的证书或者获得access group的名称。设备越狱后相当于对苹果做签名检查的地方打了个补丁,伪造一个证书的app也能正常使用,并且加上Keychain Dumper这些工具获取Keychain内的信息会非常容易。
keychain 是一个很有用的工具,我们可以在保存密码或者证书的时候使用keychain,并且支持不同应用分享Keychain内的信息,或者支持iCloud备份跨设备分享,但是越狱版应用还是不建议使用
在我们提交安装包到App Store的时候,如果安装包过大,有可能会收到类似如下内容的一封邮件:
收到这封邮件的时候,意味着安装包在App Store上下载的时候,有的设备下载的安装包大小会超过100M。对于超过100M的安装包,只能在WIFI环境下下载,不能直接通过4G网络进行下载。
在这里,我们提交App Store的安装包大小为67.6MB,在App Store上显示的下载大小和实际下载下来的大小,我们通过下表做一个对比:
iPhone型号 | 系统 | AppStore 显示大小 | 下载到设备大小 |
---|---|---|---|
iPhone6 | 10.2.1 | 91.5MB | 88.9MB |
iPhone6 | 10.1.1 | 91.5MB | 88.9MB |
iPhone6 | 9.3.5 | 91.5MB | 84.8MB |
iPhone 5 | 9.2 | 91.5MB | 84.8MB |
iPhone6 plus | 10.0.2 | 95.7MB | 93.2MB |
iPhone7 plus | 10.3.0 | 95.7MB | 93.2MB |
iPhone5C | 9.2 | 83.9MB | 76MB |
iPhone5S | 7.1.1 | 147MB | 144MB |
iPhone5C | 7.1.2 | 147MB | 未知 |
iPhone5C 越狱 | 8.1.1 | 83.9MB | 144MB |
从上表可以看到:
优化安装包分为如下几个步骤:
首先进行第一步,分析安装包的构成: 88M的安装包解压后变成220MB。
ipa是一个压缩包, 安装包里的主要构成是(图片+文档+二进制文件),我们下面的分析
从上面来看,图片的压缩比最小。几乎没有压缩,这也说明每减少一张图片,就实实在在的减少了ipa的大小。 为了验证上面的数据,我们来做一些实验: 我们新建一个项目, 测试资源图片对安装包的大小的影响: 目录结构如下:
其中资源信息如下:
然后进行打包Archive→Export.得到IPA文件
从上面的结果来看,安装包的大小基本等于图片资源的大小,可以看一下IPA的内容详情视图(下图), 发现图片确实没有怎么压缩:
下面我们进一步使用ImageOptim对图片进行压无损缩优化(如下图)。看能否优化下安装包大小。
压缩后,总文件大小为:屏幕快照 2016-10-25 下午8.58.24.png, 优化掉了1MB的大小。 我们然后进行打包操作,最终的安装包确实也小了0.8MB,从11.6MB变成了10.8MB,。还是有优化的,如下图所示:
此时我们看xcode里的工程配置,COMPRESSPNGFILES 是YES的,有一些说法是这个变量的设置和ImageOptim冲突, 这里看起来不是如此。
是否因此可以完全确定ImageOptim的优化能力, 我觉得看情况而定, 上面的几种图片都是我iPhone手机里的相片导出的。是JPG的格式。
我们再对PNG做一些测试, 找一些资源图片放到工程中(我就不截图了,直截大小):
打包后的大小是:3.3M
系统帮我们优化掉1.1MB。 同样我们队图片进行一轮无损压缩优化, 经过ImageOptim优化后效果:
我们进行打包,得到的安装包的大小是:还是2.2MB(特意将系统优化关闭):
在我们项目中也对所有资源图片使用ImageOptim进行了优化,20多MB的资源最后优化掉1MB左右, 这里怀疑ImageOptim对PNG的优化能力一般。 而且如果能优化的PNG资源,系统默认可以进行一些优化。
从上面来看,文档有一定的压缩比,大概40%,也就是如果工程里有40MB的文档,体验到压缩包里大概是40*0.6=24MB。
二进制代码的压缩率是最高的。
上面都是研究不同的资源对最后安装包大小的影响情况,现在有了一些理论依据,我们就可以开始对安装包进行优化工作了。
所以先从图片开始优化吧:
指定搜索路径, 第二行(EXClude)指定哪些文件夹路径不被扫描。
然后这些资源可以清除掉了。但是存在着图片被误删除的可能性,譬如代码中使用图片的方式是:[UIImage imageNamed:[NSString stringWithFormat:”icon_%d.png”,index]]; 这种情况下,图片可能被误删,所以删除的时候不妨10张一组的进行,用眼睛过滤一遍。避免因为图片名字拼接,删除搜索出的结果。
现在网上有非常多的关于ImageOptim对资源图片进行无损压缩的方式, 在文章开头部分我们也对ImageOptim的优化能力进行了一些验证,通过上面的实验,我觉得结论不能确定,但值得一试。
文档资源主要是排查:
二进制包是由各种代码文件,静态库 动态库 经过编译后生成的可执行文件。 以头条二进制包125MB为例, 他是如何组成的?
上图可以看到armv7 占可执行文件的58MB。 arm64占可执行文件的66MB。 加起来=125MB。 进一步分析
通过右侧的pfile偏移可以大概算出每个段的大小,但不直观, 我们可以通过开启一些编译选项,生成可执行文件结构,然后借助一些工具生成更加直观的
XCode开启编译选项Write Link Map File XCode -> target -> Build Settings -> 搜map -> 把Write Link Map File选项设为yes,并指定好linkMap的存储位置:
编译后到编译目录里找到该txt文件,文件名和路径就是上述的Path to Link Map File。
~/Library/Developer/Xcode/DerivedData/XXX-eumsvrzbvgfofvbfsoqokmjprvuh/Build/Intermediates/XXX.build/Debug-iphoneos/XXX.build/。 这个LinkMap里展示了整个可执行文件的全貌,列出了编译后的每一个.o目标文件的信息(包括静态链接库.a里的),以及每一个目标文件的代码段,数据段存储详情
归类,去https://github.com/huanxsd/LinkMap 下载这个mac工程 然后运行。
譬如内部播放器sdk MediaPlayer在arm64架构下大小为4.92MB, armv7也可以分析另一份armv7 linkmap文件大概4.5MB 二者加起来就是在二进制占据的总大小—10MB左右。 通过对上面的文件进行分析,就知道每个类在最终的可执行文件中占据的大小。 然后有针对性的进行优化就可以了。
如果项目是很早之前(xcode4,5)建立的,迭代到现在 的确可以检查一下有利于减少安装包的编译选项:
将Enable C++ Exceptions和Enable Objective-C Exceptions设为NO,去掉异常支持; 如果你的项目比较大,有很多try cache, 想去掉这些异常可能是一个比较大的工作量,我在头条项目里尝试去掉了所有异常,打包测试,安装包大小没有 变化, 因为只是一个项目的测试,我只能比较怀疑去掉异常对最终安装包大小的优化能力。
iOS的项目,在安装包没有大到一定程度之前,研发和产品一般都关注较少,我们在平时的开发中也会有一些不好的习惯,在这里枚举一下:
1、解包:
file staticLibrary.a
2、抽离单个架构的.a
lipo staticLibrary.a -thin armv7 -output v7.a
3、抽离.a中的object
ar -x v7.a
4、从.o文件获取.m文件
nm View.o > view.m
删除重复定义
如果在项目中加入多个第三方库后出现类似下面的问题(XXX.o重复定义):
duplicate symbol _OBJCCLASS$_EAGLView in:
/Users/XXXname/Library/Developer/Xcode/DerivedData/XXObjext-gcnzomsbbunnlyfihyndulghsulr/Build/Products/Debug-iphonesimulator/libXXX.a(EAGLView.o)
/Applications/Xcode.app/Contents/Developer/Library/Frameworks/NChart3D.framework/NChart3D(EAGLView.o)
ld: 2 duplicate symbols for architecture i386
clang: error: linker command failed with exit code 1 (use -v to see invocation)
我们可以把其中一个.a中的.o删除掉来解决这一问题,解决方法步骤:
把一个.a命名为libx.a,在终端中cd到.a所属文件夹下,输入命令来查看包(.a)信息:lipo -info libx.a
如果提示fat file,那么代表这个包是支持多平台的,例如armv7,armv7s,i386等,这需要我们逐一做解包重打包操作。否则我们只需要做一次[1-6]操作即可。
1、创建临时文件夹,用于存放armv7平台解压后的.o文件:mkdir armv7
2、从.a中取出armv7平台的包:lipo libx.a -thin armv7 -output armv7/libx-armv7.a
3、查看armv7平台的包中所包含的文件列表:ar -t armv7/libx-armv7.a
4、从armv7平台的包中解压出object file(即.o后缀文件):cd armv7 && ar xv libx-armv7.a
5、找到冲突的包(EAGLView),删除掉rm EAGLView.o
6、重新打包object file(armv7平台包):cd .. && ar rcs libx-armv7.a armv7/*.o,可以再次使用[2]中命令确认是否已成功将文件去除
7、将其他几个平台(armv7s, i386)包逐一做上述[1-6]操作
8、重新合并为fat file的.a文件(去掉重复.o的):lipo -create libx-armv7.a libx-armv7s.a libx-i386.a -output libMiPushSDK-new.a
9、拷贝到项目中覆盖原来的.a文件就可以了。
我的问题虽然只有在i386下重复定义,但我还是把armv7和arm64下的也删除掉了。
tag:
缺失模块。
1、请确保node版本大于6.2
2、在博客根目录(注意不是yilia根目录)执行以下命令:
npm i hexo-generator-json-content --save
3、在根目录_config.yml里添加配置:
jsonContent: meta: false pages: false posts: title: true date: true path: true text: false raw: false content: false slug: false updated: false comments: false link: false permalink: false excerpt: false categories: false tags: true