静态库/内存分析/通讯录/换肤/硬件信息获取

时间:2022-12-27 00:07:55

静态库/内存分析/通讯录/换肤/硬件信息获取

一.静态库

1.静态库简介

  • 什么是库
    • 库就是程序代码的集合,是共享程序代码的一种方式
  • 库的分类
    • 开源库
      • 公开源代码,能看到具体实现
      • 例如Github上的著名开源库AFNetworking等
    • 闭源库
      • 不公开源代码,是经过编译后的二进制文件,看不到具体的实现
      • 主要分为:静态库和动态库
  • 静态库的存在形式
    • .a
    • .framework
  • 动态库的存在形式
    • .dylib
    • .framework
  • 静态库和动态库的区别
    • 静态库在链接时,会被完整的复制到可执行文件中,被多次使用,就有多份拷贝
    • 动态库则不会复制,只有一份,程序运行时动态加载到内存,系统只加载一次,多个程序公用,节省内存
    • 但是,项目中如果使用到自己的动态库,不允许上架
    • 然而,在WWDC2014上公布的苹果对iOS8开放动态加载dylib的接口,也就是说开放了动态库挂载
  • 静态库的应用场景
    • 保护自己的核心代码
      • 国内的企业,掌握有核心技术,同时是又希望更多的程序员来使用其技术,因此采用"闭源"的方式开发使用
      • 例如,百度地图,友盟,JPush等
    • 将MRC项目打包成静态库,可以在ARC下直接使用,不需要转换
  • 静态库的特点
    • .a + .h
    • 看不到具体实现的代码

2.静态库的制作(.a)

  • 生成静态库的步骤
    1. 创建项目时,直接选择静态库(.a)
    2. 设置需要暴露的头文件
      • TARGETS->Build Phases->Copy Files->把需要暴露的头文件添加进来即可
    3. 在模拟器环境下编译(得到模拟器环境下的静态库,选择模拟器6s,编译)
    4. 在真机环境下编译(得到真机环境下的静态库)
  • 静态库使用测试
    • 使用模拟器的静态库,拖入测试工程
      • 使用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静态库的区别
    1. .a是一个纯二进制文件,.framework中除了有二进制文件外还有资源文件
    2. .a文件不能直接使用,至少有.h文件的配合,.framework文件可以直接使用
    3. .a + .h + sourceFile = .framework
    4. 建议使用.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:方法)来加载图片,不会有缓存
        • 结论
          • 小图片/使用频率较高的图片
            • 放在Images.xcassets里面
          • 大图片/使用频率较低的图片(一次性的图片,比如版本新特性的图片)
            • 不要放在Image.xcassets里面
      • 经验:怎样解压.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.通讯录简介

  • 应用场景
    • 即时通讯APP,关联联系人
  • 通讯录获取方案
    • AddressBookUI.framework框架
      • 提供了联系人列表界面,联系人详情界面,添加联系人界面
      • 一般用于选择联系人
    • AddressBook.framework框架
      • 纯C语言的API,仅仅是获得联系人的数据
      • 没有提供UI界面展示,需要自己搭建联系人展示界面
      • 里面的数据类型大部分基于CoreFoundation框架,使用起来将其蛋疼
      • 从iOS6开始,需要得到用户的授权才能访问通讯录,因此在使用之前,需要检查用户是否已经授权
    • 第三方框架RHAddressBook
      • 对AddressBook.framework框架的封装
    • iOS9.0最新通讯录获取框架
      • ContactsUI.framework(方案1的替代品)
        • 特点:面向对象,使用简单,有界面
      • Contacts.framework(方案2的替代品)
        • 特点:面向对象,使用简单,*面

2.获取通讯录(AddressBookUI)

  • 实现步骤
    1. 创建选择联系人的控制器
    2. 设置代理(用来接收用户选择的联系人信息)
    3. 弹出联系人控制器
    4. 实现代理
    5. 在对应的代理方法中获取联系人信息
  • 具体代码实现
    1. 创建选择联系人的控制器
      • ABPeoplePickerNavigationController *ppnc = [[ABPeoplePickerNavigationController alloc] init];
    2. 设置代理(用来接收用户选择的联系人信息)
      • ppnc.peoplePickerDelegate = self;
    3. 弹出联系人控制器
      • [self presentViewController:ppnc animated:YES completion:nil];
    4. 实现代理
      • 选中某个联系人时调用
        • peoplePickerNavigationController:didSelectPerson
      • 选中某个联系人某个属性时调用
        • peoplePickerNavigationController:didSelectPerson:property:identifier
      • 点击了取消按钮会执行的方法
        • peoplePickerNavigationControllerDidCancel:
    5. 在对应的代理方法中获取联系人信息
      • 使用须知
        • 属性分类
          • 简单属性:姓和名等
          • 复杂属性:电话号码/电子邮件等,如果是复杂属性,那么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);
      • 释放不在使用的对象
        • CFRelease(phones);

3.获取通讯录(AddressBook)

  • 实现步骤
    1. 请求授权
    2. 判断授权状态,如果已授权,则继续;未授权,则提示用户,并返回
    3. 创建通讯录对象
    4. 从通信录对象中,获取所有的联系人
    5. 遍历所有的联系人
    6. 释放不再使用的对象
  • 代码实现
    1. 请求授权
      • 获取授权状态
        • ABAuthorizationStatus status = ABAddressBookGetAuthorizationStatus();
      • 判断授权状态,如果是未决定状态,才需要请求
        • if (status != kABAuthorizationStatusNotDetermined) return;
      • 创建通讯录对象
        • ABAddressBookRef addressBook = ABAddressBookCreateWithOptions(NULL, NULL);
      • 请求授权
        • ABAddressBookRequestAccessWithCompletion(addressBook, block)
      • info.plist对通讯录的使用说明
        • Privacy - Contacts Usage Description
    2. 判断授权状态,如果已授权,则继续;未授权,则提示用户,并返回
      • ABAuthorizationStatus status = ABAddressBookGetAuthorizationStatus();
      • 如果用户没有授权,返回
        • if (status != kABAuthorizationStatusAuthorized) return;
    3. 创建通讯录对象
      • ABAddressBookRef addressBook = ABAddressBookCreateWithOptions(NULL, NULL);
    4. 从通信录对象中,获取所有的联系人
      • CFArrayRef peopleArray = ABAddressBookCopyArrayOfAllPeople(addressBook);
    5. 遍历所有的联系人
      • 遍历所有的联系人(每一个联系人都是一条记录)
        • 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);
    6. 释放不再使用的对象
      • 可以通过静态内存分析检测
      • 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.知识补充

  • Unmanaged
  • UnsafePointer

四.换肤

1.换肤的应用场景

  • 一般应用在某些APP,在节假日更换主题,或者切换白天/夜间模式的时候使用

2.演练步骤

  1. 实现基本的换肤功能,直接替换图片
    • 无缓存主题
  2. 使用用户偏好缓存当前皮肤主题,方便下次进来后依然是上次所选主题
    • 代码冗余,重用性差
  3. 在控制器中直接抽取对应方法,简化代码
    • 代码重用性差,如果在别的控制器需要获取或者设置当前主题时,又需要将代码拷贝一份
    • 逻辑分工不明确,控制器不应该关心具体如何存储主题,获取主题
  4. 抽取公共的皮肤管理类,简化控制器逻辑
    • 存在问题:需要写主题名称,一个字符串容易写错
    • 解决方案:由工具类提供一个枚举类型,供外界直接选择
  5. 优化工具类,使用全局常量和枚举
    • 存在问题:外界获取到主题名称的主要目的,就是要拼接该主题下的图片(背景图片/按钮图片),这个不是控制器负责的事情
    • 解决方案:由工具类提供当前主题下的背景图片和按钮图片
  6. 再次优化工具类,增加提供当前主题下对应图片的方法
    • 存在问题:图片素材名称跟主题名称相关,美工作图命名比较复杂(对开发人员无碍)
    • 解决方案:改成靠文件夹进行区分不同主题
  7. 使用文件夹,对主题图片进行分类
    • 虚拟文件夹,无法再APP的mainBundle中创建物理路径
  8. 直接使用bundle文件夹
  9. 实现主题颜色

五.硬件信息的获取

1.硬件信息获取简介

  • 功能
    1. 设备的型号
    2. 设备的CPU型号/使用情况
    3. 设备的内存容量/使用情况
    4. 设备的硬盘容量/使用情况
  • 应用场景
    • 社交软件发状态时,显示手机型号
    • 下载软件下载文件时,提示剩余空间
  • 实现方案
    • 直接通过第三方工具类(UIDevice的分类),进行获取对应信息
      • UIDevice-extension
    • 原因:自己写起来比较复杂,很多C语言的东西,而且没有必要

2.框架完善

  • 框架存在问题:该第三方框架从2012年就停止更新了,意味着12年之后的手机型号都没有,需要手动添加,修改框架
  • 解决方案:找到对应的实现方法,使用真机进行测试,手动新增手机型号

3.补充框架

六.课后问题

1.静态库打包注意点

  1. 确认是否是静态库
    • .a肯定是
    • .framework mach-o type 静态库
  2. 确定支持所有架构
    • build set->build active->NO
  3. 静态库,应该给别人的是release版本
  4. 一边开发,一边调试(复合工程)

2.简单说下,QQ推荐联系人的流程

  1. 创建选择联系人控制器
  2. 设置代理
  3. 弹出联系人选择控制器
  4. 实现代理
  5. 在代理方法中获得联系人信息