如有任何疑问或问题请联系我:fishnewsdream@gmail.com,欢迎交流,共同提高!
Objective-C/Swift技术开发交流群201556264,讨论何种技术并不受限,欢迎各位大牛百家争鸣!
微信公众号OldDriverWeekly,欢迎关注并提出宝贵意见
老司机iOS周报,欢迎关注或订阅
刚刚在线工作室,欢迎关注或提出建设性意见!
刚刚在线论坛, 欢迎踊跃提问或解答!
如有转载,请注明出处,谢谢!
使用技术: 逆向工程,是一种产品设计技术再现的过程,即对一项目标产品进行逆向分析及研究,从而演绎并得出该产品的处理流程、组织结构、功能特性及技术规格等设计要素,其主要目的是在不能轻易获得必要的生产信息的情况下,直接从成品分析,推导出产品的设计原理。
分析对象: 某程、某龙
分析方法: 静态分析法,在不执行iOS应用的情形下,对应用进行静态分析的一种方法。比如获取应用的文件系统结构,本地文件的分析、使用反汇编工具(Disassembler,比如IDA)查看内部代码,分析代码结构也是静态分析。
app bundle 分析
提取.h文件分析
提取可执行文件包含字符串分析
沙盒文件分析
页面结构分析


iTunesArtwork 用浏览器打开,是1024的app icon。
META-INF目录:存放签名信息
Payload(资源存放文件 )
- 资源包中包含众多资源文件(图片、plist文件、Assets.car、本地化文件、签名相关文件(CodeResources、SC_Info)及其他资源文件)
iOS7后可使用Images.cxassets,打包后会生成Assets.car文件。可使用工具打开这个文件
也可以直接下载 百度云下载地址

加入到Assets.xcassets中
- 只支持png格式的图片
- 图片只支持[UIImage imageNamed]的方式实例化,但是不能从Bundle中加载
- 在编译时,Images.xcassets中的所有文件会被打包为Assets.car的文件
CreateGroup
- 黄色文件夹图标;Xcode中分文件夹,Bundle中所有所在都在同一个文件夹下,因此,不能出现文件重名的情况
- 可以直接使用[NSBundle mainBundle]作为资源路径,效率高!
- 可以使用[UIImage imageNamed:]加载图像
CreateFolderRefences
- 蓝色文件夹;Xcode中分文件夹,Bundle中同样分文件夹,因此,可以出现文件重名的情况
- 需要在[NSBundle mainBundle]的基础上拼接实际的路径,效率较差
- 不能使用[UIImage imageNamed:]加载图
PDFs矢量图(Xcode6+)
Bundle(包)中的图片素材
在给ipa包瘦身的时候,可以使用图片压缩工具,通过无损或有损压缩的方式减小图片大小,还可以通过改变图片导入的方式给ipa包瘦身。携程、艺龙均有使用Images.xcassets。
CreateGroup、CreateFolderRefences两种方式打出来的包,图片都会直接放在.app文件中,所以打包前后,图片的大小不会改变,而加入到Assets.xcassets中的方法则不同,打包后,在.app中会生成Assets.car文件来存储Assets.xcassets中的图片,并且文件大小也大大降低。也可使用工具给图片压缩,比如 imageoptim。
其他可以给ipa包瘦身的也有通过设置Bitcode、设置编译器优化级别、去除符号信息甚至修改方法名长度等。
- 通过逆向工程相关技术(class-dump-z),拿到沙盒文件、.h文件集合等。


某龙:
有大量图片资源使用中文命名
部分图片包含了1倍图片,部分图片只有@2x图片,使用了Images.assets
文件中包含momd文件,使用了CoreData
Frameworks文件夹中没有包含Swift需要的库,没有使用Swift
JSPatch文件
Plist文件中统计页面及事件
全局搜索配置(CoreSpotlight.plist)
webp引导页(减小大小)
配置url跳转(xxxRoute.plist)
某程:
部分图片包含了1倍图片,部分图片只有@2x图片,使用了Images.assets
图片命名均以_分隔,形如
call\_ico\_conceal\_click@2x项目中没有使用Swift
JSPatch文件
相关Plist文件中存储SQL语句
内置H5页面(webapp)
Plist文件配置相关电话
开发员工配置(BundleVersion.json)
配置工程依赖(crip.json)
某龙:
部分Model文件以model结尾,类名没有统一加前缀
使用了Cocoapods
某程:
Model文件命名以model结尾,类名没有统一加前缀
没有使用Cocoapods
CNPPopupController
简单和通用的弹出控件

FXBlurView
实现背景模糊效果


FXLabel

BlocksKit
BlocksKit 是一个开源的框架,对 Cocoa 进行了扩展,将许多需要通过 delegate 调用的方法转换成了 block。在很多情况下,blocks 比 delegate 要方便简单,因为 block 是紧凑的,可以使代码更简洁,提高代码可读性,另外 block 还可以进行异步处理。使用 block 要注意避免循环引用。
BlocksKit 的所有方法都以bk_开头,这样可以方便地列出所有 BlocksKit 的所有方法。最常用的是 UIKit Category,它为 UIAlertView,UIActionSheet,UIButton,UITapGestureRecognizer 等提供了 blocks。BlocksKit 主要目录结构
Core:存放 Foundation 相关的 Block category,如 NSObject、NSTimer、NSarray、NSDictionary、NSSet、NSIndexSet、NSMutableArray等
DynamicDelegate:动态代理(消息转发机制)
UIKit:扩展了 UIAlertView,UIActionView,UIButton 等
主要功能有下面三个方面:
通过block传入事件处理函数
创建动态代理,传入block给想要实现的方法。
在很多基础的类上增加额外的方法。


当一个对象收到它没实现的消息的时候,通常会发生如下的情况。
- 调用+(BOOL)resolveInstanceMethod:(SEL)aSEL,如果对象在这里动态添加了selector 的实现方法,则消息转发结束,否则执行步骤2
- 调用 - (id)forwardingTargetForSelector:(SEL)aSelector,在这里你可以将消息转发给其他对象,如果实现则消息转发结束,否则执行步骤3
- 执行完整的消息转发机制,调用-(void)forwardInvocation:(NSInvocation *)invocation 在这一步,你可以修改消息的任何内容,包括目标(target),selector,参数。如果没有实现在这里还未实现转发则程序将抛出异常。
BlocksKit 动态代理实现方式是最后一步,即-(void)forwardInvocation:(NSInvocation *)invocation,使得动态代理能够接受任意消息。
网上有两篇对BlocksKit源码的解析,看了觉得很受益匪浅。
NJKWebViewProgress
Webview进度条控件

SDCycleScrollView



KissXML
解析xml数据


MGSwipeTableCell
侧滑UITableViewCell展示多个可操作按钮


MPFoldTransition
一个实现多种视图切换效果的库。

越狱设备可使用Reveal查看任意App的UI页面。
官网下载地址: http://revealapp.com
如有任何疑问或问题请联系我:fishnewsdream@gmail.com,欢迎交流,共同提高!
Objective-C/Swift技术开发交流群201556264,讨论何种技术并不受限,欢迎各位大牛百家争鸣!
微信公众号OldDriverWeekly,欢迎关注并提出宝贵意见
老司机iOS周报,欢迎关注或订阅
刚刚在线工作室,欢迎关注或提出建设性意见!
刚刚在线论坛, 欢迎踊跃提问或解答!
如有转载,请注明出处,谢谢!
YYKit : 一组功能强大的iOS组件集合,支持iOS6.0及以上。
各组件之间有比较强的依赖关系,但原作者为了方便其他开发者使用,已将其拆分成以下独立组件。
YYModel — 高性能的 iOS JSON 模型框架。
YYCache — 高性能的 iOS 缓存框架。
YYImage — 功能强大的 iOS 图像框架。
YYWebImage — 高性能的 iOS 异步图像加载框架。
YYText — 功能强大的 iOS 富文本框架。
YYKeyboardManager — iOS 键盘监听管理工具
YYDispatchQueuePool — iOS 全局并发队列管理工具
YYAsyncLayer — iOS 异步绘制与显示的工具
YYCategories — 功能丰富的 Category 类型工具库
- 在 Podfile 中添加 pod ‘YYKit’
- 执行 pod install 或 pod update
- 导入
<YYKit/YYKit.h>
- 在 Cartfile 中添加 github “ibireme/YYKit”
- 执行 carthage update –platform ios 并将生成的 framework 添加到你的工程
- 导入
<YYKit/YYKit.h>- 注意: carthage framework 并没有包含 webp 组件。如果你需要支持 webp,可以用 CocoaPods 安装,或者手动安装
- 下载 YYKit 文件夹内的所有内容
- 将 YYKit 内的源文件添加(拖放)到你的工程
- 为 NSObject+YYAddForARC.m 和 NSThread+YYAdd.m 添加编译参数 -fno-objc-arc
- 链接以下 frameworks:
- UIKit
- CoreFoundation
- CoreText
- CoreGraphics
- CoreImage
- QuartzCore
- ImageIO
- AssetsLibrary
- Accelerate
- MobileCoreServices
- SystemConfiguration
- sqlite3
- libz
- 如果你需要支持 WebP,可以将 Vendor/WebP.framework(静态库) 加入你的工程
- 导入
YYModel.h
提到的WebP是Google新推出的影像技术,它可让网页 图档有效进行压缩,同时又不影响图片格式、兼容与实际清晰度,进而让整体网页下载速度加快。与JPEG相同,WebP是一种“有损压缩“,利用”预测编码“技术。但谷歌表示,这种格式的主要优势在于高效率。他们发现,“在质量相同的情况下,WebP格式图像的体积要比JPEG格式图像小40%。
更详细的使用文档可到CocoaDocs查看
这是作者使用YYKit提供的项目中的使用技巧。
YYKit这个库,而是根据需求单独使用这些组件
- 在
Podfile中添加pod 'YYModel'- 执行
pod install或pod update- 导入
<YYModel/YYModel.h>
- 在
Cartfile中添加github "ibireme/YYModel"- 执行
carthage update --platform ios并将生成的framework添加到你的工程- 导入
<YYModel/YYModel.h>
- 下载 YYModel 文件夹内的所有内容
- 将 YYModel 内的源文件添加(拖放)到你的工程
- 导入 YYModel.h
使用OC运行时函数将模型类进行解析,包括所有的属性、方法、实例变量等,然后将模型按照名字进行索引调用相应的set方法,模型类通常是固定的,并不用对每次转换的类进行解析,利用缓存机制,每个模型类只用解析一次,固性能会很高。
提供的演示Demo地址: https://github.com/ibireme/YYModel
使用了六种模型转换方法来对比各自的性能。分别是
除了手动转换外其余均使用了第三方的开源库,我个人之前常用的是手动转换和MJExtension
手动转换一般是这样



也就是对象之间的映射手动完成。上图也列举了系统的这个方法setValuesForKeysWithDictionary,这个方法用起来也还不错,不需要一一的来给对象赋值而直接从字典初始化即可,但用的不好会经常崩溃。
举个例子
创建PersonInfoModel
@interface PersonInfoModel : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *age;
@property (nonatomic, strong) NSString *address;
@end
PersonInfoModel *personInfoModel = [[PersonInfoModel alloc]init];
[personInfoModel setValuesForKeysWithDictionary:dic];

这是比较标准的使用方法,使用时完全没问题。如果我在`PersonInfoModel中再加一个属性

这样也没有出错,可以正常使用,也就是说用字典初始化对象时,只要对象中都包含了这些键值,是可以正常初始化的,我们再在字典中添加一个键值对

这样就会直接导致程序崩溃,因为该对象中没有 sex 这个属性,解决方法就是,实现一个方法setValue:forUndefinedKey:,然后实现这个方法,这样就会过滤掉给不存在的键值赋值。

如果有个属性是id,而id本身就是系统自身关键字,不能够当做属性使用,这时可以这么做。



setValue:forUndefinedKey:这个方法是关键,只有存在这个方法后,才可以过滤掉不存在的键值对而防止崩溃。在项目中使用时可以创建一个Model基类,添加这个方法setValue:forUndefinedKey:并实现。之后的Model继承自这个基类Model,这时候如果存在model中没有的键,可以重载父类的setValue:forUndefinedKey方法即可,否则会崩溃。
另外,程序在获取Json数据的时候可能会出现这种数据 "somevalue": null,也就是没有返回值,通过一些第三方的库可能会解析成这样 somevalue = "<null>";这种数据不是nil也不是string,如果向这个对象发送消息就会崩溃。
在AFNetworking中有这个方法可以解决,将这个空值去除
AFJSONResponseSerializer *responseSerializer = [[AFJSONResponseSerializer alloc] init];
responseSerializer.removesKeysWithNullValues = true;
而网上有个Category,叫做NullSafe,下载地址
https://github.com/nicklockwood/NullSafe
是在运行时操作把空值置为nil,因为nil是安全的,可以向nil对象发送消息不会奔溃,只需将这个category加入项目即可。
作者给出了这几种开源库的比较结果,YYModel的性能仅次于手动转换。
JSON{
"uid":123456,
"name":"Harry",
"created":"1965-07-31T00:00:00+0000"
}
Model
@interface User : NSObject
@property UInt64 uid;
@property NSString *name;
@property NSDate *created;
@end
// 将 JSON (NSData,NSString,NSDictionary) 转换为 Model:
User *user = [User yy_modelWithJSON:json];
// 将 Model 转换为 JSON 对象:
NSDictionary *json = [user yy_modelToJSONObject];
当 JSON/Dictionary 中的对象类型与 Model 属性不一致时,YYModel 将会进行如下自动转换。自动转换不支持的值将会被忽略,以避免各种潜在的崩溃问题。

JSON:
{
"n":"Harry Pottery",
"p": 256,
"ext" : {
"desc" : "A book written by J.K.Rowing."
}
}
Model:
@interface Book : NSObject
@property NSString *name;
@property NSInteger page;
@property NSString *desc;
@end
`@implementation Book
+ (NSDictionary *)modelCustomPropertyMapper {
return @{@"name" : @"n",
@"page" : @"p",
@"desc" : @"ext.desc"};
}
@end
通过实现 协议中的 modelCustomPropertyMapper,可以将 Model 属性的名字对应到 JSON/NSDictionary 相应的字段。可以把一个或一组 json key (key path) 映射到一个或多个属性。如果一个属性没有映射关系,那默认会使用相同属性名作为映射。
在 json->model 的过程中:如果一个属性对应了多个 json key,那么转换过程会按顺序查找,并使用第一个不为空的值。
在 model->json 的过程中:如果一个属性对应了多个 json key (key path),那么转换过程仅会处理第一个 json key (key path);如果多个属性对应了同一个 json key,则转换过过程会使用其中任意一个不为空的值。
JSON
{
"author":{
"name":"J.K.Rowling",
"birthday":"1965-07-31T00:00:00+0000"
},
"name":"Harry Potter",
"pages":256
}
Model: 什么都不用做,转换会自动完成
@interface Author : NSObject
@property NSString *name;
@property NSDate *birthday;
@end
@interface Book : NSObject
@property NSString *name;
@property NSUInteger pages;
@property Author *author; //Book 包含 Author 属性
@end
author必须和Json串中键author相同。@class Shadow, Border, Attachment;
@interface Attributes
@property NSString *name;
@property NSArray *shadows; //Array<Shadow>
@property NSSet *borders; //Set<Border>
@property NSMutableDictionary *attachments; //Dict<NSString,Attachment>
@end
@implementation Attributes
// 返回容器类中的所需要存放的数据类型 (以 Class 或 Class Name 的形式)。
+ (NSDictionary *)modelContainerPropertyGenericClass {
return @{@"shadows" : [Shadow class],
@"borders" : Border.class,
@"attachments" : @"Attachment" };
}
@end



@interface User
@property NSString *name;
@property NSUInteger age;
@end
@implementation Attributes
// 如果实现了该方法,则处理过程中会忽略该列表内的所有属性
+ (NSArray *)modelPropertyBlacklist {
return @[@"test1", @"test2"];
}
// 如果实现了该方法,则处理过程中不会处理该列表外的属性。
+ (NSArray *)modelPropertyWhitelist {
return @[@"name"];
}@end
如果同时实现modelPropertyBlacklist方法和modelPropertyWhitelist方法,且忽略和处理属性相同, 则所有属性都不会有值。


iOS JSON 模型转换库评测 这篇文章作者对比了几个库的优劣,可以看看
- 在
Podfile中添加pod 'YYCache'- 执行
pod install或pod update- 导入
<YYCache/YYCache.h>
- 在
Cartfile中添加github "ibireme/YYCache"- 执行
carthage update --platform ios并将生成的framework添加到你的工程- 导入
<YYCache/YYCache.h>
下载
YYCache文件夹内的所有内容将
YYCache内的源文件添加(拖放)到你的工程链接以下的
frameworks:
- UIKit
- CoreFoundation
- QuartzCore
- sqlite3
4.导入
YYCache.h
提供的演示Demo地址: https://github.com/ibireme/YYCache
通常一个缓存是由内存缓存和磁盘缓存组成,内存缓存提供容量小但高速的存取功能,磁盘缓存提供大容量但低速的持久化存储.
作者比较了三个第三方的开源库YYCache、PINCache、TMCache。内存缓存方面分别比较了
得出的性能结果如下

上图TMMemoryCache的测试结果因为性能太差并没有显示出来。
磁盘缓存的实现技术大致分为
TMDiskCache, PINDiskCache, SDWebImage 等缓存,都是基于文件系统的,即一个 Value 对应一个文件,通过文件读写来缓存数据。
FastImageCache 采用的是 mmap 将文件映射到内存。
NSURLCache、FBDiskCache 都是基于 SQLite 数据库的。SQLite 写入性能比直接写文件要高,但读取性能取决于数据大小:当单条数据小于 20K 时,数据越小 SQLite 读取性能越高;单条数据大于 20K 时,直接写为文件速度会更快一些。
所以磁盘缓存最好是把 SQLite 和文件存储结合起来:key-value 元数据保存在 SQLite 中,而 value 数据则根据大小不同选择 SQLite 或文件存储。YYDiskCache 就是采用的 SQLite 配合文件的存储方式,参数inlineThreshold,默认20KB,小于它存数据库,大于它存文件。
作者对比了YYDiskCache、PINDiskCache、TMDiskCache三种磁盘存储方式,得出的性能结果如下
YYCache的优势包括
- (BOOL)containsObjectForKey:(NSString *)key;
返回一个布尔值,指示是否一个给定的关键是在缓存中。这种方法可能会阻塞调用线程,直到文件读取完成。
- (void)containsObjectForKey:(NSString *)key withBlock:(void ( ^ ) ( NSString *key , BOOL contains ))block
返回一个布尔值表示的块是否一个给定的关键字在缓存中。该方法立即返回,调用时在后台通过块队列操作完成。
- (instancetype)initWithPath:(NSString *)path
根据指定的名称创建一个新的实例。具有相同名称的多个实例将使缓存不稳定。
- (void)objectForKey:(NSString *)key withBlock:(void ( ^ ) ( NSString *key , id<NSCoding> object ))block
返回给定的键相关联的值。该方法立即返回,调用时在后台通过块队列操作完成。
- (void)removeObjectForKey:(NSString *)key withBlock:(void ( ^ ) ( NSString *key ))block
在缓存中移除指定键的值。该方法立即返回,调用时在后台通过块队列操作完成。
- (void)removeAllObjects
清空缓存。这种方法可能会阻塞调用线程,直到文件删除了。
- (void)removeAllObjectsWithBlock:(void(^)(void))block;
清空缓存。该方法立即返回,调用时在后台通过块队列操作完成。
YYCache 设计思路与技术细节 这篇文章作者讲述了部分设计思路和实现细节,可以看看
- 将 cocoapods 更新至最新版本.
- 在 Podfile 中添加 pod ‘YYImage’
- 执行 pod install 或 pod update
- 导入
<YYImage/YYImage.h>- 注意:pod 配置并没有包含 WebP 组件, 如果你需要支持 WebP,可以在 Podfile 中添加 pod ‘YYImage/WebP’。你可以调用 YYImageWebPAvailable() 来检查一下 WebP 组件是否被正确安装
- 在 Cartfile 中添加 github “ibireme/YYImage”。
- 执行 carthage update –platform ios 并将生成的 framework 添加到你的工程。
- 导入
<YYImage/YYImage.h>- 注意:carthage framework 并没有包含 WebP 组件。如果你需要支持 WebP,可以用 CocoaPods 安装,或者手动安装。
- 下载 YYImage 文件夹内的所有内容
- 将 YYImage 内的源文件添加(拖放)到你的工程
3 . 链接以下 frameworks:
- UIKit
- CoreFoundation
- QuartzCore
- AssetsLibrary
- ImageIO
- Accelerate
- MobileCoreServices
- libz
4 . 导入
YYImage.h5 . 注意:如果你需要支持 WebP,可以将 Vendor/WebP.framework(静态库) 加入你的工程。你可以调用 YYImageWebPAvailable() 来检查一下 WebP 组件是否被正确安装
提供的演示Demo地址: https://github.com/ibireme/YYImage
图片本身有 3 种常见的编码方式



第一种baseline,即逐行扫描。默认情况下JPEG、PNG、GIF 都是这种保存方式
第二种interaced,即隔行扫描。PNG 和 GIF 在保存时可以选择这种格式
第三种是progressive,即渐进式。JPEG 在保存时可以选择这种方式。
在下载图片时,首先用 CGImageSourceCreateIncremental(NULL) 创建一个空的图片源,随后在获得新数据时调用CGImageSourceUpdateData(data, false) 来更新图片源,最后在用 CGImageSourceCreateImageAtIndex() 创建图片来显示。
UIImage *image = [YYImage imageNamed:@"ani.gif"];
UIImageView *imageView = [[YYAnimatedImageView alloc]initWithImage:image];
[self.view addSubView:imageView];
NSArray *paths = @[@"/ani/frame1.png", @"/ani/frame2.png", @"/ani/frame3.png"];
NSArray *times = @[@0.1, @0.2, @0.1];
UIImage *image = [YYFrameImage alloc] initWithImagePaths:paths frameDurations:times repeats:YES];
UIImageView *imageView = [YYAnimatedImageView alloc]initWithImage:image];
[self.view addSubView:imageView];
UIImage *spriteSheet = [UIImage imageNamed:@"sprite-sheet"];
NSMutableArray *contentRects = [NSMutableArray new];
NSMutableArray *durations = [NSMutableArray new];
for (int j = 0; j < 12; j++) {
for (int i = 0; i < 8; i++) {
CGRect rect;
rect.size = CGSizeMake(img.size.width / 8, img.size.height / 12);
rect.origin.x = img.size.width / 8 * i;
rect.origin.y = img.size.height / 12 * j;
[contentRects addObject:[NSValue valueWithCGRect:rect]];
[durations addObject:@(1 / 60.0)];
}}
YYSpriteSheetImage *sprite;sprite = [[YYSpriteSheetImage alloc] initWithSpriteSheetImage:img
contentRects:contentRects
frameDurations:durations
loopCount:0];
YYAnimatedImageView *imageView = [YYAnimatedImageView new];
imageView.size = CGSizeMake(img.size.width / 8, img.size.height / 12);
imageView.image = sprite;
[self.view addSubView:imageView];
YYAnimatedImageView *imageView = ...;
// 暂停:
[imageView stopAnimating];
// 播放:
[imageView startAnimating];
// 设置播放进度:
imageView.currentAnimatedImageIndex = 12;
// 获取播放状态:
image.currentIsPlayingAnimation;
// 解码单帧图片:
NSData *data = [NSData dataWithContentOfFile:@"/tmp/image.webp"];
YYImageDecoder *decoder = [YYImageDecoder decoderWithData:data scale:2.0];
UIImage image = [decoder frameAtIndex:0 decodeForDisplay:YES].image;
// 渐进式图片解码 (可用于图片下载显示):
NSMutableData *data = [NSMutableData new];
YYImageDecoder *decoder = [[YYImageDecoder alloc] initWithScale:2.0];
while(newDataArrived) {
[data appendData:newData];
[decoder updateData:data final:NO];
if (decoder.frameCount > 0) {
`` UIImage image = [decoder frameAtIndex:0 decodeForDisplay:YES].image;``
// progressive display...
}}
[decoder updateData:data final:YES];UIImage image = [decoder frameAtIndex:0 decodeForDisplay:YES].image;
// final display...
// 编码静态图 (支持各种常见图片格式):
YYImageEncoder *jpegEncoder = [[YYImageEncoder alloc] initWithType:YYImageTypeJPEG];
jpegEncoder.quality = 0.9;
[jpegEncoder addImage:image duration:0];
NSData jpegData = [jpegEncoder encode];
// 编码动态图 (支持 GIF/APNG/WebP):
YYImageEncoder *webpEncoder = [[YYImageEncoder alloc] initWithType:YYImageTypeWebP];
webpEncoder.loopCount = 5;
[webpEncoder addImage:image0 duration:0.1];
[webpEncoder addImage:image1 duration:0.15];
[webpEncoder addImage:image2 duration:0.2];
NSData webpData = [webpEncoder encode];
// 获取图片类型
YYImageType type = YYImageDetectType(data);
if (type == YYImageTypePNG) ...
Demo展示效果如下


这两篇文章是作者的相关解读。
- 将 cocoapods 更新至最新版本
- 在 Podfile 中添加 pod ‘YYWebImage’
- 执行 pod install 或 pod update
- 导入
<YYWebImage/YYWebImage.h>- 注意:pod 配置并没有包含 WebP 组件, 如果你需要支持 WebP,可以在 Podfile 中添加 pod ‘YYImage/WebP’。你可以调用 YYImageWebPAvailable() 来检查一下 WebP 组件是否被正确安装
- 在 Cartfile 中添加 github “ibireme/YYWebImage”
- 执行 carthage update –platform ios 并将生成的 framework 添加到你的工程。
- 导入
<YYWebImage/YYWebImage.h>- 注意: carthage framework 并没有包含 webp 组件。如果你需要支持 WebP,可以用 CocoaPods 安装,或者手动安装
- 下载 YYWebImage 文件夹内的所有内容。
- 将 YYWebImage 内的源文件添加(拖放)到你的工程。
- 链接以下 frameworks:
- UIKit
- CoreFoundation
- QuartzCore
- AssetsLibrary
- ImageIO
- Accelerate
- MobileCoreServices
- sqlite3
- libz
4.导入 YYWebImage.h。
5.注意:如果你需要支持 WebP,可以将 Vendor/WebP.framework(静态库) 加入你的工程。你可以调用 YYImageWebPAvailable() 来检查一下 WebP 组件是否被正确安装
提供的Demo演示地址: https://github.com/ibireme/YYWebImage
YYWebImage是一个异步图片加载框架,目的是为了试图替代SDWebImage、PINRemoteImage、FLAnimatedImage 等开源框架。支持这些开源框架的大部分功能,同时增加了大量新特性、并且有不小的性能提升。其底层用 YYCache 实现了内存和磁盘缓存, 用 YYImage 实现了 WebP/APNG/GIF 动图的解码和播放。
// 加载网络图片
imageView.yy_imageURL = [NSURL URLWithString:@"http://github.com/logo.png"];
// 加载本地图片
imageView.yy_imageURL = [NSURL fileURLWithPath:@"/tmp/logo.png"];
// 只需要把 UIImageView 替换为 YYAnimatedImageView 即可。
UIImageView *imageView = [YYAnimatedImageView new];
imageView.yy_imageURL = [NSURL URLWithString:@"http://github.com/ani.webp"];
// 渐进式:边下载边显示
// 渐进式加载,增加模糊效果和渐变动画
[imageView yy_setImageWithURL:url
placeholder:nil
options:YYWebImageOptionSetImageWithFadeAnimation
progress:^(NSInteger receivedSize, NSInteger expectedSize) {
progress = (float)receivedSize / expectedSize;
}
transform:^UIImage *(UIImage *image, NSURL *url) {
image = [image yy_imageByResizeToSize:CGSizeMake(100, 100) contentMode:UIViewContentModeCenter];
return [image yy_imageByRoundCornerRadius:10];
}
completion:^(UIImage *image, NSURL *url, YYWebImageFromType from, YYWebImageStage stage, NSError *error) {
if (from == YYWebImageFromDiskCache) {
NSLog(@"load from disk cache");
}
}];
YYImageCache *cache = [YYWebImageManager sharedManager].cache;
// 获取缓存大小
cache.memoryCache.totalCost;
cache.memoryCache.totalCount;
cache.diskCache.totalCost;
cache.diskCache.totalCount;
// 清空缓存
[cache.memoryCache removeAllObjects];
[cache.diskCache removeAllObjects];
// 清空磁盘缓存,带进度回调
[cache.diskCache removeAllObjectsWithProgressBlock:^(int
removedCount, int totalCount) {
// progress
} endBlock:^(BOOL error) {
// end
}];
- 在 Podfile 中添加 pod ‘YYText’
- 执行 pod install 或 pod update
- 导入
<YYText/YYText.h>
- 在 Cartfile 中添加 github “ibireme/YYText”
- 执行 carthage update –platform ios 并将生成的 framework 添加到你的工程
- 导入
<YYText/YYText.h>
- 下载 YYText 文件夹内的所有内容
- 将 YYText 内的源文件添加(拖放)到你的工程
- 链接以下 frameworks:
- UIKit
- CoreFoundation
- CoreText
- QuartzCore
- Accelerate
- MobileCoreServices
- 导入 YYText.h
提供Demo地址:https://github.com/ibireme/YYText
YYText是功能强大的富文本编辑与显示框架











- 在 Podfile 中添加 pod ‘YYKeyboardManager’
- 执行 pod install 或 pod update
- 导入
<YYKeyboardManager/YYKeyboardManager.h>
- 在 Cartfile 中添加 github “ibireme/YYKeyboardManager”
- 执行 carthage update –platform ios 并将生成的 framework 添加到你的工程
- 导入
<YYKeyboardManager/YYKeyboardManager.h>
- 下载 YYKeyboardManager 文件夹内的所有内容
- 将 YYKeyboardManager 内的源文件添加(拖放)到你的工程
- 导入 YYKeyboardManager.h
提供的Demo地址:https://github.com/ibireme/YYKeyboardManager
YYKeyboardManager是一个iOS键盘监听管理工具类。兼容 iPhone / iPad / iPod,兼容 iOS 6 / 7 / 8 / 9, 并且能很好的处理屏幕旋转。我之前一直使用的监听键盘工具主要是TPKeyboardAvoiding、IQKeyboardManager和自己写的一个工具类,大体可以满足需求。

- 在 Podfile 中添加 pod ‘YYDispatchQueuePool’
- 执行 pod install 或 pod update
- 导入
<YYDispatchQueuePool/YYDispatchQueuePool.h>
- 在 Cartfile 中添加 github “ibireme/YYDispatchQueuePool”
- 执行 carthage update –platform ios 并将生成的 framework 添加到你的工程
- 导入
<YYDispatchQueuePool/YYDispatchQueuePool.h>
- 下载 YYDispatchQueuePool 文件夹内的所有内容
- 将 YYDispatchQueuePool 内的源文件添加(拖放)到你的工程
- 导入 YYDispatchQueuePool.h
YYDispatchQueuePool是iOS全局并发队列管理工具。
提供的下载地址: https://github.com/ibireme/YYDispatchQueuePool
// 从全局的 queue pool 中获取一个 queue
dispatch_queue_t queue = YYDispatchQueueGetForQOS(NSQualityOfServiceUtility);
// 创建一个新的 serial queue pool
YYDispatchQueuePool *pool = [[YYDispatchQueuePool alloc] initWithName:@"file.read" queueCount:5 qos:NSQualityOfServiceBackground];
dispatch_queue_t queue = [pool queue];
- 在 Podfile 中添加 pod ‘YYAsyncLayer’
- 执行 pod install 或 pod update
- 导入
<YYAsyncLayer/YYAsyncLayer.h>
- 在 Cartfile 中添加 github “ibireme/YYAsyncLayer”。
- 执行 carthage update –platform ios 并将生成的 framework 添加到你的工程。
- 导入
<YYAsyncLayer/YYAsyncLayer.h>
- 下载 YYAsyncLayer 文件夹内的所有内容。
- 将 YYAsyncLayer 内的源文件添加(拖放)到你的工程。
- 导入 YYAsyncLayer.h。
YYAsyncLayer是iOS异步绘制与显示的工具类。
提供的下载地址: https://github.com/ibireme/YYAsyncLayer


- 在 Podfile 中添加 pod ‘YYCategories’。
- 执行 pod install 或 pod update。
- 导入
<YYCategories/YYCategories.h>
######Carthage
- 在 Cartfile 中添加 github “ibireme/YYCategories”。
- 执行 carthage update –platform ios 并将生成的 framework 添加到你的工程。
- 导入
<YYCategories/YYCategories.h>
- 下载 YYCategories 文件夹内的所有内容。
- 将 YYCategories 内的源文件添加(拖放)到你的工程。
- 为 NSObject+YYAddForARC.m 和 NSThread+YYAdd.m 添加编译参数 -fno-objc-arc
- 链接以下 frameworks:
- UIKit
- CoreGraphics
- QuartzCore
- Accelerate
- ImageIO
- CoreText
- CoreFoundation
- libz
- 导入 YYCategories.h。
YYCategories是功能丰富的 Category 类型工具库。
提供的下载地址:https://github.com/ibireme/YYCategories
以上,YYDispatchQueuePool和YYAsyncLayer这两个工具类并没有花时间好好去研究,使用方法只是照搬过来,对于文中作者所讲的鉴于水平有限仍然有些疑惑,以后有时间再去研究。
YYKit包含的知识容量太过庞大,知识体系要求也更为全面,在研究之初对于文中的很多点并没有直观的认识,现在再去通读一遍感受也更为真切,更为深刻的认识只能在以后项目使用中再去体会!
如有任何疑问或问题请联系我:fishnewsdream@gmail.com,欢迎交流,共同提高!
Objective-C/Swift技术开发交流群201556264,讨论何种技术并不受限,欢迎各位大牛百家争鸣!
微信公众号OldDriverWeekly,欢迎关注并提出宝贵意见
老司机iOS周报,欢迎关注或订阅
刚刚在线工作室,欢迎关注或提出建设性意见!
刚刚在线论坛, 欢迎踊跃提问或解答!
如有转载,请注明出处,谢谢!
本文系转载
作者:minggo620
相比“凌波微步”的swift,Object-C被誉为“如来神掌”。传说Runtime就是支持这“如来神掌”说法的最好体现。听起来总是这么的神秘高级,于是总能在各个论坛看到碎片资料,时间一长总记不住哪里是哪里,每次都要打开还几个网页。这中记不住显然是知识体系还不完整重要体现。还是自己从Runtime的思想到动手代码呈现上做出总结尚为上策。

- RunTime简称运行时,就是系统在运行的时候的一些机制,其中最主要的是消息机制。
- 对于C语言,函数的调用在编译的时候会决定调用哪个函数( C语言的函数调用请看这里 )。编译完成之后直接顺序执行,无任何二义性。
- OC的函数调用成为消息发送。属于动态调用过程。在编译的时候并不能决定真正调用哪个函数(事实证明,在编 译阶段,OC可以调用任何函数,即使这个函数并未实现,只要申明过就不会报错。而C语言在编译阶段就会报错)。
- 只有在真正运行的时候才会根据函数的名称找 到对应的函数来调用。
官网文档还提供关于传统和现代版本Runtime的说明
- In the legacy runtime, if you change the layout of instance variables in a class, you must recompile classes that inherit from it.
- In the modern runtime, if you change the layout of instance variables in a class, you do not have to recompile classes that inherit from it.
In addition, the modern runtime supports instance variable synthesis for declared properties (see Declared Properties in The Objective-C Programming Language).
[self.loginBt login];
objc_msgSend(self.loginB,@selector(login));
既然是“如来神掌”,简直可以无法无天啦,当街拦下一个人问道“这是马还是鹿啊?”,那人看是Runtime大人惧怕道“Runtime大人,您说是马就是马,是鹿就是鹿~”。Runtime大快“wow哈哈哈~,见你乖巧,我也不为难于你。你缺头驴是吧?,本大人现在造一头送于你,迁回家便是!喔~哈哈哈”。
呵呵,扯远了,回到Runtime作用上。无所不能的事情就不一一介绍了,梳理下较为可能用的几个地方:
- 动态的添加对象的成员变量和方法
- 动态交换两个方法的实现
- 实现分类也可以添加属性
- 实现NSCoding的自动归档和解档
- 实现字典转模型的自动转换
Teacher: What's your name?
XiaoMing: My name is XiaoMing.
Teacher: Pardon?
XiaoMing: My name is __
在程序当中,假设XiaoMing的name原来的值为XiaoMing,后来被Runtime偷换了一个名字叫Minggo。那么,Runtime是如何做到的呢?
①动态获取XiaoMing类中的所有属性[当然包括私有]
Ivar *ivar = class_copyIvarList([self.xiaoMing class], &count);
②遍历属性找到对应name字段
const char *varName = ivar_getName(var);
③修改对应的字段值成Minggo
object_setIvar(self.xiaoMing, var, @"Minggo");
-(void)answer{
unsigned int count = 0;
Ivar *ivar = class_copyIvarList([self.xiaoMing class], &count);
for (int i = 0; i<count; i++) {
Ivar var = ivar[i];
const char *varName = ivar_getName(var);
NSString *name = [NSString stringWithUTF8String:varName];
if ([name isEqualToString:@"_englishName"]) {
object_setIvar(self.xiaoMing, var, @"Minggo");
break;
}
}
NSLog(@"XiaoMing first answer is %@",self.xiaoMing.englishName);
self.nameTf.text = self.xiaoMing.englishName;
}
Teacher: What's your name?
XiaoMing: My name is XiaoMing.
Teacher: Pardon?
XiaoMing: My name is __
在程序当中,假设XiaoMing的第一次回答为firstSay,后来被Runtime交换了一个名字叫secondSay的方法,最终再调用firstSay的时候,其实是调用了secondSay的实现。那么,Runtime是如何做到的呢?
######2)Step:
①动态找到firstSay和secondSay方法
Method m1 = class_getInstanceMethod([self.xiaoMing class], @selector(firstSay));
Method m2 = class_getInstanceMethod([self.xiaoMing class], @selector(secondSay));
②交换两个方法
method_exchangeImplementations(m1, m2);
-(void)answer{
Method m1 = class_getInstanceMethod([self.xiaoMing class], @selector(firstSay));
Method m2 = class_getInstanceMethod([self.xiaoMing class], @selector(secondSay));
method_exchangeImplementations(m1, m2);
NSString *secondName = [self.xiaoMing firstSay];
self.nameTf.text = secondName;
NSLog(@"XiaoMing:My name is %@",secondName);
}
Teacher: Where is LiLei from?
XiaoMing: I don't know.
Teacher: Guess?.
LiHua: He is from __
在程序当中,假设XiaoMing的中没有guess这个方法,后来被Runtime添加一个名字叫guess的方法,最终再调用guess方法做出相应。那么,Runtime是如何做到的呢?
2)Step:
①动态给XiaoMing类中添加guess方法:
class_addMethod([self.xiaoMing class], @selector(guess), (IMP)guessAnswer, "v@:");
这里参数地方说明一下: (IMP)guessAnswer 意思是guessAnswer的地址指针; “v@:” 意思是,v代表无返回值void,如果是i则代表int;@代表 id sel; : 代表 SEL _cmd; “v@:@@” 意思是,两个参数的没有返回值。
②调用guess方法响应事件:
[self.xiaoMing performSelector:@selector(guess)];
③编写guessAnswer的实现:
void guessAnswer(id self,SEL _cmd){
NSLog(@"He is from GuangTong");
}
这个有两个地方留意一下: 1.void的前面没有+、-号,因为只是C的代码。 2.必须有两个指定参数(id self,SEL _cmd)
3)Show Code:
-(void)answer{
class_addMethod([self.xiaoMing class], @selector(guess), (IMP)guessAnswer, "v@:");
if ([self.xiaoMing respondsToSelector:@selector(guess)]) {
[self.xiaoMing performSelector:@selector(guess)];
} else{
NSLog(@"Sorry,I don't know");
}
self.cityTf.text = @"GuangTong";
}
void guessAnswer(id self,SEL _cmd){
NSLog(@"He is from GuangTong");
}
这一点上有两点要表达一下:第一,XCode运行你在Category的.h文件申明@Property,编译通过,但运行时如果没有Runtime处理,进行赋值取值,就马上报错。第二,这一点是iOS面试当中经常面到的问题:如何给扩展添加属性?。
Teacher: What's your Chinese name?
XiaoMing: I have no one.
LiHua: You should have one.
LiHua: Your Chinese name is __
在程序当中,假设XiaoMing的中没有chineseName这个属性,后来被Runtime添加一个名字叫chineseName的属性。那么,Runtime是如何做到的呢?
①申明chineseName属性
#import "XiaoMing.h"
@interface XiaoMing (MutipleName)
@property(nonatomic,copy) NSString *chineseName;
@end
②动态添加属性和实现方法
#import "XiaoMing+MutipleName.h"
#import <objc/runtime.h>
@implementation XiaoMing (MutipleName)
char cName;
-(void)setChineseName:(NSString *) chineseName{
objc_setAssociatedObject(self, &cName, chineseName, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
-(NSString *)chineseName{
return objc_getAssociatedObject(self, &cName);}
@end
③使用chineseName属性
-(void)answer{
NSLog(@"My Chinese name is %@",self.xiaoMing.chineseName);
self.chineseNameTf.text = self.xiaoMing.chineseName;
}
3)Show Code:
上边就是最要的Code了。以下更精彩。
三.效果图更直观
四.源码下载地址更详细
本文系转载
作者:编程小翁
用户行为统计(User Behavior Statistics, UBS)一直是移动互联网产品中必不可少的环节,也俗称埋点。在保证移动端流量不会受较大影响的前提下,PM们总是希望埋点覆盖面越广越好。目前常规的做法是将埋点代码封装成工具类,但凡工程中需要埋点(如点击事件、页面跳转)的地方都插入埋点代码。一旦项目越来越复杂,你会发现埋点的代码散落在程序的各个角落,不利于维护以及复用。本文旨在探讨利用iOS的运行时机制实现一种可复、解耦、容易维护的用户统计方案。探讨毕竟是探讨,欢迎到在简书留言讨论。本文虽有些长却是用心之作,希望你有耐心看完。
注:本文需要一些iOS的Runtime基础
该方案的完成将会用到以下知识:
Method Swizzling(Hook)
单元测试
接着开头的话题,我们先回顾一下主流的埋点是怎么做的。我粗糙地将埋点分为两种:1、页面统计,包括页面停留时间、页面进入次数;2、交互事件统计,包括单击、双击、手势交互等。
以统计页面进入次数为例,最简单粗暴的做法是在所有页面的viewDidppear1:以及viewDidDisappear:中分别埋点,将自己对应的pageID上传给服务端。代码大概长酱紫:
@implementation HomeViewController
- (void)viewDidAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[WUserStatistics sendEventToServer:@"PAGE_EVENT_HOME_ENTER"];}
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
[WUserStatistics sendEventToServer:@"PAGE_EVENT_HOME_LEAVE"];
}
@end
+[WUserStatistics sendEventToServer:]封装网络请求,将ID上传给服务器。上述方案有以下弊端:
1、复用性差。这部分埋点代码很难给其他项目复用
2、工作量大。尤其当页面较多时,需要修改的代码较多
3、引入“脏代码”,不易维护
第3点提到的“脏代码”意思是用户行为分析这种业务其实跟主业务没太大关系,不应该保持如此高的耦合度,因为这些代码会干扰我们对项目主业务的维护。这个我个人看法。
2)常规交互事件埋点
常规做法一般在交互事件的selector中获取该事件的ID并上传给服务端,代码大概长酱紫:
- (IBAction)onFavBtnPressed:(id)sender
{
[WUserStatistics sendEventToServer:@"CTRL_EVENT_HOME_FAV"];
//...do other things
}
稍微大一点的APP如果采用这种方式,那诸如此类的埋点代码将遍地都是。它的缺点参考页面统计埋点部分,其复用性基本为零,也就是在新项目中根本无法复用埋点代码。
小总结一下,采用常规的做法虽然直观方便,但在可复用性、可维护性等方面有所欠缺。在我看来,借助运行时可以很好地避开这些缺点。
由于Runtime知识不属于本文的重点,这里只简单介绍。
在iOS中,我们可以在运行时替换两个方法的实现,达到“勾住”某个方法并注入代码的目的。具体做法是:
重载类的+(void)load方法,在程序加载到内存时利用Runtime的method_exchangeImplementations等接口将方法(设为M)的实现互相交换。当方法M被调用时就会被勾住(Hook),执行我们的方法。
这种技术也称为Method Swizzling,属于面向切面编程(Aspect-Oriented Programming)的一种实现。对应的类是Aspects。
替换两个方法的实现,代码一般长酱紫:
@interface WHookUtility : NSObject
+ (void)swizzlingInClass:(Class)cls originalSelector:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector;
@end
@implementation WHookUtility
+ (void)swizzlingInClass:(Class)cls originalSelector:(SEL)originalSelector swizzledSelector:(SEL)swizzledSelector{
Class class = cls;
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
` BOOL didAddMethod =`
` class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));`
if (didAddMethod) {
` class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));`
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
@end
这个WHookUtility工具类下文会用到。比如现在我们要勾住UIViewController的viewWillAppear:方法,可以这样做:
@implementation UIViewController (userStastistics)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
`SEL originalSelector = @selector(viewWillAppear:);`
`SEL swizzledSelector = @selector(swiz_viewWillAppear:);`
`[WHookUtility swizzlingInClass:[self class] originalSelector:originalSelector swizzledSelector:swizzledSelector];`
});}
#pragma mark - Method Swizzling
- (void)swiz_viewWillAppear:(BOOL)animated{
`NSLog(@"...............");`
`[self swiz_viewWillAppear:animated];`
}@end
更多关于Runtime、method swizzling、面向切面编程的介绍请参考这里
为了便于下文叙述,先引入一个简单的项目,共有两个页面(HomeViewController,DetailViewController),如下:

需求是:
统计两个页面的展示与离开次数
统计收藏、分享单击事件的次数
对现有工程代码影响越小越好
1)统计两个页面的展示与离开次数
这部分应该比较直观了,摒弃掉在每个controller中埋点的方式,我们对UIViewController添加category从而Hook到viewWillAppear:与viewWillDisappear:。在这两个方法中注入埋点代码:

这时候问题来了,项目中每个页面都会有自己的页面事件编号(pageEventID),此处的埋点代码如何知道要发送什么pageEventID给服务端呢?轻松祭出if-else神器:
- (NSString *)pageEventID:(BOOL)bEnterPage{
``
NSString *selfClassName = NSStringFromClass([self class]);
NSString *pageEventID = nil;
if ([selfClassName isEqualToString:@"HomeViewController"]) {
pageEventID = bEnterPage ? @"EVENT_HOME_ENTER_PAGE" : @"EVENT_HOME_LEAVE_PAGE";
} else if ([selfClassName isEqualToString:@"DetailViewController"]) {
pageEventID = bEnterPage ? @"EVENT_DETAIL_ENTER_PAGE" : @"EVENT_DETAIL_LEAVE_PAGE";
}
//else if (<#expression#>)...
``
}
当然,我们可以有更优雅的方式,比如用一个配置表替代上面一长串的if判断,这样无论页面数怎么增加,代码始终是那么一小段。我们新建一个WGlobalUserStatisticsConfig.plist的配置表来存放每个页面在进入以及离开时的pageEventID,结构如下:

)
因此,页面进出统计中获取pageEventID的代码始终是以下这几句:
- (NSString *)pageEventID:(BOOL)bEnterPage{
NSDictionary *configDict = [self dictionaryFromUserStatisticsConfigPlist];
NSString *selfClassName = NSStringFromClass([self class]);
return configDict[selfClassName][@"PageEventIDs"][bEnterPage ? @"Enter" : @"Leave"];
}
- (NSDictionary *)dictionaryFromUserStatisticsConfigPlist{
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"WGlobalUserStatisticsConfig" ofType:@"plist"];
NSDictionary *dic = [NSDictionary dictionaryWithContentsOfFile:filePath];
return dic;}
效果如下:

以上就是完成了页面进出统计的埋点,并且达到了我们的第三点预期:对现有代码基本无影响。通过Method Swizzling的方式现有的工程甚至不需要import任何文件!后期代码变动时需要维护的仅仅是plist配置表。
2)统计收藏、分享单击事件的次数
与上一节思路一致,要做到解耦显然需要通过category+hook来实现。本文demo中收藏跟分享都是UIButton类型,可以考虑添加UIButton的catogory。但更好的方式是添加UIControl的category,这样可以让埋点代码覆盖到所有UIControl的子类中去,比如button、switch、segment等,提高复用性。
既然要hook,那就要清楚到底要hookUIControl的哪(几)个方法,只有部分方法是满足埋点需求的,最好是所hook的方法能提供target、actionName等信息。这是个尝试的过程。
UIControl的方法列表有以下:

通过观察方法名和参数,我们有理由怀疑是倒数第二个,因其携带了不少貌似有价值的信息:
- (void)sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event;
于是写出测试代码看看:
@implementation UIControl (userStastistics)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originalSelector = @selector(sendAction:to:forEvent:);
SEL swizzledSelector = @selector(swiz_sendAction:to:forEvent:);
[WHookUtility swizzlingInClass:[self class] originalSelector:originalSelector swizzledSelector:swizzledSelector];
});}
#pragma mark - Method Swizzling
- (void)swiz_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event;
{
//插入埋点代码
[self performUserStastisticsAction:action to:target forEvent:event];
`[self swiz_sendAction:action to:target forEvent:event];`
}
- (void)performUserStastisticsAction:(SEL)action to:(id)target forEvent:(UIEvent *)event;
{
NSLog(@"\n***hook success.\n[1]action:%@\n[2]target:%@ \n[3]event:%ld", NSStringFromSelector(action), target, (long)event);
}
@end
Log如下图:

可以看到,通过category+method swizzling的方式在没有修改现有工程任何代码的情况下已经成功Hook到所有点击事件,在Hook代码中我们知道了一个点击事件的target也就是ViewController,也知道了点击事件的响应函数名,知道了点击的TouchSet。这些信息已经能满足埋点需求了。
与页面统计埋点类似,我们同样采用plist配置表的方式避免一大长串的if-else判断:

有了这张配置表就很容易得到某次单击事件的事件ID(ControlEventID):
NSString *actionString = NSStringFromSelector(action);
//获取SEL string
NSString *targetName = NSStringFromClass([target class]);
//viewController name
NSDictionary *configDict = [self dictionaryFromUserStatisticsConfigPlist];
eventID = configDict[targetName][@"ControlEventIDs"][actionString];
事实上,我把某个页面单元的所有事件ID分成了两类:页面事件ID(PageEventIDs,页面的进出等)、交互事件ID(ControlEventIDs,单击、双击、手势等)。分类有助于下文使用单元测试(Unit Test)进行自动化后期维护。
埋点效果如图:

到这里先做了阶段性的总结,本文提出的思路有以下优越性:
与工程代码基本解耦,避免引入“脏代码”
即使后期工程代码发生重构,需要修改的仅仅是plist配置表
维护配置表比维护散落在工程各个角落的代码简单
四、基于单元测试的后期维护
俗话说,创业难守业更难。前面的思路基本可以完成初步的埋点需求。但是在实际项目中代码重构是很频繁的。这意味着在多人协作开发、代码重构频繁的项目中响应事件方法甚至页面名称都可能被改掉,造成事件ID获取不到导致埋点失效。
代码变动的情况无非以下几种(这里只介绍响应事件发生改变的情况):
1、响应事件方法名称改变或者删除
比如收藏事件原先是onFavBtnPressed:,之后被改成onFavouriteBtnPressed:。代码发生变动但是plist配置表中由于开发人员疏忽忘记同步修改了。这种疏忽在开发压力大进度赶的情况下是有很大概率发生的。由于代码与配置表不匹配将导致eventID为nil。在这种情况下单元测试就很有必要了,使用完备的测试用例能在发版前检测到这种不匹配情况从而避免埋点失效。
在单元测试中我们首先读取plist配置文件,遍历所有的页面。在一个页面内遍历所有的ControlEventIDs,对每个响应函数名进行respondsToSelector:判断:

单测代码如下:

我们来测试一下,如果把HomeViewController的onFavBtnPressed:改成onMyFavBtnPressed:后单元测试的结果就是:

这种改变给单测轻松捕捉到了,
只要XCTAssert的log够详细,维护起来其实相当轻松的。
上图中的log已经明确指出-[HomeViewController onFavBtnPressed:]方法发生了改变。
2、代码中新增了响应事件
这种情况常见于新版本中有新的埋点需求。如果代码中新增了响应事件并且该响应事件是在PM要求的埋点列表中,但是plist有可能会漏掉该事件。这种情况是比较棘手的。上一种情况是基于plist列表去校验代码,这里就要反过来,根据代码去校验plist是否有缺失。但问题来了,一个项目中响应函数往往是非常多的,并不是任何响应函数都需要埋点。需要埋点的响应函数与其他响应函数并没有区别。
对于这种情况,一种方式是加强code review避免忘记往配置表中添加埋点(这简直就是废话);一种是:要求埋点响应函数的方法名中包含约定的字符串,比如收藏事件的方法名为onFavBtnPressed_UA:表示这个事件是需要埋点的。然后在单元测试中使用运行时APIclass_copyMethodList取出标记了_UA的所有函数,随后到plist中校验是否存在。不存在则表示测试用例不通过,提示开发人员校验。
代码略。如果对单元测试不熟悉,可以参考单元测试
小总结:
合理的单元测试可以为本文方案的后期维护减轻相当大的负担,测试用例的完备性很重要,需要用心设计考虑周全。
五、结语
以上就是结合运行时所设计出的用户统计思路全部内容。应该说该方案的可复用性与解耦程度都是不错的,既适合于新建的工程,也适合于已经创建的工程。看起来内容多,其实总结起来无非几个步骤:plist配置表+Hook+单元测试。利用Method Swizzling把埋点代码集中管理其实也是合理的,有利于专人开发、跟踪及维护。当然以上思路只考虑简单的情形,更复杂的情况就需要变通了,但总体思路就是如此。
思路可能不完美,但作为一种尝试也未尝不可。路都是走出来的。
本文demo地址:https://github.com/weng1250/UserStatisticsWithRuntime
刚冒雨去外面买了个地球仪,毕竟五一假期这就结束了,世界这么大,买个地球仪,你不但可以看看,还可以随便转转。刚就尝试了下随便转转,觉得效果还可以。这手感,这速度,这体验,没的说!

这不,打算去浪漫之都巴黎逛逛,不到5秒就到了,省时省力。这以后假期,地球仪可能就必不可少了,毕竟这酸爽,也是没谁了~

网上这旅游攻略给的,对我而言参考价值也不大,严格意义上来说也没有我的区间,于是最终只能选择了朋友圈三日游了!
出去旅行总要啪啪啪和拍拍拍的,这每天被刷屏的,以前没见过面的学姐又嫁人了,以前旅游时搭讪的姑娘又换男友了,以前的死党又勾搭起小姨了子了,以前的发小又感慨孩子难带了。曾经学校操场上聊过两句的姑娘一天走了五万多步,这是脚踩风火轮了吗;以前的班主任家门前的丁香花又开了,感情开了几个月了是吗;以前大学同窗又远涉重洋见了各种海外人士,感情装逼不累是吗!由衷的赞美现代科技,这些在现实中早已走失在人海,却还能偶尔无关痛痒的和对方分享彼此的诗和远方,这长久以往积攒了无数不再有交集的ID。在这个通讯如此便捷的时代,更换个手机号可能就再也联系不上对方了,坚定如我,每个月固定为不怎么用的手机号按时交话费,尽管也没什么卵用~
这每次假期快过完都有想重新再来一次的冲动,一鼓作气再而衰三而竭吗,连着三次才过瘾不是。如果人类跑步的速度可以超过光速的话,这回到过去重新开始还是可以憧憬一下的,不过这样如果人类转圈跑的话,岂不是有可能make love with self,这也变相解决了很多问题,估计单身狗的独木舟更加屹立不倒了。
虽说这几天放假,不过百度的运营公关估计也是没闲着。这几天黑百度的浪潮真是此起彼伏,脉脉上对百度的讨伐也是连绵不绝,夹杂着多少不会真正思考的人们。百度的确需要承担责任,但却没见一个媒体敢质疑医院和政府。也是,在天朝现有体制下,就个人而言可能随时得被约谈,就企业而言可能随时得准备背锅。现在的媒体人又有几个能做到公平、公正、客观的呢,前些日子余太的医院接收了一对被撞的环卫工人,被媒体大肆报道是驾驶人酒驾导致,事后却被证明这是环卫工人闯红灯所致。多少新闻是为了吸引眼球而满嘴跑火车,多少人所谓的“圣母”心其实就是人云亦云。现在网上的键盘侠如果每人支援我一块钱的话,估计我现在早已是亿万富翁了。昨晚看了场西甲联赛,那弹幕的精彩程度不亚于比赛本身,各种人身攻击,多半以对方母上为圆心,七大姑八大姨为半径开骂,这是人文素养的缺失!越来越认识到那些心理阴暗、满怀心计的人是因为智力不够,这与以前的认识大致相反。我相信足够聪明的、进化的更好的人,通常会倾向于选择正义、善良这些优良的品质!
现在感觉身体真是大不如前,尽管我还那么的,那么的年轻。很多年轻人通宵上网那跟玩似的,第二天依然溜得飞起。而现在晚睡一会就觉得第二天状态不好。以前一个同事没事就劝诫我说“看你能坚持几年”,没想到这么快就切身感受到了。时间无情的地方,不仅仅是让人衰老,重要的是,犯了错再也不能把原因归结为年轻了。都说年龄越大应该越沉稳了,现在已经实现一半了都,的确是越来越沉了~
又要起身去接余太回府了,讲真,怪浪费时间的,但是却浪费的值得!这大半夜的,听一些抒情悲怆的歌,总有一两句歌词切中要害,感同身受,脑海里翻涌的画面让我想起了旧时光,似曾相识~
平静的日子可能会不甘寂寞,但嘈杂的生活又扰人清幽。所以人生的每个阶段尽力就好,其他的,交给命运!
如有任何疑问或问题请联系我:fishnewsdream@gmail.com,欢迎交流,共同提高!
Objective-C/Swift技术开发交流群201556264,讨论何种技术并不受限,欢迎各位大牛百家争鸣!
微信公众号OldDriverWeekly,欢迎关注并提出宝贵意见
老司机iOS周报,欢迎关注或订阅
刚刚在线工作室,欢迎关注或提出建设性意见!
刚刚在线论坛, 欢迎踊跃提问或解答!
如有转载,请注明出处,谢谢!
成年人的世界因为受实际生活和世间习惯的限制,所以非常狭小苦闷。反而孩子的世界不受这种限制,因此非常广大自由。越来越对丰子恺的这句话表示认同。
昨天还在下着大雨,今天就晴空万里。然而余太这两天都在上班,我也不好出去潇洒怕刺激到她。持续好几个星期了,怪蛋疼的。总有些行业作息没有规律,很不幸余太从事的就是,经常休息都是恰到好处的和周末避开,而且这工作我觉得还累的够呛。越来越觉得应该大幅提升上海护士的从业待遇,也许其他地方也有必要,至少应该觉得劳有所得,怪心塞的。大学五年每天都和高中似得那么努力,毕业后工作每天竟然还这么累,怪心疼的。不仅是医护行业,很多行业都是。
4月14号周四科比正式退役了,传奇落幕总是伤感的。50次的出手拿到60分准绝杀带队逆转取胜的结局实在完美,文字所带来的震撼并不直观,好在当时看的视频直播,实在热血沸腾,不能自已。本来和余太说我想请半天假好好看这最后一场,答案很果断的是不行,想来也是新公司入职才一周,频繁请假实在不合适。不过到了公司还是打开手机,腾讯视频有直播,常年会员,看起来不能更爽,虽然腾讯经常被人诟病,但是这些软件做的实在很不错。旁边的同事选择了看勇士创造历史的73胜,就这样两场比赛同时进行,但是同事却时不时的扭头过来看,其实团队里有些根本不是科密,但是却无人愿意错过这场比赛。比赛结束前伴随着一声又一声的卧槽,最终绝杀逆转谢幕。这个曾经让人一觉醒来觉得可以改变世界的男人,这三个赛季行走的异常艰难。都说这个赛季每场比赛都可以看到科比温和的笑容,但我每次看到这样的场景都异常难过,这根本不是应该出现的画面,凄凉又无奈。都说这就是好莱坞似的结局,传奇魔幻且意想不到,其实,赛前我有想到,想了很多不同的版本,最终演绎了一个最好的版本,不可思议又觉得自有天意。这位NBA历史最具竞争心态以最异于常人的努力最终成为了NBA历史最伟大的球员之一。晚上回来声情并茂的和余太分享了下,余太平静且淡然,真是友谊的小船说翻就翻,但爱情的巨轮却不会这么容易触礁,也是啊,这毕竟是我青春中追随的事情。其实出手过40次时心里很是担心,不希望临近末了还受这种诟病,然而即便结局如此完美,也依然有人肆意抹黑。也是,抹黑其他人也根本没人理会啊,重要的是黑的毫无水平,这种事在过去这些年发生的实在太多,让人作呕。各大门户网站上但凡一个新闻,下面的评论都是异常精彩,这些生瓜蛋子妄图旁征博引来说服他人,其实根本不可能。没有任何一个争论是值得争论的,因为根本得不到统一的结果,每个人观念的由来是这么多年成长的一个缩影,牵扯方方面面,怎么可能会因为一些话改变长久以来的价值观念。之前有两个同事,平时闲聊扯淡中发现总会有些普世观念在他们那里被解读的乌烟瘴气,没有统一答案受个人主观影响更多的一些事情,每个人的看法更不可能统一。但是我想,你可以不了解,但是不能不尊重。
近来一个在上海的朋友也加入了移动互联网的开发大军中来,很替他高兴找到了还算不错的工作。但是过程中的一些事情,我一直都嗤之以鼻的,但是我却一直向他表达了对这种方式的支持。的确,人们抱怨这个社会的不公平,其实大多是抱怨自己不是这种不公平中受益的一方。当这种不公平有利于自己的亲人或朋友身上时,又没有了之前的愤慨。也许目前只有这样,才能更容易的获得一个较好的起点,希望之后能遵从内心,也许改变不了这种风气,但是自己可以做到问心无愧。
一方水土养一方人,某种程度上其实我很信奉这句话,也可以说成是一个外在环境对一个人的成长会产生巨大的影响。昨天一朋友问了我一地方的风土人情,想了解下,我也就打探了下。最后询问了下,是关于对择偶的选择,婚姻的思考,人文的判断。我在大学时,我姐嫁到了浙江,离家说远不远,说近不近,这是对我而言,但是对我父母而言,这离家已经非常远了,当时我虽然不是特别支持,但是也没有反对。如果现在让我选择,我会毫不犹豫的反对。人们对自己亲人的另一半总是要求的格外严格,这毕竟关乎漫长人生的生活,可惜人在年轻时,经历的事情不够多,对未来想的不够远,总觉得自己是特殊的那一个。如果让我说,父母强烈反对的婚姻,听父母的,绝对有父母反对日后生活幸福的案例,但是没有一个父母希望子女去挑战这种小概率事件。另外,为父母,不远嫁,尽管现在这个社会很难做到了。 就我所看到的恋情可以长跑多年的,都是女方占有更大的话语权,无一例外,不然也无法持续多年。一个婚姻的幸福与否,更大程度上是由女人决定。现在很多女人都在为女性争取更大的权益,这得益于时代的发展,其实,她们宣称的更多的是女霸权,更多的享受权利,更少的履行义务。男人们向来喜欢沾化惹草,但别忘了每一个出轨的男人都至少对应着一个或多个小三或出轨的女人,而且这个世界还有那么多的女人从事着出卖肉体的工作,虽然她们服务的对象是男同胞,纵然男性理应承担更多一点的指责,但其实这是男女共同造成的结果。很多男性在承受非议时并不会大吵大闹,而女性却总是宣扬自己在婚姻中、爱情中付出的更多却得到的更少,其实根本不是这样。女性的确承担了更多的事物,大多是可见的,但是男性也同样承担了更多的精神上的来自亲人的依靠。这种分工的不同很容易造成对男性的偏见,毕竟女性的很多事都可以量化,而压力却不可以。虽然渣男更多,但是烂女也不少,如果遇到,还是多从自己身上找原因吧。
近来大事虽没有,琐事却不断,希望尽快解决一切,让我可以尽全力的去迎接工作。但是驾照和工作室的事情怎么着估计也得到六月份了,各种消耗精力,没法不蛋疼~
想想上次全身心的去工作还是几个月前,状态迟迟无法找回,心塞塞的,尽力而为吧~
如有任何疑问或问题请联系我:fishnewsdream@gmail.com,欢迎交流,共同提高!
Objective-C/Swift技术开发交流群201556264,讨论何种技术并不受限,欢迎各位大牛百家争鸣!
微信公众号OldDriverWeekly,欢迎关注并提出宝贵意见
老司机iOS周报,欢迎关注或订阅
刚刚在线工作室,欢迎关注或提出建设性意见!
刚刚在线论坛, 欢迎踊跃提问或解答!
如有转载,请注明出处,谢谢!
三月底了,金三银四这就过一半了,好快!世界这么大,也没人带我去看看~
上海的春天总体来说天气还算不错,让我想起这个季度的泉州。以往总是无法抗拒这么好的天气,毕竟出去旅行与这个天气更搭啊。然而余太总是没有时间,一次次的爽约,而且爽出惯性来了,总不至于让我和一群糙老爷们出去踏青吧,总是感觉辜负了这么好的天气!
3月底调休清零,在这个流量现在都可以保留到次月的年代,也能赶上调休清零的公司,也是难得。这些天上班断断续续,无法堕落更多。早上看了场NBA比赛,想到我科即将退役了,悲从中来。虽然不怎么打篮球,然而喜欢科比却已很多年了。没了科比,的确没什么欲望去看了,虽然有时间偶尔还是会去看几场。想到那些科黑想必也会感觉到些许落寞吧,很多估计也黑出了感情。想想老大对一代人的激励,也足够了,然而想想依然非常非常非常难过。“在生命中的有些时候,你必须去挑战伟大”,看到你曾经说的这句话,至今我依然作为我个人的座右铭,不管是否真正起到了作用。 早已过了所有问题非要争出对错高低的年纪,但是很多时候还是无法容忍一些粗浅幼稚的论断,最主要的是,你无法改变别人固有的观点,基本不能。这个社会分工明确,总有些人需要去充当被人鄙视的角色,每个人必然都经历过,心里互骂对方傻逼后该干嘛还是干嘛去,没改变任何。等待下一个会让我如此热爱的人,也许不会再有。
上周看了两部很久前的电影,《触不可及》和《黄金时代》,抛开影片质量而言,这是难得的走心得影片,我喜欢这种风格的电影,在当今的社会风气下尤其难得。这几年产出了太多的垃圾影片,是的,我拍不出来,you can you up这种欠缺思考的言论依然会被很多人拿来做挡箭牌,仿佛你说鸡蛋不好吃的时候别人不去考虑为什么不好吃反而会说你为什么不去下个蛋一样荒谬!那种你多看她一眼便能脑补一出韩剧,这种情节依然时不时的会被搬上荧屏,屡屡被刷新下限,但谁让他们可以凭此赚钱呢?林子大了什么鸟都有,人多了什么奇葩也都不算奇怪,一个明星的走穴费用也许一个教师穷其一生也积攒不到,想想都悲哀。存在并非就一定合理,改变也许需要时间,也许会更为恶劣!
近来在上海的小伙伴又要离开一个,前往合肥。众宝宝都纷纷离去了,虽然不舍,但是路终归得选,我还在坚守,在可预见的未来内,我都会待在上海,坐等你们回来。对城市选择的不同也许就决定了对不同生活的选择,遵从内心就好,都好好的,比什么都重要。那些多年未联系的朋友就不要试图联系了,是的,不要联系了,尘封在记忆中就好,虽然我都还记得。
现在扯蛋已成为一种习惯,深受逗逼境伟的影响。今年的互联网行业感觉是时候到了重新洗牌的时候了,混乱不堪,择业艰难。明天正式离职了,很不舍! 近来各种琐碎的事情,也幸亏公司没什么事。不想抹黑公司,同事也都非常不错,对于我这种重感情的人来说,离职是痛苦的,离职前也是经历了长久的挣扎,很多人都是!我已经很久没有进步了,倍感焦虑,这是我离职最主要的原因,尽管这不全是公司的错! 我们这些从事于移动互联网行业的人来说,很多人可能熬不过去。现在的培训结构太过猖獗了,当技能可以量产的时候也就不算是一门技能了。各行各业的人都加入了培训大军,良莠不齐。“你弱你有理”的言论常备提及,还说的理直气壮,不断抬高的招聘门槛更加滋生了这种言论。培训机构有什么错?懂得人自然都懂,不懂得人是并不想懂。这作为一种入门途径无可厚非,但是弄虚作假才是被人厌恶的根源。现在有深深的危机感, 普通庸俗的大学并没有反向激励我,而是临近毕业才突然觉醒。毕业文凭的差距很多时候代表了学习能力的差距,我花了很长时间才意识到这点。作为企业,并不需要花费成本去筛选那些低文凭中的佼佼者,所以很多时候自然就抬高了门槛,毕竟学历好的人拥有更大的可能性拥有更好的学习能力!还有很多人可能忽视的一点,更好的学识在一帆风顺时也许看不出作用,但一旦陷入窘境,这会形成四面八方的支援,自行体会吧。
就这样!依然感谢大数聚!清明回来后入职新公司,这是新的开始!
如有任何疑问或问题请联系我:fishnewsdream@gmail.com,欢迎交流,共同提高!
Objective-C/Swift技术开发交流群201556264,讨论何种技术并不受限,欢迎各位大牛百家争鸣!
微信公众号OldDriverWeekly,欢迎关注并提出宝贵意见
老司机iOS周报,欢迎关注或订阅
刚刚在线工作室,欢迎关注或提出建设性意见!
刚刚在线论坛, 欢迎踊跃提问或解答!
如有转载,请注明出处,谢谢!
一直想写又不想写。因为近来网上关于Autolayout的文章不在少数,重复相同的内容没什么意思。但是只写不同的地方感觉我也写不出多少,比较心塞。既然坑都占了,那就先意思一下,接下来发现Autolayout其他特性或比较高深的用法会再继续补充。
自iOS6苹果就推出了Autolayout,但那时候很多人都在用AutoresizingMasks,国内对新鲜出土的技术并没有那么热衷,最主要的是改变终归是痛苦的,所以鲜有应用。iOS8之后新推出了iPhone6、iPhone6+及Sizeclass,这时候不得不使用Autolayout了。现在觉得这其实也并不是必须的,代码适配也不是不可以。
现在代码添加约束一般有三种,常规的约束语法、可视化格式语言约束、使用第三方组件。
代码基本长这样
高度约束(添加到yellowView身上)
1 | NSLayoutConstraint *heightConstraint = [NSLayoutConstraint constraintWithItem:yellowView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:0.0 constant:50]; |
间距约束(添加到self.view身上)1
2
3
4
5
6
7
8CGFloat margin = 20;
[self.view addConstraints:@[
[NSLayoutConstraint constraintWithItem:yellowView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeft multiplier:1.0 constant:margin],
[NSLayoutConstraint constraintWithItem:yellowView attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeRight multiplier:1.0 constant: - margin],
[NSLayoutConstraint constraintWithItem:yellowView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeBottom multiplier:1.0 constant: - margin]
]];

用到了一个基本公式1
obj1.property1 = (obj2.property2 * multiplier)+ constant value
multiplier和constant 是向量系数和偏移量,一般还要设置translateAutoresizingMaskIntoConstraints为NO,默认是YES,使得不和系统的自动伸缩约束冲突
可视化格式语言,即Visual Format Language(VFL),是为了简化Autolayout而推出的抽象语言。
基本长这样1
2
3
4
5
6UIButton *b1 = ...
UIButton *b2 = ...
UIButton *b3 = ...
b1.translatesAutoresizingMaskIntoConstraints = NO;
b2.translatesAutoresizingMaskIntoConstraints = NO;
b3.translatesAutoresizingMaskIntoConstraints = NO;
将传入的对象引用作为value,将引用名变成字符串作为key,生成的字典如: {@”b1”:b1,@”b2”:b2,@”b3”:b3}1
NSDictionary *diction1 = NSDictionaryOfVariableBindings(b1,b2,b3);
为vfl式子中的一些特殊含义的数字做一个名称对照表1
NSDictionary *diction2 = @{@"top":@20,@"left":@20,@"right":@20,@"spacing":@10};
1 | NSString *hVFL = @"|-left-[b1]-spacing-[b2(b1)]-spacing-[b3(b1)]-right-|"; |
部分使用规则如下:
|: 表示父视图
-:表示距离
V: :表示垂直
H: :表示水平
>= :表示视图间距、宽度和高度必须大于或等于某个值
\<= :表示视图间距、宽度和高度必须小宇或等于某个值
== :表示视图间距、宽度或者高度必须等于某个值
|-30.0-[view]-30.0-|: 表示离父视图 左右间距 30
[view(200.0)] : 表示视图宽度为 200.0
|-[view(view1)]-[view1]-| :表示视图宽度一样,并且在父视图左右边缘内
V:|-[view(50.0)] : 视图高度为 50
V:|-(==padding)-[imageView]->=0-[button]-(==padding)-| : 表示离父视图的距离
为Padding,这两个视图间距必须大于或等于0并且距离底部父视图为 padding。[wideView(>=60@700)] :视图的宽度为至少为60 不能超过 700
这里只是基本的部分使用规则,要了解更多请自行查阅。
UIView+Autolayout下载地址:https://github.com/ChinaFishNews/UIView-AutoLayout.git
Masonry下载地址 : https://github.com/ChinaFishNews/Masonry.git
在最初的时候使用的是UIView+Autolayout,后来发现Masonry就基本使用Masonry了,这两个用起来都很好上手。
直接设置大小1
[_redView autoSetDimensionsToSize:CGSizeMake(200, 200)];
设置除顶部距四周的间隔,然后设置高度1
2
3[_redVeiw autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsMake(20, 20, 20, 20)excludingEdge:ALEdgeTop];
[_redView autoSetDimension:ALDimensionHeight toSize:100];
直接设置四周间隔1
[_redView autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsMake(0, 0, 0, 0)];
1 | UIEdgeInsets padding = UIEdgeInsetsMake(10, 10, 10, 10); |
或者直接这样1
2
3[view1 mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.equalTo(superview).with.insets(padding);
}];
1 | make.top.mas_equalTo(42); |
1 | [view1 mas_makeConstraints:^(MASConstraintMaker *make) { |
这只是随便举例,想更详细的了解可以到Github源码处具体学习下。
以上,代码添加约束总体较为繁琐,然而有些时候布局比较繁杂、不方便在Storyboard添加的只能只用代码来添加约束了。现在对我而言基本是一般约束直接在Storyboard上拖拽添加,一些不太方便的需要代码添加约束的使用Masonary。
iOS9之后已经可以使用引用了,把Storyboard拆分成多个。多人合作项目的时候经常会产生冲突,Storyboard的冲突改起来比较酸爽,Sotryboard其实是一个巨大的XML文件,之前每次产生冲突都需要以XML文件的形式打开Storyboard修改冲突。即便现在可以把Storyboard拆分成多个,但有时冲突依然不可避免,XML文件其实也并不可怕,毕竟多改几次就习惯了。
添加约束其实比较降低效率,随着布局的繁杂效率下降得更为明显,只在在硬件提升的同时一般我们体会的并不明显。iOS9之后新加了UIStackView,一个管理视图的容器,这样只需要对外面的这个容器添加约束,里面的只需要设置一些属性,虽然归根结底还是设置约束。虽然UIStackView是iOS9之后,但是网上已经有支持iOS9以下的框架,叫FDStackView,下载地址:
https://github.com/ChinaFishNews/FDStackView.git
下载后导入项目即可,无需做其他配置,即可在iOS9以下使用StackView。
先上个图大家感受一下Autolayout

默认大家都已经可以基本使用Autolayout.
一般在右下角可以方便的添加约束,也有人通过菜单栏中Editor设置,其实也可以按住control键,点击控件拖拽

添加约束后有时想知道在不同设备上的布局,这时候可以选中控制器,改变Size的大小,也可以这样
按住option键点击Main.Storyboard

然后可以添加设备

还有时候想观察视图的层次机构,可以在运行时点击Debug View Hierarchy

其实还可以使用工具Reveal,之前的博文中有讲到。
在添加约束的时候,左侧一般是这样的

为什么有的约束在这个控件下,有的约束在其他控件下呢? 是因为只有设置当前控件宽高的时候约束才会出现在当前控件下,设置和其他控件相对位置、距离的时候,会找到他们最近的父控件,在他们最近的父控件下显示约束。
使用Autolayout的时候,有个属性IntrinsicContentSize,UILabel,UIButton,UIImageView拥有这个属性,可以自己根据内容调整大小,不用设置宽和高。文字有多长就显示多长,图片有多大,就显示多大,对于哪些View有IntrinsicContentSize,苹果给了一张表:

这张表显示,UIView和NSView是没有IntrinsicContentSize的。Sliders只能定义width。Sliders的height拥有IntrinsicContentSize,Labels, buttons, switches,and text fields完全支持,Text viewsand image views,在有内容的时候支持,没有内容的时候不支持。对于没有这个属性的,我们也希望他拥有默认宽高的话该怎么办呢?我们可以重写IntrinsicContentSize方法,这样就可以拥有默认的宽高1
2
3
4- (CGSize)intrinsicContentSize
{
return CGSizeMake(100,100);
}
对于IntrinsicContentSize,Autolayout又把他分成了2个部分:ContentHugging和CompressionResistance。我们点击控件的时候在右侧会发现

ContentHugging是内容凝聚力,优先级是250,表示View的宽度和高度紧靠内容,不让其扩展的力量
CompressionResistance是指压缩阻力,优先级是750,表示当有力量要对其进行压缩的时候,其阻力的大小
对于同一个View,ContentHugging和CompressionResistance不会同时起作用
而我们添加约束的时候是这样的,优先级是1000

当一个label有文字的时候,label会存在一个内容的Size。
如果有外力让其size扩张,ContentHugging会起作用,外力大于ContentHugging的力量,label的size由外力决定,反之,label的Size由内容决定。
如果有外力让其size压缩,CompressionResistance会起作用,外力大于CompressionResistance的力量,label的size由外力决定,反之,label的Size由内容决定。
也就是说如果我们给label加了一个宽度约束,line设为0,我们在给line设置内容的时候,一旦内容size大于其原有大小的时候label的高度就会增加,而当我们只给label添加一个宽度的时候,一旦我们设置的内容size大于其原有宽度的时候label的宽度就会增加。而当我们同时设置宽度和高度约束的时候,无论我们怎么设置内容,其就会被固定在我们设置的这个size里,因为添加约束的优先级默认是1000,优先级最大,默认会执行这个。
constraint的优先级(Priorities),优先级越高,力量越大,越被优先执行。系统的优先级由1~1000的数字表示,值越大,优先级越高。NSLayoutConstraint中一共定义了4种比较常用的优先级1
2
3
4
5
6
7
8
9typedef float UILayoutPriority;
static const UILayoutPriority UILayoutPriorityRequired NS_AVAILABLE_IOS(6_0) = 1000;
static const UILayoutPriority UILayoutPriorityDefaultHigh NS_AVAILABLE_IOS(6_0) = 750;
static const UILayoutPriority UILayoutPriorityDefaultLow NS_AVAILABLE_IOS(6_0) = 250;
static const UILayoutPriority UILayoutPriorityFittingSizeLevel NS_AVAILABLE_IOS(6_0) = 50;
UILayoutPriorityRequired: 必须级别优先级,值为最高值1000,一般平时定义约束,默认都是这个优先级。
UILayoutPriorityDefaultHigh: 高优先级,值为750,CompressionResistance的默认优先级是这个。
UILayoutPriorityDefaultLow: 低优先级,值为250,ContentHugging的默认优先级是这个
UILayoutPriorityFittingSizeLevel: 极低的优先级,让系统估算Size的时候使用,不适合做约束
知道了各个属性的默认优先级之后,就可以解释为什么一般情况我们给Lable设置Size约束之后,Label由我们设置的Size决定,而不是由其内容决定。因为我们没有特意设置优先级,用的都是默认优先级。Size约束的优先级比CompressionResistance和ContentHugging的优先级高。如果我们想让Label由内容决定,我们可以不设置Size约束或者调低自己Size约束的优先级。
上面参考了网上的一篇文章,讲的很赞,并且给了一个案例。
假设有2个Label,并列放着,他们都是使用IntrinsicContentSize自动根据文字适应宽度。效果如图所示:

那么我们设置一个优先级为500宽度为100的约束(100小于Label2的宽度,大于Label1的宽度)
结果是2个Label都变成100的宽度,还是都保持原来的宽度不变?还是一个变成100,一个保持原来的宽度?我们Run一下:

Label1变成了100,Label2还是原来的宽度,为什么呢?
Label1的IntrinsicContentSize宽度比100小,所以当添加一个宽度为100的约束时,ContentHugging在起作用。ContentHugging的优先级为250。宽度为100的约束优先级为500大于ContentHugging。所以宽度为100.
Label2的IntrinsicContentSize宽度比100大。所以当添加一个宽度为100的约束时,CompressionResistance在起作用,CompressionResistance的优先级为750。宽度为100的约束优先级为500小于CompressionResistance。所以宽度还是IntrinsicContentSize的宽度
正常情况下我们在设置Label的时候,给它加个宽度约束,运行是这样的

款读设置的是100,高度没有设置,内容过多的时候回自动自适应,不用使用代码计算size。但是我们设置宽度是100,但是内容太少的时候右边会有空白,这时候怎么办呢?我们可以将宽度设为less than or equal 100,这样在内容少的时候就是这样的

这样就解决了内容少而右边有空白的问题。有时候可能没有内容,但是我们又希望它占位,这时候该怎么办呢?我们可以这样设置,即便在没有内容的时候依然在占位

使用的过程中被还会有一些其他的问题
对于Autolayout,制作动画和以往也有些不同,使用Frame的时候,我们做动画一般都调用-animateWithDuration:animations:方法,在animations的block里面调整Frame即可,使用Autolayout之后,由于Autolayout是延迟布局的,并不是约束更新之后就立刻布局,所以大家可以发现。在-animateWithDuration:animations方法里面修改约束是不能实现动画的。而使用Autolayout该怎么做呢? 应该是在block外改变约束,在block内调用layoutIfNeeded方法。
Autolayout自动布局是在UIView的layoutSubviews中,所以TableView上的子view(如:cell,headerView,footerView)使用了Autolayout,tableView在布局的时候调用layoutSubviews,就会抛出异常。直接对TableView使用Autolayout是不会有问题的,TableView是否调用layoutSubviews在于他上面的子view是否使用Autolayout,而不是他本身。
正确的做法是:如果是cell,我们经常使用[cell addSubview:view]再对view做一个相对cell的约束,这时候就会出现问题。解决方案就是使用[cell.contentView addSubview:view]。我们约束是对cell的contentView添加,跟cell无关。tableView就不会调用layoutSubviews了。
如果是headerView或者footView。解决方案是直接使用frame,或者自己定义一个类似Cell的contentView的view,子view相对contentView布局使用Autolayout,contentView对headerView布局使用frame。使用中还会发现一个特殊的控件,UIScrollView,因为contentSize跟着里面的子view发生变化。正确的做法是给UIScrollview添加一个ContentView,设置edge均为0,如果垂直滚动那就设置一个宽度,反之设置一个高度约束。
使用Autolayout之后,系统中多了一个更新约束的方法updateConstraints。updateConstraints方法仅用于提升性能。当你更新大量约束,发现由于约束太多,布局有点卡。这时候你可以使用updateConstraints,因为在updateConstraints中更新约束会批量操作,能获得更好的性能。
以上,只是Autolayout部分用法,具体的需要在项目中体验,注意点比较多。
就先到这里,今天妇女节,祝广大女同胞节日快乐。觉得应该放半天假,然而这么人性化得公司毕竟不多!
如有任何疑问或问题请联系我:fishnewsdream@gmail.com,欢迎交流,共同提高!
Objective-C/Swift技术开发交流群201556264,讨论何种技术并不受限,欢迎各位大牛百家争鸣!
微信公众号OldDriverWeekly,欢迎关注并提出宝贵意见
老司机iOS周报,欢迎关注或订阅
刚刚在线工作室,欢迎关注或提出建设性意见!
刚刚在线论坛, 欢迎踊跃提问或解答!
如有转载,请注明出处,谢谢!
iOS开发中证书制作和项目上传发布这一块,很多初学者都对此比价困扰,有些即便是工作有些时日的对此可能也不是很清楚,毕竟可能只是写个代码,这一块并不怎么参与。我刚工作那会开始时一直在网上看了很多文档,总是过一段时间就会忘,后来参与项目中操作几次基本就很顺手了。
我也见过工作几年的对此不了解的人,然而这什么都说明不了,人家工作能力依然是很强。只是术业有专攻,在很多专项上造诣很深,但是这却被很多人低估。
我所见过的觉得技术比较厉害的人,身上并没有什么戾气,相反会觉得自己知之甚少,毕竟接触的越多觉得不会的就更多。而表现很嚣张的人一般有两类,一是在大学时就有接触,并且小有研究,这时候正是心高气傲的年纪,经常会指点江山,越俎代庖替那些行业大佬做决断,另一种就是刚工作没多久觉得了解的差不多了的人,能力大小决定视野层次,有时候真的觉得这也许和能力没有关系,只和为人有关。犹如以前看到的一段子,一人问学计算机专业的朋友能不能帮忙盗个QQ密码,帮忙写个游戏外挂,帮忙刷个票等,得到不能的答案后心里嘀咕:”你tm不是学计算机的吗”。正因为学计算机的,所以才知道这事情的难度,然而外人可能并不知道,理所当然的觉得这并非难事,说不定还会想这点事都办不成这tm的也算是计算机高材生?工作以来偶尔在群里给一些人解决问题,慢慢的意识到一些现象,现在也就仅限于常联系的那几个人了。别人问你问题的时候,也许是听别人说了你很厉害,也可能只是单纯的看你工作几年了觉得肯定有一定能力,也许仅仅是因为看到你刚刚为其他人解决了一个问题,这时候如果你不会,那结果很自然,他们会觉得这货也并不怎么样,而如果你会但是你所讲的可能他们并不是听得很懂,他们又会觉得你是在装逼,可能是在不懂装懂,只有你很顺利的解决他们的问题并且他们很快就能理解的时候,这才是一次皆大欢喜的问答流程,然而这种可能性并不大,随着问题的难度越来越深,这种可能性越来越小。所以现在很多时候,如果我觉得并不能马上解决,或者要解决又需要科普一些其他的技术要点,或者本身就不太清楚,干脆就直接说不会或者没时间,这已经违背了我的意愿,然而我却越来越多的这么做,这不是一个好现象。
在生活中也时常会有这种问题,本身计算机专业,但是我大学时是个典型的渣渣,虽然现在也是,但是大学更甚。经常会有朋友的电脑出问题打电话咨询我,尽管经常我觉得回答的还可以,但问题却少有解决,所以我一次又一次的让他们失望了,虽然有时觉得这根本不是一个问题,但是结果很明显。有时是他们的执行力的问题,有时是我的问题,或描述不清楚,或力所不能及。我把这当成一个遗憾,没错,是遗憾。
有人会说,真正的大牛并不介意他人的观点,毕竟实力在那摆着。这话从来都是个伪命题,纯粹是在扯犊子,难道包括诋毁吗?古代中国文人无法容忍别人看低自己的风骨,现在做技术的也一样不能接受别人对自己的质疑,尤其是来自技术不堪的人的质疑。哪些人可以做到“我不在意,便伤不到我”这个境界,只有那些功成名就,已经不需要考虑外界声音的时候。我对陌生人的贬低、谩骂、不信任毫无在意,但是我的那些朋友的声音我没法不在意,因为他们是我生活的一部分,是这样。
以上,旨在说明,一切技术要点并不是衡量一个开发人员的能力标杆。闻道有先后没错,术业有专攻也是真的,别去妄图通过某个单一指标去判断技能水平,很多人总是惊艳与无形处!
这都是在扯犊子,说正经的。
开发者账号分为个人、公司和企业,当然还有教育结构。公司账号相比个人账号多了些账号管理的设置,允许多个开发者协作开发,在申请时须填写公司的邓白氏编码(DUNS Number),企业账号不能发布应用到App Store,只能企业内部使用,申请时也需要邓白氏编码。而教育账号费用0美元 ,只能教育机构或学院内部使用,必须是苹果iOS开发者计划授权机构,不能对外正式发布iOS应用程序。
开发者账号在申请后,需要创建开发证书和发布证书。一般而言,一个账号需要一个开发证书和一个发布证书,使用过程可能还有若干个推送证书。其实可以创建不止一个开发和发布证书,但是再次创建的时候,之前的就不能使用了,所以账号申请下来以后,开发和发布证书创建好后,下载下来,导出私钥,保存好,在组内其他成员使用的时候避免再次创建开发和发布证书,只需把私钥给他即可。
账号申请下来后如何创建证书呢?下面演示一下流程
首先打开钥匙串,创建一个证书签名请求文件,点击从证书颁发机构请求证书

电子邮件地址填写申请的开发者账号,选择存储到磁盘,找个位置存放

接下来进入官网,如下图,Certificaes下面的即是该账号下的证书文件,1是推送证书(发布模式),对应的Type是Apple Push Services,2是开发证书,对应的Type是iOS Development,3是发布证书,对应的Type是iOS Distribution,4是推送证书(开发模式),对应的Type是Apns Development iOS,如果项目不需要推送,推送证书完全不需要创建,只需要一个开发证书一个发布证书即可。初次申请账号后这里面是空的,需要自己创建一个开发和发布证书。点击右上角的+号

然后选择创建的证书类型,1是开发证书,2是推送证书(开发模式),3是发布证书,4是推送证书(发布模式),选择后然后下一步

这一步是选择App ID,也就是项目的唯一标示,对应的是项目中的Bund ID,在制作推送证书的时候需要这一步,普通的开发证书和发布证书不会出现这一步,接下来会讲到

Choose File点击选择刚才我们生成的证书签名请求文件,再下一步即生成了相应的证书。把证书下载下来,双击安装,这时钥匙串里就会显示我们生成的证书。

打开钥匙串,如下图,我这里1、2都是推送证书(开发模式),3、4都是推送证书(发布模式),5是系统证书,6、7、8是开发证书,因为我有三个开发账号,所以这里有三个开发证书,9是发布证书。这个时候官网上就会显示我们生成的证书,但是别人下载的时候安装并不能使用,因为没有证书签名请求文件,安装后会显示缺少私钥,就是8、9下面钥匙显示的地方。那别人怎么拿到这个证书呢?记住,不要重新去创建,会导致之前的不能使用,需要我们把开发和发布证书的私钥导出来,可以随便设置一下密码,发给需要用到的人,这个导出的p12文件包含证书和签名请求。以前在Xcode4.5的时候还有一种导出方法,就是通过Xcode直接导出来一个文件即可,但是Xcode4.5之后就不再支持这种方法了。

另外,证书签名请求文件,即我们第一步生成的那个CSR文件,只能使用一次,如果需要再次制作证书需要重新生成证书签名请求文件。证书做好之后,如果我们想真机调试,还需要做什么呢? 我们每个程序都有一个Bund ID,用来唯一标示一个程序。这个Bund ID在推送的时候需要使用到,苹果需要根据Bund Id来找到手机上的指定应用。下图Identifiers目录下App IDS就需要填写项目签名,也就是标示。这里是使用反向域名的形式,一般是com.公司名.*。分为两种,1是Explict App ID,固定标示,这个要和项目的Bund ID严格一致,2是Wildcard App ID广域标示,*是通配符,可以匹配所有项目。如果项目需要用到推送,需要创建一个固定标示,因为推送需要根据Bund ID找到手机上的应用。如果不需要推送,两种都可以使用,为了方便,不推送的情况下建议使用通配符

下图是我的一些App ID,1、2、5均使用了通配符,其中使用1的App ID可以调试所有的应用,3、4是固定标示。

接下来还要添加我们的设备,告诉苹果哪些设备需要用来调试。个人和公司账号都是99台的上限,企业是299台,只在开发模式下需要。将设备的UDID添加进来


接下来是生成描述文件

描述文件分为三种,开发(iOS App Development)、发布(App Store)和测试(ADHoc),ADHoc是指打包后用来测试的,有时候需要在上传到App Store前测试一下,选择后进行下一步

这个时候需要选择App ID,如果选择含有通配符的App ID,那就可以运行在所有符合这个App ID的项目中,如果选择固定标示的App ID,那生成的描述文件则只能运行在指定Bund ID的项目上。

这一步需要选择证书,如果上一步选的是开发环境下的描述文件,那这一步就需要选择开发证书,一般来说这里只有一个开发证书。但是也有可能其他人也创建了证书,那这里就可能不止一个了。可以根据过期日期判断创建先后顺序,不要同时选择多个,只需要选择最新的证书,反之这里就是发布证书。

这一步是选择需要调试的设备,只在开发模式下和ADHoc模式下才会有这个选项,一般都全部勾选

这就生成了后缀为.mobileprovision的描述文件,可以通过这个描述文件判断类型,是开发还是发布,还可以看App ID,所选设备数、过期日期等信息。然后下载下来双击安装

这样所需要的文件就全部生成了。在Xcode中需要做些配置。1处是开发证书,点击如果没有显示,可能是没有证书或者缺少私钥,说明之前的步骤有问题,需要重新检查一下,2处点击应该显示的是发布证书,3、4分别设置为开发和发布模式下的描述文件。

注意,有时描述文件可能会被更新的比较频繁,因为调试设备一旦增加就需要更新描述文件,有的人会通过Xcode直接生成描述文件,这样官网上描述文件的后面一半都会标有Mangeged by Xcode,最好别这么干,一来显的不专业且比较乱,二是极端情况下可能会导致该账号下的所有证书和描述文件都不可用。在更新描述文件的时候,有时候下载下来安装好后依然不可用,这可能是因为系统还在使用之前我们本地的描述文件,这个时候可以去存放描述文件的目录下删除,如下图所示

但是名字都是一串数字,怎么判断对应的是哪个描述文件呢?可以到Xcode中去查看,点击选中的描述文件,再点击other就会出现对应的一串数字。现在Xcode感觉是越来越不稳定了,很多时候都感觉反应慢半拍,所以再重新下载安装描述文件的时候最好先把之前对应的删了。

这时候就可以真机调试了。如果其他组员也参与项目需要真机调试怎么办呢?之前有说到,如下图,到钥匙串中导出对应证书的私钥给到你的组员,这个私钥其实包含证书和证书签名请求,双击安装即可。至于描述文件你给到组员或者他们自己到官网去下载都可以

在项目完成后,需要打包测试或提交到App Store,在下图Provisioning Profile下Release模式下选择对应的描述文件,Coding Signing Identity下Release模式下选择相应的证书,直接选Automatic也可以,系统可以智能识别。然后菜单栏Product下选择Archive开始打包。注意,在选择设备的时候最好不要选择你的手机,而是选择Generic iOS Device,否则只会在你当前手机环境下进行编译打包

这时候可以直接点击Upload to App Store,接下来还有选择开发者账号这一步,但是这种通过Xcode直接上传的方式近来很多人都说特别慢或者上传不成功,上传失败的话可以使用另外一种方式Application Loader。右下角有是否下载dSYM文件,也就是符号集,每一个.dSYM文件都有一个UUID,和.app文件中的UUID对应,代表着是一个应用。而.dSYM文件中每一条崩溃信息也有一个单独的UUID,用来和程序的UUID进行校对。使用友盟或者百度错误统计的时候,一些运行时或者内存错误并不能判断出具体位置,这个时候就需要通过.dSYM文件。每次Archive一个包之后,都会随之生成一个dSYM文件,当程序崩溃的时候,我们可以获得到崩溃的错误堆栈,但是这个错误堆栈都是0x开头的16进制地址,需要我们使用Xcode自带的symbolicatecrash工具来将.Crash和.dSYM文件进行符号化,就可以得到详细崩溃的信息。

点击Export导出包,分别是App Store、测试、企业分发和开发包,选择后下一步

注意,左下角的对勾默认是不勾的,如果是企业账号打包,要做企业分发,需要勾选左下角的对勾

然后就需要填写ipa在服务器上的路径及应用名称,这和以前企业打包的时候流程有些偏差.Xcode 6以下ipa 和 plist 同时生成,Xcode6以上 只生成ipa,不过plist文件在包里也可以找到,然后将这两个文件交给服务器人员,然后将plist在服务器上的地址加到字符串“itms-services://?action=download-manifest&url=”之后,将这个地址用浏览器打开就会提示安装应用。

如果只是个普通的测试包,可以直接上传到蒲公英分发平台,通过链接下载安装即可。如果上传到App Store可以通过Aoolication Loader上传,每次上传我都会打开Vpn,5m/s的上传速度简直不能更爽,但是很多人会卡在这一步,有时候因为网络原因会上传失败。一般在早上上传成功的可能性更大一些。

以上,即是整个流程。包括账号申请、续费、Ituens上信息填写就不在赘述了,很多文档都比价齐全。现在整体比以前简单多了,苹果一直在优化,所以有些流程可能会有小的出入。
最后,扯个淡,事情没办完的感觉是痛苦的,希望早点结束,可以让我全身心的去投入战斗!
只是希望!
如有任何疑问或问题请联系我:fishnewsdream@gmail.com,欢迎交流,共同提高!
Objective-C/Swift技术开发交流群201556264,讨论何种技术并不受限,欢迎各位大牛百家争鸣!
微信公众号OldDriverWeekly,欢迎关注并提出宝贵意见
老司机iOS周报,欢迎关注或订阅
刚刚在线工作室,欢迎关注或提出建设性意见!
刚刚在线论坛, 欢迎踊跃提问或解答!
如有转载,请注明出处,谢谢!