静态库/内存分析/通讯录/换肤/硬件信息获取
一.静态库
1.静态库简介
- 什么是库
- 库的分类
- 开源库
- 公开源代码,能看到具体实现
- 例如Github上的著名开源库AFNetworking等
- 闭源库
- 不公开源代码,是经过编译后的二进制文件,看不到具体的实现
- 主要分为:静态库和动态库
- 静态库的存在形式
- 动态库的存在形式
- 静态库和动态库的区别
- 静态库在链接时,会被完整的复制到可执行文件中,被多次使用,就有多份拷贝
- 动态库则不会复制,只有一份,程序运行时动态加载到内存,系统只加载一次,多个程序公用,节省内存
- 但是,项目中如果使用到自己的动态库,不允许上架
- 然而,在WWDC2014上公布的苹果对iOS8开放动态加载dylib的接口,也就是说开放了动态库挂载
- 静态库的应用场景
- 保护自己的核心代码
- 国内的企业,掌握有核心技术,同时是又希望更多的程序员来使用其技术,因此采用"闭源"的方式开发使用
- 例如,百度地图,友盟,JPush等
- 将MRC项目打包成静态库,可以在ARC下直接使用,不需要转换
- 静态库的特点
2.静态库的制作(.a)
- 生成静态库的步骤
- 创建项目时,直接选择静态库(.a)
- 设置需要暴露的头文件
- TARGETS->Build Phases->Copy Files->把需要暴露的头文件添加进来即可
- 在模拟器环境下编译(得到模拟器环境下的静态库,选择模拟器6s,编译)
- 在真机环境下编译(得到真机环境下的静态库)
- 静态库使用测试
- 使用模拟器的静态库,拖入测试工程
- 使用6s模拟器进行测试->通过
- 使用真机,编译->失败
- 使用地型号模拟器测试->失败
- 测试结果分析
- 模拟器下的静态库和真机下的静态库不能共用
- 主要原因是模拟器和真机CPU架构不一样(各个模拟器型号之间的架构也不一样)
- 注意静态库所支持的架构
- 不同机型的CPU对应的架构不同
- 模拟器
- 4s->5:i386
- 5s->6s Plus:x86_64
- 真机
- 3gs->4s:armv7
- 5/5c:armv7s(armv7兼容armv7s)
- 5s->6s Plus:arm64
- 查看静态库支持的架构
- lipo -info 库文件
- 分别选中不同的模拟器,进行编译,查看不同的静态库支持架构
- 怎样一次编译支持多个架构的静态库
- 正常情况下,需要选中每一个模拟器进行编译,生成支持对应架构的静态库,然后合并,非常繁琐
- 解决方案:build settings->build active->NO
- 表示不止编译活跃的架构,让所有的架构都编译
- 静态库文件的版本
- 调试版本
- 真机(debug版本)
- 模拟器(debug版本)
- 特点:
- 调试版本会包含完整的符号信息,以方便调试
- 调试版本不会对代码进行优化
- 发布版本
- 真机(Release版本)
- 模拟器(Release版本)
- 特点:
- 发布版本不会包含完整的符号信息
- 发布版本的执行代码是进行过优化的
- 发布版本的大小比调试版本的略小
- 在执行速度方面,发布版本会更快一些,但不意味着有显著的提升
- 怎样生成不同版本
- 项目->Edit Scheme->Run->Release/Debug分别进行编译
- 如果想要一个静态库,既可以在模拟器上运行,也可以在真机上运行怎么做
- 因为静态库针对于模拟器和真机生成了不同的版本(支持不同架构),所以没法同时运行
- 解决方案:静态库合并
- 检测.a类型
- $ lipo -info libWJQTools.a
- 合并.a
- lipo -create Debug-iphoneos/libTools.a Debug-iphonesimulator/libTools.a -output libTools.a
- 特点
- 合并.a的好处,开发过程中既可以在真机上调试,也可以在模拟器上调试
- 合并.a的坏处,如果静态库太大,合并打包后,会非常大,因此很多第三方的静态库的.a是区分版本的
- 今后在使用.a时一定注意版本
3.静态库的制作(.framework)
- 新建工程,直接选择.framework静态库
- 编译时,设置编译所有架构
- TARGETS->Build Settings->Architecture->Build Active Architecture Only->NO
- 默认制作的是动态库,需要设置链接类型
- TARGETS->Build Setting->Linking->Mach-o Type->改为静态库
4.总结
- 静态库打包的完整步骤
- 确定是静态库
- .a肯定是静态库
- .framework的需要设置链接类型
- targets->build setting->搜索Mach-o Type->改为静态库
- 确定支持模拟器或真机中的所有架构
- Build setting->build active->NO
- 表示不止编译活跃的架构,让所有的架构都编译
- 提供的静态库应该是release版本
- 项目->Edit Scheme->Run->Release/Debug分别进行编译
- .a静态库和.framework静态库的区别
- .a是一个纯二进制文件,.framework中除了有二进制文件外还有资源文件
- .a文件不能直接使用,至少有.h文件的配合,.framework文件可以直接使用
- .a + .h + sourceFile = .framework
- 建议使用.framework
- 静态库开发中的常见问题
- 有些第三方库会使用到一些图片素材,例如公司logo等
- 由于Xcode默认在编译时会把所有的素材文件导入到mainBundle中,可能与使用静态库的程序冲突
- 解决方案:在静态库中如果要使用图片素材,会利用bundle手段
- 建立bundle,并向其中添加图片
- 创建一个类方法,返回图片
- 编译
- 调用方如果需要使用,要导入.h + .a + xxx.bundle
- 如果用户需要导入的头文件过多怎么添加
- 建议使用一个主头文件包含其他头文件,让用户只导入一个主头文件
- 静态库程序怎样测试
- 静态库本身就是一个小项目,实现某些功能,但是这些功能再开发中也需要测试,而测试代码又不能作为静态库的一部分
- 解决方案:创建复合项目
5.将MRC的项目打包成静态库,可以在ARC下直接使用,不需要转换
- Objective-C Automatic Reference Counting->NO
6.swift打包动态库
二.内存分析
- 主要目的
- 静态内存分析
- 概念
- 静态内存分析是不运行程序,直接对代码进行分析
- 根据代码的上下文的语法结构,来分析内存状况
- 作用
- 调整环境到MRC
- 逻辑错误:访问未初始化的变量或者野指针等
- 声明错误:从未使用过的对象
- 内存管理错误:如内存泄漏等(包括MRC和ARC)
- 缺点
- 不一定准确,但是如果发现有提示,那么去结合上下文看一下,这里的代码是否有问题
- 场景演练(OC)
- MRC下桥接
- Foundaton->CoreFoundation数据类型转换
- CoreFoundation->Foundation数据类型转换
- ARC下桥接
- Foundaton->CoreFoundation数据类型转换
- __bridge,不会移交对象内存管理所有权
- CFBridgingRetain=__bridge_retained,会移交对象内存管理所有权
- CoreFoundation->Foundation数据类型转换
- __bridge,不会移交对象内存管理所有权
- CFBridgingRelease=__bridge_transfer,会移交对象内存管理所有权
- MRC->ARC环境的切换方式
- target->build setting->搜索automatic reference counting
- 关于swift中使用CoreFoundation数据类型
- 使用了"类型重映射"机制,转换成为了能够自动管理内存的映射,不需要我们手动释放
- 内存分配
- 作用
- 场景演示
- UIImage的两种创建方法测试
- imageNamed:图片会在内存中有缓存,而且不会被释放
- imageWithContentOfFile:图片没有缓存,可以被释放
- 概念解释
- Anonymous VM(匿名虚拟内存)是系统为程序预留的,可能会立即被重复使用的一部分可用内存
- 补充:图片使用技巧
- 图片在沙盒中的存在形式
- 如果项目的Deployment Target <= 6.x(不支持图片压缩)
- 所有图片直接暴露在沙盒的资源包(mainBundle),不会压缩到Assets.car文件
- 如果项目的Deployment Target >= 7.x(支持图片压缩)
- 放在Images.xcassets里面的所有图片会压缩到Assets.car文件,不会直接暴露在沙盒的资源包(mainBundle)
- 没有放在Images.xcassets里面的所有图片会直接暴露在沙盒的资源包(mianBundle),不会压缩到Assets.car文件
- 使用对比
- 会压缩到Assets.car文件,没有直接暴露在沙盒的资源包
- 条件:
- Deployment Target >= 7.x
- 放在Images.xcassets里面的所有图片
- 影响:
- 无法得到图片的全路径,只能通过图片名(imageNamed:方法)来加载图片,永远会有内存
- 不会压缩到Assets.car文件,直接暴露在沙盒的资源包(mainBundle)
- 除上述条件以外的所有情况
- 影响:可以得到图片的全路径,可以通过全路径(imageWithContentOfFile:方法)来加载图片,不会有缓存
- 结论
- 小图片/使用频率较高的图片
- 大图片/使用频率较低的图片(一次性的图片,比如版本新特性的图片)
- 经验:怎样解压.car压缩包,获取资源
- 动态内存分析
- 作用:检测程序在运行过程中是否存在内存泄漏
- 场景演示:模拟循环引用,测试内存泄漏
- 内存使用总结(如何让程序尽量减少内存泄漏)
- 非ARC
- Foundation对象(OC对象)
- 只要方法中包含了alloc/new/copy/mutableCopy/retain等关键字,那么这些方法产生的对象,就必须在不再使用的时候调用1次1次release或者autorelease
- CoreFoundation对象(C对象)
- 只要函数中包含了create/new/copy/retain等关键字,那么这些方法产生的对象,就必须在不再使用的时候调用一次CFRelease或者其他release函数
- ARC
- 只自动管理OC对象,不会自动管理C语言对象
- CoreFoundation对象
- 只要函数中包含了create\new\copy\retain等关键字, 那么这些方法产生的对象, 就必须在不再使用的时候调用1次CFRelease或者其他release函数
- 如果是swift里面,使用CoreFoundation对象,不需要手动释放,因为使用了"类型重映射"机制,可以把对象转换成为能够自动管理内存的对象
三.通讯录获取
1.通讯录简介
- 应用场景
- 通讯录获取方案
- AddressBookUI.framework框架
- 提供了联系人列表界面,联系人详情界面,添加联系人界面
- 一般用于选择联系人
- AddressBook.framework框架
- 纯C语言的API,仅仅是获得联系人的数据
- 没有提供UI界面展示,需要自己搭建联系人展示界面
- 里面的数据类型大部分基于CoreFoundation框架,使用起来将其蛋疼
- 从iOS6开始,需要得到用户的授权才能访问通讯录,因此在使用之前,需要检查用户是否已经授权
- 第三方框架RHAddressBook
- 对AddressBook.framework框架的封装
- iOS9.0最新通讯录获取框架
- ContactsUI.framework(方案1的替代品)
- Contacts.framework(方案2的替代品)
2.获取通讯录(AddressBookUI)
- 实现步骤
- 创建选择联系人的控制器
- 设置代理(用来接收用户选择的联系人信息)
- 弹出联系人控制器
- 实现代理
- 在对应的代理方法中获取联系人信息
- 具体代码实现
- 创建选择联系人的控制器
- ABPeoplePickerNavigationController *ppnc = [[ABPeoplePickerNavigationController alloc] init];
- 设置代理(用来接收用户选择的联系人信息)
- ppnc.peoplePickerDelegate = self;
- 弹出联系人控制器
- [self presentViewController:ppnc animated:YES completion:nil];
- 实现代理
- 选中某个联系人时调用
- peoplePickerNavigationController:didSelectPerson
- 选中某个联系人某个属性时调用
- peoplePickerNavigationController:didSelectPerson:property:identifier
- 点击了取消按钮会执行的方法
- peoplePickerNavigationControllerDidCancel:
- 在对应的代理方法中获取联系人信息
- 使用须知
- 属性分类
- 简单属性:姓和名等
- 复杂属性:电话号码/电子邮件等,如果是复杂属性,那么ABRecordCopyValue函数返回的就是ABMultiValueRef类型的数据
- 使用方式
- 使用ABRecordCopyValue可以从一条Person记录中获取到对应的记录,但是后续处理则需要根据记录的具体类型加以区分
- ABRecordCopyValue函数接收两个参数
- 第一个参数是ABRecordRef实例
- 第二个参数是属性关键字,定义在ABPerson.h中
- 关于获取的key,定义在ABPerson.h文件中
- 获取选中联系人的姓名(姓lastname/名firstname)
- CFStringRef firstname = ABRecordCopyValue(person, kABPersonFirstNameProperty);
- CFStringRef lastname = ABRecordCopyValue(person, kABPersonLastNameProperty);
- NSString *firstName = (__bridge_transfer NSString *)(firstname);
- NSString *lastName = (__bridge_transfer NSString *)(lastname);
- 获取联系人的电话号码
- ABMultiValueRef phones = ABRecordCopyValue(person, kABPersonPhoneProperty);
- CFIndex count = ABMultiValueGetCount(phones);
- for (CFIndex i = 0; i < count; i++)
- NSString *phoneLabel = (__bridge_transfer NSString *)ABMultiValueCopyLabelAtIndex(phones, i);
- NSString *phoneValue = (__bridge_transfer NSString *)ABMultiValueCopyValueAtIndex(phones, i);
- 释放不在使用的对象
3.获取通讯录(AddressBook)
- 实现步骤
- 请求授权
- 判断授权状态,如果已授权,则继续;未授权,则提示用户,并返回
- 创建通讯录对象
- 从通信录对象中,获取所有的联系人
- 遍历所有的联系人
- 释放不再使用的对象
- 代码实现
- 请求授权
- 获取授权状态
- ABAuthorizationStatus status = ABAddressBookGetAuthorizationStatus();
- 判断授权状态,如果是未决定状态,才需要请求
- if (status != kABAuthorizationStatusNotDetermined) return;
- 创建通讯录对象
- ABAddressBookRef addressBook = ABAddressBookCreateWithOptions(NULL, NULL);
- 请求授权
- ABAddressBookRequestAccessWithCompletion(addressBook, block)
- info.plist对通讯录的使用说明
- Privacy - Contacts Usage Description
- 判断授权状态,如果已授权,则继续;未授权,则提示用户,并返回
- ABAuthorizationStatus status = ABAddressBookGetAuthorizationStatus();
- 如果用户没有授权,返回
- if (status != kABAuthorizationStatusAuthorized) return;
- 创建通讯录对象
- ABAddressBookRef addressBook = ABAddressBookCreateWithOptions(NULL, NULL);
- 从通信录对象中,获取所有的联系人
- CFArrayRef peopleArray = ABAddressBookCopyArrayOfAllPeople(addressBook);
- 遍历所有的联系人
- 遍历所有的联系人(每一个联系人都是一条记录)
- CFIndex peopleCount = CFArrayGetCount(peopleArray);
- 开始遍历
- for (CFIndex i = 0; i < peopleCount; i++)
- 获取到联系人
- ABRecordRef person = CFArrayGetValueAtIndex(peopleArray, i);
- 获取姓名
- NSString *lastname = (__bridge_transfer NSString *)ABRecordCopyValue(person, kABPersonLastNameProperty);
- NSString *firstName = (__bridge_transfer NSString *)ABRecordCopyValue(person, kABPersonFirstNameProperty);
- 打印并结束
- NSLog(@"%@ %@", lastname, firstName);
- 释放不再使用的对象
- 可以通过静态内存分析检测
- CFRelease(peopleArray)
- CFRelease(addressBook)
4.获取通讯录(第三方框架RHAddressBook)
- 实现步骤
- 具体实现
- 集成框架
- 将整个工程拖入项目
- 添加工程依赖
- build phases->target dependencies->+
- 添加链接项
- build settings->other linker flages->"-ObjC -all_load"
- 导入框架头文件
- #import <RHAddressBook/AddressBook.h>
- 使用框架获取所有联系人信息
- 请求授权
- 获取授权状态
- RHAuthorizationStatus status = [RHAddressBook authorizationStatus];
- 如果授权状态用户未选择,则请求授权
- if (status == RHAuthorizationStatusNotDetermined)
- 创建通讯录对象
- RHAddressBook *addressBook = [[RHAddressBook alloc] init];
- 请求授权
- [addressBook requestAuthorizationWithCompletion:block];
- 获取联系人信息
- 判断当前授权状态
- RHAuthorizationStatus status = [RHAddressBook authorizationStatus];
- if (status != RHAuthorizationStatusAuthorized) return;
- 创建通讯录对象
- RHAddressBook *addressBook = [[RHAddressBook alloc] init];
- 获取所有联系人
- NSArray *peoples = addressBook.people;
- 遍历联系人
- for (RHPerson *person in peoples)
- 获取联系人姓名
- NSString *firstName = person.firstName;
- NSString *lastName = person.lastName;
- 获取联系人电话
- 获取所有联系人电话, 开始遍历
- RHMultiStringValue *mv = person.phoneNumbers;
- for (int i = 0; i < mv.count; i ++)
- 获取电话标签
- NSString *label = [mv labelAtIndex:i];
- 获取电话号码
- NSString *phone = [mv valueAtIndex:i];
5.获取通讯录(iOS9.0新框架简单实用)
- 学习方式:修改部署版本为iOS9.0,让Xcode报警告,然后找到替换方法,逐个进行替换
- ContactsUI.framework的使用
- 使用步骤
- 创建选择联系人界面
- CNContactPickerViewController
- 设置代理
- 弹出选择联系人界面的控制器
- 获取联系人
- 实现对应的代理方法
- 如果实现了选择联系人的代理方法,则无法进入详情界面
- 获取联系人的姓名
- 获取联系人的电话号码
// 1.创建选择联系人的控制器
CNContactPickerViewController *cpvc = [[CNContactPickerViewController alloc] init];
// 2.设置代理
cpvc.delegate = self;
// 3.弹出控制器
[self presentViewController:cpvc animated:YES completion:nil];
- (void)contactPicker:(CNContactPickerViewController *)picker didSelectContact:(CNContact *)contact
- (void)contactPicker:(CNContactPickerViewController *)picker didSelectContactProperty:(CNContactProperty *)contactProperty
// 1.获取选中联系人的姓名
NSString *firstName = contact.givenName;
NSString *lastName = contact.familyName;
NSLog(@"%@ %@", firstName, lastName);
// 2.获取电话号码
for (CNLabeledValue *phone in contact.phoneNumbers) {
CNPhoneNumber *phoneNumber = phone.value;
NSLog(@"%@ %@", phone.label, phoneNumber.stringValue);
}
- Contacts.framework的使用
- 请求授权
- 获取联系人
- 获取授权状态
- 判断是否是已授权状态
- 创建联系人仓库
- 创建联系人的请求对象
- 获取用户的姓名
- 获取电话号码
// 1.获取授权状态
CNAuthorizationStatus status = [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts];
// 2.如果是未决定,请求授权
if (status == CNAuthorizationStatusNotDetermined) {
CNContactStore *store = [[CNContactStore alloc] init];
[store requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) {
if (granted) {
NSLog(@"授权成功");
}
}];
}
// 1.获取授权状态
CNAuthorizationStatus status = [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts];
// 2.如果不是已经授权,则直接返回
if (status != CNAuthorizationStatusAuthorized) return;
// 3.获取联系人
// 3.1.创建联系人仓库
CNContactStore *store = [[CNContactStore alloc] init];
// 3.2.创建联系人的请求对象
// keys决定这次要获取哪些信息,比如姓名/电话
NSArray *fetchKeys = @[CNContactGivenNameKey, CNContactFamilyNameKey, CNContactPhoneNumbersKey];
CNContactFetchRequest *request = [[CNContactFetchRequest alloc] initWithKeysToFetch:fetchKeys];
// 3.3.请求联系人
NSError *error = nil;
[store enumerateContactsWithFetchRequest:request error:&error usingBlock:^(CNContact * _Nonnull contact, BOOL * _Nonnull stop) {
// stop是决定是否要停止
// 1.获取姓名
NSString *firstname = contact.givenName;
NSString *lastname = contact.familyName;
NSLog(@"%@ %@", firstname, lastname);
// 2.获取电话号码
NSArray *phones = contact.phoneNumbers;
// 3.遍历电话号码
for (CNLabeledValue *labelValue in phones) {
CNPhoneNumber *phoneNumber = labelValue.value;
NSLog(@"%@ %@", phoneNumber.stringValue, labelValue.label);
}
}];
6.知识补充
四.换肤
1.换肤的应用场景
- 一般应用在某些APP,在节假日更换主题,或者切换白天/夜间模式的时候使用
2.演练步骤
- 实现基本的换肤功能,直接替换图片
- 使用用户偏好缓存当前皮肤主题,方便下次进来后依然是上次所选主题
- 在控制器中直接抽取对应方法,简化代码
- 代码重用性差,如果在别的控制器需要获取或者设置当前主题时,又需要将代码拷贝一份
- 逻辑分工不明确,控制器不应该关心具体如何存储主题,获取主题
- 抽取公共的皮肤管理类,简化控制器逻辑
- 存在问题:需要写主题名称,一个字符串容易写错
- 解决方案:由工具类提供一个枚举类型,供外界直接选择
- 优化工具类,使用全局常量和枚举
- 存在问题:外界获取到主题名称的主要目的,就是要拼接该主题下的图片(背景图片/按钮图片),这个不是控制器负责的事情
- 解决方案:由工具类提供当前主题下的背景图片和按钮图片
- 再次优化工具类,增加提供当前主题下对应图片的方法
- 存在问题:图片素材名称跟主题名称相关,美工作图命名比较复杂(对开发人员无碍)
- 解决方案:改成靠文件夹进行区分不同主题
- 使用文件夹,对主题图片进行分类
- 虚拟文件夹,无法再APP的mainBundle中创建物理路径
- 直接使用bundle文件夹
- 实现主题颜色
五.硬件信息的获取
1.硬件信息获取简介
- 功能
- 设备的型号
- 设备的CPU型号/使用情况
- 设备的内存容量/使用情况
- 设备的硬盘容量/使用情况
- 应用场景
- 社交软件发状态时,显示手机型号
- 下载软件下载文件时,提示剩余空间
- 实现方案
- 直接通过第三方工具类(UIDevice的分类),进行获取对应信息
- 原因:自己写起来比较复杂,很多C语言的东西,而且没有必要
2.框架完善
- 框架存在问题:该第三方框架从2012年就停止更新了,意味着12年之后的手机型号都没有,需要手动添加,修改框架
- 解决方案:找到对应的实现方法,使用真机进行测试,手动新增手机型号
3.补充框架
六.课后问题
1.静态库打包注意点
- 确认是否是静态库
- .a肯定是
- .framework mach-o type 静态库
- 确定支持所有架构
- build set->build active->NO
- 静态库,应该给别人的是release版本
- 一边开发,一边调试(复合工程)
2.简单说下,QQ推荐联系人的流程
- 创建选择联系人控制器
- 设置代理
- 弹出联系人选择控制器
- 实现代理
- 在代理方法中获得联系人信息