iOS runtime之--动态修改字体大小
介绍一下runtime的实际应用场景之一:怎样利用runtime的方法交换,在不修改原有代码的基础上动态的根据屏幕尺寸修改字体大小,包括xib和storyboard中拖的控件。
我们知道,通常代码设置字体大小用的是UIFont的几个类方法 :
systemFontOfSize
fontWithName:size
boldSystemFontOfSize
italicSystemFontOfSize
...
那么既然runtime可以进行方法交换,我们只要自定义一个方法,替换系统的方法不就可以实现了吗?话不多说,我们开始动手
实现NSObject类方法交换
创建NSObject分类,并增加一个可进行“Method交换”的方法。Method交换的本质,其实就是imp指针的交换。系统给我们提供了一个C语言的函数method_exchangeImplementations可以进行交换。流程如下:
1.根据原方法和目标方法的selector,获取方法的method。如果是类方法用class_getClassMethod获取method,如是对象方法则用class_getInstanceMethod获取method
2.获取到method后,调用method_exchangeImplementations函数进行两个method的imp指针的交换
#import "NSObject+Category.h"
#import
@implementation NSObject (Category)
/**
@brief 方法替换
@param originselector 替换的原方法
@param swizzleSelector 替换后的方法
@param isClassMethod 是否为类方法,YES为类方法,NO为对象方法
*/
+ (void)runtimeReplaceFunctionWithSelector:(SEL)originselector
swizzleSelector:(SEL)swizzleSelector
isClassMethod:(BOOL)isClassMethod
{
Method originMethod;
Method swizzleMethod;
if (isClassMethod == YES) {
originMethod = class_getClassMethod([self class], originselector);
swizzleMethod = class_getClassMethod([self class], swizzleSelector);
}else{
originMethod = class_getInstanceMethod([self class], originselector);
swizzleMethod = class_getInstanceMethod([self class], swizzleSelector);
}
method_exchangeImplementations(originMethod, swizzleMethod);
}
@end
UIFont设置font的类方法替换
#import "UIFont+Category.h"
#import "NSObject+Category.h"
@implementation UIFont (Category)
//+(void)load方法会在main函数之前自动调用,不需要手动调用
+ (void)load
{
//交换systemFontOfSize: 方法
[[self class] runtimeReplaceFunctionWithSelector:@selector(systemFontOfSize:) swizzleSelector:@selector(customSystemFontOfSize:) isClassMethod:YES];
//交换fontWithName:size:方法
[[self class] runtimeReplaceFunctionWithSelector:@selector(fontWithName:size:) swizzleSelector:@selector(customFontWithName:size:) isClassMethod:YES];
}
//自定义的交换方法
+ (UIFont *)customSystemFontOfSize:(CGFloat)fontSize
{
CGFloat size = [UIFont transSizeWithFontSize:fontSize];
///这里并不会引起递归,方法交换后,此时调用customSystemFontOfSize方法,其实是调用了原来的systemFontOfSize方法
return [UIFont customSystemFontOfSize:size];
}
//自定义的交换方法
+ (UIFont *)customFontWithName:(NSString *)fontName size:(CGFloat)fontSize
{
CGFloat size = [UIFont transSizeWithFontSize:fontSize];
return [UIFont customFontWithName:fontName size:size];
}
///屏幕宽度大于320的,字体加10。(此处可根据不同的需求设置字体大小)
+ (CGFloat)transSizeWithFontSize:(CGFloat)fontSize
{
CGFloat size = fontSize;
CGFloat width = [UIFont getWidth];
if (width > 320) {
size += 10;
}
return size;
}
///获取竖屏状态下的屏幕宽度
+ (CGFloat)getWidth
{
for (UIScreen *windowsScenes in UIApplication.sharedApplication.connectedScenes) {
UIWindowScene * scenes = (UIWindowScene *)windowsScenes;
UIWindow *window = scenes.windows.firstObject;
if (scenes.interfaceOrientation == UIInterfaceOrientationPortrait) {
return window.frame.size.width;
}
return window.frame.size.height;
}
return 0;
}
@end
至此就实现了,动态改变字体大小的目的,那xib和storyboard拖的控件怎么修改呢?我们接着看
动态修改xib和storyboard控件的字体大小
xib和sb拖拽的控件,都会调用 initWithCoder方法,那么我们可以自定义一个方法,替换掉initWithCoder,并在此方法中修改控件的字体不就可以了吗。我们先用UILabel举例,先创建一个UILabel的分类,然后在+(void)load方法中进行initWithCoder方法的交换
#import "UILabel+Category.h"
#import "NSObject+Category.h"
@implementation UILabel (Category)
+ (void)load
{
[[self class] runtimeReplaceFunctionWithSelector:@selector(initWithCoder:) swizzleSelector:@selector(customInitWithCoder:) isClassMethod:NO];
}
- (instancetype)customInitWithCoder:(NSCoder *)coder
{
if ([self customInitWithCoder:coder]) {
///此时调用fontWithName:size:方法,实际上调用的是方法交换后的customFontWithName:size:
self.font = [UIFont fontWithName:self.font.familyName size:self.font.pointSize];
}
return self;
}
@end
此时我们就实现了,UILabel字体大小的动态修改,同理我们实现其它几个开发中常用的几个控件修改
UIButton的分类
#import "UIButton+Category.h"
#import "NSObject+Category.h"
@implementation UIButton (Category)
+ (void)load
{
[[self class] runtimeReplaceFunctionWithSelector:@selector(initWithCoder:) swizzleSelector:@selector(customInitWithCoder:) isClassMethod:NO];
}
- (instancetype)customInitWithCoder:(NSCoder *)coder
{
if ([self customInitWithCoder:coder]) {
if (self.titleLabel != nil) {
self.titleLabel.font = [UIFont fontWithName:self.titleLabel.font.familyName size:self.titleLabel.font.pointSize];
}
}
return self;
}
@end
UITextField的分类
iOS runtime之--动态添加属性和方法
一、runtime添加属性
//新建一个NSObject的category类,并添加一个customString属性
@interface NSObject (Category)
@property(nonatomic,copy)NSString *customString;
@end
//在.m文件中实现set、get方法,此时添加属性代码便完成了,就是如此简单
#import "NSObject+Category.h"
#import <objc/message.h>
- (void)setCustomString:(NSString *)customString {
objc_setAssociatedObject(self, &customStringKey, customString, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)customString {
return objc_getAssociatedObject(self, &customStringKey);
}
//测试一下,如果打印出1111,就代表添加属性成a国
- (void)viewDidLoad {
[super viewDidLoad];
///动态添加属性
NSObject *objct = [[NSObject alloc] init];
objct.customString = @"1111";
NSLog(@"%@",objct.customString);
}
动态添加属性,主要用到了两个runtime函数:
1.添加属性
此函数有四个参数。
第一个参数指给哪个对象添加属性,第二个参数指属性的key指针,第三个参数指属性的名字,第四个参数指引用类型和原子性。
其中着重讲一下第四个参数,此参数有五个值:
OBJC_ASSOCIATION_ASSIGN 代表生成一个弱类型属性,相当于@property(atomic,assign)
OBJC_ASSOCIATION_RETAIN_NONATOMIC相当于@property(nonatomic,strong)
OBJC_ASSOCIATION_COPY_NONATOMIC,相当于@property(nonatomic,copy)
OBJC_ASSOCIATION_RETAIN,相当于@property(atomic,strong)
OBJC_ASSOCIATION_COPY,
相当于@property(atomic,copy)
上面代码生成的是string对象,所以我们一般用OBJC_ASSOCIATION_COPY_NONATOMIC
二、runtime动态添加方法
///例如我们有一个people类,people类中没有任何属性和方法,//我们为之添加一个名为sing的方法
- (void)viewDidLoad {
[super viewDidLoad];
People *people = [[People alloc] init];
//添加方法
class_addMethod([People class], @selector(sing), class_getMethodImplementation([self class], @selector(peopleSing)), "v@:");
//people调用刚添加的方法
[people performSelector:@selector(sing)];
}
- (void)peopleSing
{
NSLog(@"在唱歌");
}
添加方法主要用到两个runtime函数
1.添加方法函数
此函数有四个参数
第一个参数代表为哪个类添加方法
第二个参数代表添加的方法名称
第三个参数已经实现的方法的imp指针
第一个参数为方法实现所在的类。
第二个参数为实现的方法的SEL
iOS- Dealloc流程解析 Dealloc 实现原理
当对象的引用计数为0时, 系统会调用对象的dealloc方法释放
- (void)dealloc {
_objc_rootDealloc(self);
}
在内部
void
_objc_rootDealloc(id obj)
{
assert(obj);
obj->rootDealloc();
}
继续调用了rootDealloc方法
显然调用顺序为:先调用当前类的dealloc,然后调用父类的dealloc,最后到了NSObject的dealloc.
inline void
objc_object::rootDealloc()
{
//判断对象是否采用了Tagged Pointer技术
if (isTaggedPointer()) return; // fixme necessary?
//判断是否能够进行快速释放
//这里使用了isa指针里的属性来进行判断.
if (fastpath(isa.nonpointer && //对象是否采用了优化的isa计数方式
!isa.weakly_referenced && //对象没有被弱引用
!isa.has_assoc && //对象没有关联对象
!isa.has_cxx_dtor && //对象没有自定义的C++析构函数
!isa.has_sidetable_rc //对象没有用到sideTable来做引用计数
))
{
//如果以上判断都符合条件,就会调用C函数 free 将对象释放
assert(!sidetable_present());
free(this);
}
else {
//如果以上判断没有通过,做下一步处理
object_dispose((id)this);
}
}
内部做了一些判断, 如果满足这五个条件,直接调用free函数,进行内存释放.
当一个最简单的类(没有任何成员变量,没有任何引用的类),这五个判断条件都是成立的,直接free.
id
object_dispose(id obj)
{
if (!obj) return nil;
objc_destructInstance(obj);
free(obj);
return nil;
}
调用objc_destructInstance函数来析构对象obj,再free(obj)释放内存.
objc_destructInstance内部函数会销毁C++析构函数以及移除关联对象的操作.
继续调用objc_object的clearDeallocating函数做下一步处理
objc_object::clearDeallocating()
{
if (slowpath(!isa.nonpointer)) {
// Slow path for raw pointer isa.
// 如果要释放的对象没有采用了优化过的isa引用计数
sidetable_clearDeallocating();
}
else if (slowpath(isa.weakly_referenced || isa.has_sidetable_rc)) {
// Slow path for non-pointer isa with weak refs and/or side table data.
// 如果要释放的对象采用了优化过的isa引用计数,并且有弱引用或者使用了sideTable的辅助引用计数
clearDeallocating_slow();
}
assert(!sidetable_present());
}
根据是否采用了优化过的isa做引用计数分为两种:
- 要释放的对象没有采用优化过的isa引用计数:
会调用sidetable_clearDeallocating() 函数做进一步处理
void
objc_object::sidetable_clearDeallocating()
{
// 在全局的SideTables中,以this指针(要释放的对象)为key,找到对应的SideTable
SideTable& table = SideTables()[this];
// clear any weak table items
// clear extra retain count and deallocating bit
// (fixme warn or abort if extra retain count == 0 ?)
table.lock();
//在散列表SideTable中找到对应的引用计数表RefcountMap,拿到要释放的对象的引用计数
RefcountMap::iterator it = table.refcnts.find(this);
if (it != table.refcnts.end()) {
//如果要释放的对象被弱引用了,通过weak_clear_no_lock函数将指向该对象的弱引用指针置为nil
if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
weak_clear_no_lock(&table.weak_table, (id)this);
}
//从引用计数表中擦除该对象的引用计数
table.refcnts.erase(it);
}
table.unlock();
}
- 如果该对象采用了优化过的isa引用计数
并且该对象有弱引用或者使用了sideTable的辅助引用计数,就会调用clearDeallocating_slow()函数做进一步处理.
NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
assert(isa.nonpointer && (isa.weakly_referenced || isa.has_sidetable_rc));
// 在全局的SideTables中,以this指针(要释放的对象)为key,找到对应的SideTable
SideTable& table = SideTables()[this];
table.lock();
if (isa.weakly_referenced) {
//要释放的对象被弱引用了,通过weak_clear_no_lock函数将指向该对象的弱引用指针置为nil
weak_clear_no_lock(&table.weak_table, (id)this);
}
//使用了sideTable的辅助引用计数,直接在SideTable中擦除该对象的引用计数
if (isa.has_sidetable_rc) {
table.refcnts.erase(this);
}
table.unlock();
}
以上两种情况都涉及weak_clear_no_lock函数, 它的作用就是将被弱引用对象的弱引用指针置为nil.
void
weak_clear_no_lock(weak_table_t *weak_table, id referent_id)
{
//获取被弱引用对象的地址
objc_object *referent = (objc_object *)referent_id;
// 根据对象地址找到被弱引用对象referent在weak_table中对应的weak_entry_t
weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
if (entry == nil) {
/// XXX shouldn't happen, but does with mismatched CF/objc
//printf("XXX no entry for clear deallocating %p\n", referent);
return;
}
// zero out references
weak_referrer_t *referrers;
size_t count;
// 找出弱引用该对象的所有weak指针地址数组
if (entry->out_of_line()) {
referrers = entry->referrers;
count = TABLE_SIZE(entry);
}
else {
referrers = entry->inline_referrers;
count = WEAK_INLINE_COUNT;
}
// 遍历取出每个weak指针的地址
for (size_t i = 0; i < count; ++i) {
objc_object **referrer = referrers[i];
if (referrer) {
// 如果weak指针确实弱引用了对象 referent,则将weak指针设置为nil
if (*referrer == referent) {
*referrer = nil;
}
// 如果所存储的weak指针没有弱引用对象 referent,这可能是由于runtime代码的逻辑错误引起的,报错
else if (*referrer) {
_objc_inform("__weak variable at %p holds %p instead of %p. "
"This is probably incorrect use of "
"objc_storeWeak() and objc_loadWeak(). "
"Break on objc_weak_error to debug.\n",
referrer, (void*)*referrer, (void*)referent);
objc_weak_error();
}
}
}
weak_entry_remove(weak_table, entry);
}
这里也表明了为什么被weak修饰的对象在释放时, 所有弱引用该对象的指针都被设置为nil.
dealloc整个方法释放流程如下图:
看流程图发现,如果五个条件不满足.内存无法进行快速释放.在上面中,我看到博客里关于 objc_destructInstance 这个方法只是概述而过,所以我找了相关资料来了解一下.
void *objc_destructInstance(id obj)
{
if (obj) {
Class isa_gen = _object_getClass(obj);
class_t *isa = newcls(isa_gen);
// Read all of the flags at once for performance.
bool cxx = hasCxxStructors(isa);
bool assoc = !UseGC && _class_instancesHaveAssociatedObjects(isa_gen);
// This order is important.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj);
if (!UseGC) objc_clear_deallocating(obj);
}
return obj;
}
总共干了三件事::
- 执行了object_cxxDestruct 函数
- 执行_object_remove_assocations,去除了关联对象.(这也是为什么category添加属性时,在释放时没有必要remove)
- 就是上面写的那个,清空引用计数表并清除弱引用表,将weak指针置为nil
object_cxxDestruct是由编译器生成,这个方法原本是为了++对象析构,ARC借用了这个方法插入代码实现了自动内存释放的工作.
这个释放.
现象:
- 当类拥有实例变量时,这个方法会出现,且父类的实例变量不会导致子类拥有这个方法.
- 出现这个方法和变量是否被赋值,赋值成什么没有关系.
所以, 我们可以认为这个方法就是用来释放该类中的属性的. weak修饰的属性应该不包含在内。
摘自链接:https://www.jianshu.com/p/b25f50d852f2
iOS- weak 原理
一、weak 基本用法
weak 是弱引用,用 weak 来修饰、描述所引用对象的计数器并不会增加,而且 weak 会在引用对象被释放的时候自动置为 nil,这也就避免了野指针访问坏内存而引起奔溃的情况,另外 weak 也可以解决循环引用。assign 可用来修饰基本数据类型,也可修饰 OC 的对象,但如果用 assign 修饰对象类型指向的是一个强指针,当指向的这个指针释放之后,它仍指向这块内存,必须要手动给置为 nil,否则会产生野指针,如果还通过此指针操作那块内存,会导致 EXC_BAD_ACCESS 错误,调用了已经被释放的内存空间;而 weak 只能用来修饰 OC 对象,而且相比 assign 比较安全,如果指向的对象消失了,那么它会自动置为 nil,不会导致野指针
二、weak 原理概括
Runtime 维护了一张 weak 表,用来存储某个对象的所有的 weak 指针。
weak 原理实现过程三步骤
初始化开始时,会调用 objc_initWeak 函数,初始化新的 weak 指针指向对象的地址。
然后 objc_initWeak 函数里面会调用 objc_storeWeak() 函数,objc_storeWeak() 函数的作用是用来更新指针的指向,创建弱引用表。
在最后会调用 clearDeallocating 函数。而clearDeallocating 函数首先根据对象的地址获取 weak 指针地址的数组,然后紧接着遍历这个数组,将其中的数组开始置为 nil,把这个 entry 从 weak 表中删除,最后一步清理对象的记录。
id objc_initWeak(id *location, id newObj) {
// 查看对象实例是否有效,无效对象直接导致指针释放
if (!newObj) {
*location = nil;
return nil;
}
// 这里传递了三个 Bool 数值
// 使用 template 进行常量参数传递是为了优化性能
return storeWeakfalse/*old*/, true/*new*/, true/*crash*/>
(location, (objc_object*)newObj);
}
通过上面代码可以看出,objc_initWeak()函数首先判断指针指向的类对象是否有效,无效,直接返回;否则通过 storeWeak() 被注册为一个指向 value 的 _weak 对象
objc_initWeak 函数里面会调用 objc_storeWeak() 函数,objc_storeWeak() 函数的作用是用来更新指针的指向,创建弱引用表。
答:
在 dealloc 中,调用了 _objc_rootDealloc 函数
在 _objc_rootDealloc 中,调用了 object_dispose 函数
调用 objc_destructInstance
最后调用 objc_clear_deallocating,详细过程如下:
a. 从 weak 表中获取废弃对象的地址为键值的记录
b. 将包含在记录中的所有附有 weak 修饰符变量的地址,赋值为 nil
c. 将 weak 表中该记录删除
d. 从引用计数表中删除废弃对象的地址为键值的记录
摘自链接:https://www.jianshu.com/p/713f7f19d07b
iOS- Copy和Strong修饰
情况一(@property (nonatomic,copy)NSString *str;)(@property (nonatomic,strong)NSString *str;)self. str = NSString(实例)
@interface ViewController ()
@property (nonatomic,copy)NSString *str;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSString *base_str = @"我是";//实例化分配堆内存
self.str = base_str;//copy对NSString只是指针拷贝(浅拷贝)
NSLog(@"str--%p+++%@",self.str,self.str);//0x1006a4020+++我是
NSLog(@"base_str--%p+++%@",base_str,base_str);//0x1006a4020+++我是
NSLog(@"分割线---------------------------------------------");
base_str = @"haha";//重新实例化重新分配堆内存(但是对原来的地址不影响)
NSLog(@"str--%p+++%@",self.str,self.str);//0x1006a4020+++我是
NSLog(@"base_str--%p+++%@",base_str,base_str);//0x1006a40a0+++haha
}
2021-03-22 16:22:42.509744+0800 IOS--多继承[36010:335669] str--0x1006a4020+++我是
2021-03-22 16:22:42.509955+0800 IOS--多继承[36010:335669] base_str--0x1006a4020+++我是
2021-03-22 16:22:42.510093+0800 IOS--多继承[36010:335669] 分割线---------------------------------------------
2021-03-22 16:22:42.510221+0800 IOS--多继承[36010:335669] str--0x1006a4020+++我是
2021-03-22 16:22:42.510330+0800 IOS--多继承[36010:335669] base_str--0x1006a40a0+++haha
情况二(@property (nonatomic,copy)NSString *str;)self. str = NSMutableString(实例)
@interface ViewController ()
@property (nonatomic,copy)NSString *str;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSMutableString *m_str = [NSMutableString stringWithString:@"nihao"];
self.str = m_str;//copy对NSMutableString生成了新的地址(深拷贝)
NSLog(@"str--%p+++%@",self.str,self.str);//0xbe2d07ae3dfa791b+++nihao
NSLog(@"m_str--%p+++%@",m_str,m_str);//0x600000558870+++nihao
NSLog(@"分割线---------------------------------------------");
[m_str appendFormat:@"修改后"];
NSLog(@"str--%p+++%@",self.str,self.str);//0xdb33f1772ec1e5d1+++nihao
NSLog(@"m_str--%p+++%@",m_str,m_str);//0x600000724db0+++nihao修改后
}
情况三(@property (nonatomic,strong)NSString *str;)self. str = NSMutableString(实例)
@interface ViewController ()
@property (nonatomic,strong)NSString *str;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSMutableString *m_str = [NSMutableString stringWithString:@"nihao"];
self.str = m_str;//strong对NSMutableString没有生成了新的地址(浅拷贝)
NSLog(@"str--%p+++%@",self.str,self.str);//0xbe2d07ae3dfa791b+++nihao
NSLog(@"m_str--%p+++%@",m_str,m_str);//0x600000558870+++nihao
NSLog(@"分割线---------------------------------------------");
[m_str appendFormat:@"修改后"];
NSLog(@"str--%p+++%@",self.str,self.str);//0xdb33f1772ec1e5d1+++nihao
NSLog(@"m_str--%p+++%@",m_str,m_str);//0x600000724db0+++nihao修改后
}
2021-03-22 16:39:20.728281+0800 IOS--多继承[36287:351536] str--0x60000235e3d0+++nihao
2021-03-22 16:39:20.728446+0800 IOS--多继承[36287:351536] m_str--0x60000235e3d0+++nihao
2021-03-22 16:39:20.728574+0800 IOS--多继承[36287:351536] 分割线---------------------------------------------
2021-03-22 16:39:20.728697+0800 IOS--多继承[36287:351536] str--0x60000235e3d0+++nihao修改后
2021-03-22 16:39:20.728811+0800 IOS--多继承[36287:351536] m_str--0x60000235e3d0+++nihao修改后
情况四(@property (nonatomic,strong)NSMutableString *m_str;)self.m_str = NSString
@interface ViewController ()
@property (nonatomic,strong)NSMutableString *m_str;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSString *str = @"nihao";
self.m_str = str;//strong对str只是引用计数+1(此时self.m_str还是不可变NSString)
NSLog(@"str--%p+++%@",str,str);
NSLog(@"m_str--%p+++%@",self.m_str,self.m_str);
NSLog(@"分割线---------------------------------------------");
str = @"修改后";
[self.m_str appendFormat:@"修改"];//(编译能通过,运行时候Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Attempt to mutate immutable object with appendFormat:'*** First throw call stack:)
//因为appendFormat是NSMutableString的方法
NSLog(@"str--%p+++%@",str,str);
NSLog(@"m_str--%p+++%@",self.m_str,self.m_str);
}
@interface ViewController ()
@property (nonatomic,copy)NSMutableString *m_str;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSMutableString *str = [NSMutableString stringWithString:@"nihao"];
self.m_str = str;//strong对str只是引用计数+1(此时self.m_str还是不可变NSString)
NSLog(@"str--%p+++%@",str,str);
NSLog(@"m_str--%p+++%@",self.m_str,self.m_str);
NSLog(@"分割线---------------------------------------------");
[self.m_str appendFormat:@"修改"];//(编译能通过,运行时候Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Attempt to mutate immutable object with appendFormat:'*** First throw call stack:)
//因为appendFormat是NSMutableString的方法
NSLog(@"str--%p+++%@",str,str);
NSLog(@"m_str--%p+++%@",self.m_str,self.m_str);
}
当使用 strong 修饰属性的时候,属性的setter方法会直接强引用该对象,这样,当原object对象的值发生改变时,新对象的属性也改变;
但是对于可变对象类型,如NSMutableString、NSMutableArray等则不可以使用copy修饰,因为Foundation框架提供的这些类都实现了NSCopying协议,使用copy方法返回的都是不可变对象,如果使用copy修饰符在对可变对象赋值时则会获取一个不可变对象,接下来如果对这个对象进行可变对象的操作则会产生异常,因为OC没有提供mutableCopy修饰符,对于可变对象使用strong修饰符即可。
总结:
Git 操作整理
git 使用
git clone-b分支名仓库地址
。本地创建公钥:ssh-keygen-t rsa-C"邮箱"并配置
克隆最新主分支项目代码: git clone地址
创建本地分支: git branch分支名
查看本地分支:git branch
查看远程分支: git branch-a
切换分支: git checkout分支名(一般修改未提交则无法切换,大小写问题经常会有,可强制切换 git checkout分支名-f非必须慎用)
将本地分支推送到远程分支:git push<远程仓库><本地分支>:<远程分支>
必备知识点
Remote:远程主仓库
Repository:本地仓库
Index:Git追踪树,暂存区
workspace:本地工作区(即你编辑器的代码)
一般操作流程:《工作区》-> git status
查看状态 -> git add.
将所有修改加入暂存区-> git commit-m"提交描述"
将代码提交到本地仓库-> git push
将本地仓库代码更新到远程仓库。
一、git remote
为远程仓库指定别名,以便于管理远程主机,默认只有一个时为origin。
1、查看主机名: git remote
。
2、查看主机名即网址: git remote-v
。
默认克隆远程仓库到本地时,远程主机为origin,如需指定别名可使用 git clone-o<别名><远程git地址>
。
3、查看主机的详细信息: git remote show<主机名>
。
4、添加远程主机: git remote add<主机名><网址>
。
5、删除远程主机: git remote rm<主机名>
。
6、修改远程主机的别名: git remote rename<原主机名><新主机名>
。
二、git fetch
将某个远程主机的更新,全部/分支 取回本地(此时之更新了Repository)它取回的代码对你本地的开发代码没有影响,如需彻底更新需合并或使用 git pull
。
远程主机的更新,全部取回本地:
git fetch<远程主机名>
将远程仓库特定分支更新到本地:
git fetch<远程主机名><分支名>
如果需要将更新拉取但本地工作代码需要合并到本地某一分支: git merge<被合并的远程分支>
,或者在此基础上创建出新分支并切换: git checkout-b<分支名><在此分支上创建>
。
三、git pull
拉取远程主机某分支的更新,再与本地的指定分支合并(相当与fetch加上了合并分支功能的操作)。
拉取远程某分支并与本地某一分支合并(没有则默认会创建):
git pull<远程主机名><远程分支名>:<本地分支名>
。如果远程分支是与当前所在分支合并,则冒号后面的部分可以省略:
git pull<远程主机名><远程分支名>
。如果当前分支与远程分支存在追踪关系,则可以省略远程分支名:
git pull<远程主机名>
。如果当前分支只有一个追踪分支,则远程主机名都可以省略:
git pull
。
四、git push
将本地分支的更新,推送到远程主机,其命令格式与 git pull
相似。
1、将本地分支推送到远程分支: git push<远程主机名><本地分支名>:<远程分支名>
。
2、如果省略远程分支名,则默认为将本地分支推送到与之关联的远程分支:(一般设置本地分支和与之关联的远程分支同名,防止混淆) git push<远程主机名><本地分支名>
。
如果对应的远程分支不存在,则会被创建(m默认与本地分支同名)。
3、如果省略本地分支名,则表示删除指定的远程分支,这等同于推送一个空的本地分支到对应远程分支: git push origin:<远程分支>
等同于 git push origin--delete<远程分支>
。
4、如果当前分支与远程分支之间存在追踪关系,则本地分支和远程分支都可以省略 git push origin
。
5、如果当前分支只有一个追踪分支,那么主机名也可以省略: git push
。
6、如果当前分支与多个主机存在追踪关系(使用场景相对来说较少),可以使用 -u
指定默认推送主机: git push-u origin<主机名>
,设置时候需推送便可以直接使用 git push
。
7、将本地的所有分支都推送到远程主机: git push--all origin
。
8、如果远程主机的版本比本地版本更新,推送时Git会报错,要求先在本地做 git pull
合并差异,然后再推送到远程主机。如果一定要推送,可以使用 --force
选项(谨慎使用,除非你非常确认): git push--force origin
。
注意:分支推送顺序的格式为<来源地>:<目的地>,所以 git pull
格式:<远程分支>:<本地分支>, git push
格式为:<本地分支>:<远程分支>。
五、分支操作
1、创建本地分支: git branch test
:(创建名为test的本地分支)。
2、切换分支: git checkout test
:(切换到test分支)。
3、创建并切换分支 git branch-b test
:(相当于以上两条命令的合并)。
4、查看本地分支: git branch
。
5、查看远程仓库所有分支: git branch-a
。
6、删除本地分支: git branch-d test
:(删除本地test分支)。
7、分支合并: git merge master
:(将master分支合并到当前分支)。
8、本地分支重命名: git branch-m oldName newName
。
9、远程分支重命名:
重命名远程分支对应的本地分支:
git branch-m oldName newName
;删除远程分支:
git push--deleteorigin oldName
;上传新命名的本地分支:
git push origin newName
;把修改后的本地分支与远程分支关联:
git branch--set-upstream-to origin/newName
10、分支关联:
查看当前的本地分支与远程分支的关联关系: git branch-vv
。
git branch--set-upstream-to=origin/feature/clear-server-eslint-error_180713
。查看本地当前分支与远程某一分支的差异: git diff origin/feature/reserve-3.4
。
查看本地特定分支与远程分支的差异: git diff master origin/feature/reserve-3.4
(查看本地master分支与远程feature/reserve-3.4分支的差异),如图:
六、修改撤销
1、 git checkout--<文件名>
:丢弃工作区的修改,就是让这个文件回到最近一次 git commit
或 git add
时的状态。
2、 git reset HEAD<文件名>
:把暂存区的修改撤销掉(unstage),重新放回工作区。
3、 git reset--hard commit_id
:git版本回退,回退到特定的commit_id版本。
流程: git log
查看提交历史,以便确定要回退到哪个版本(commit 之后的即为ID)。
4、 git reset--hard commit_id
:回退到commit_id版本。
5、 git reflog
查看命令历史,以便确定要回到未来的哪个版本。更新远程代码到本地:
git fetch origin master(分支)
。git pull// 将fetch下来的代码pull到本地
。git diff master origin/master// 查看本地分支代码和远程仓库的差异
。
6、拉取远程分支并创建本地分支:
git checkout-b本地分支名origin/远程分支名
:使用此方式会在本地新建分支,并自动切换到该本地分支;git fetch origin远程分支名:本地分支名
:使用此方式会在本地新建分支,但是不会自动切换到该本地分支,需要手动checkout。
七、配置
1、 git config-l
// 陈列出所有的git配置项。
2、 git config core.ignorecasefalse
//配置git不忽略大小写(默认忽略)参照(git 大小写)。
原贴链接:https://www.jianshu.com/p/80252c51a70f
iOS Metal语言规范浅谈
一.Metal简述
Metal着色器语言是用来编写3D图形渲染逻辑、并行Metal计算核心逻辑的一门编程语言,当你使用Metal框架来完成APP的实现时则需要使用Metal编程语言。
Metal语言使用Clang 和LLVM进行编译处理,编译器对于在GPU上的代码执行效率有更好的控制
Metal基于C++ 11.0语言设计的,在C++基础上多了一些扩展和限制,主要用来编写在GPU上执行的图像渲染逻辑代码以及通用并行计算逻辑代码
Metal 像素坐标系统:Metal中纹理 或者 帧缓存区attachment的像素使用的坐标系统的原点是左上角
1.1Metal 语⾔中不⽀持之处
Lambda 表达式;
递归函数调⽤
动态转换操作符
类型识别
对象创建new 和销毁delete 操作符;
操作符 noexcept
goto 跳转
变量存储修饰符register 和 thread_local;
虚函数修饰符;
派⽣类
异常处理
C++ 标准库在Metal 语⾔中也不可使⽤;
1.2Metal 语⾔中对于指针使⽤的限制
Metal图形和并⾏计算函数⽤到的⼊参数; 如果是指针必须使⽤地址空间修饰符(device,threadgroup,constant)
不⽀持函数指针;
函数名不能出现main
二.Metal的数据类型及语法
2.1 Metal 数据类型--标量数据类型
bool 布尔类型, true/false
char 有符号8位整数;
unsigned char /uchar ⽆符号8-bit 整数;
short 有符号16-bit整数;
unsigned short / ushort ⽆符号32-bit 整数;
half 16位bit 浮点数;
float 32bit 浮点数;
size_t 64 ⽆符号整数;
void 该类型表示⼀个空的值集合
说明:其中half 相当于OC中的float,float 相当于OC中的doublesize_t用来表示内存空间, 相当于 OC中 sizeof
示例:boola=true;charb=5;intd=15;//用于表示内存空间size_t c=1;ptrdiff_t f=2;
2.2Metal向量
向量支持如下类型:- booln、charn、shortn、intn、ucharn、ushortn、uintn、halfn、floatn,其中 n 表示向量的维度,最多不超过4维向量示例:
//直接赋值初始化
bool2 A={1,2}
;//通过内建函数float4初始化
float4 pos=float4(1.0,2.0,3.0,4.0);
//通过下标从向量中获取某个值
floatx=pos[0];floaty=pos[1];
//通过for循环对一个向量进行运算
float4 VB;
for(inti=0;i<4;i++){
VB[i]=pos[i]*2.0f;
}
说明:在OpenGL ES的GLSL语言中,例如2.0f,在着色器中书写时,是不能加f,写成2.0,而在Metal中则可以写成2.0f,其中f可以是大写,也可以是小写
向量的访问规则:
1.通过向量字母获取元素: 向量中的向量字母仅有2种,分别为xyzw、rgba
int4 test=int4(0,1,2,3);
inta=test.x; //获取的向量元素0
intb=test.y; //获取的向量元素1
intc=test.z; //获取的向量元素2
intd=test.w;//获取的向量元素3
inte=test.r; //获取的向量元素0
intf=test.g;//获取的向量元素1
intg=test.b; //获取的向量元素2
inth=test.a; //获取的向量元素3
2.多个分量同时访问
float4 c;
c.xyzw=float4(1.0f,2.0f,3.0f,4.0f);
c.z=1.0f
c.xy=float2(3.0f,4.0f);
c.xyz=float3(3.0f,4.0f,5.0f);
说明:赋值时分量不可重复,取值时分量可重复右边是取值 和 左边赋值都合法xyzw与rgba不能混合使用,GLSL中向量不能乱序访问,只是和Metal中的向量相似,并不是等价
2.3矩阵
矩阵支持如下类型- halfnxm、floatnxm,其中 nxm表示矩阵的行数和列数,最多4行4列,其中half、float相当于OC中的float、double- 普通的矩阵其本质就是一个数组
float4x4 m;
//将第二行的所有值都设置为2.0
m[1]=float4(2.0f);
//设置第一行/第一列为1.0f
m[0][0]=1.0f;
//设置第三行第四列的元素为3.0f
m[2][3]=3.0f;
float4 类型向量的构造方式
1个float构成,表示一行都是这个值
4个float构成
2个float2构成
1个float2+2个float构成(顺序可以任意组合)
1个float2+1个float
1个float4
eg:
//float4类型向量的所有可能构造方式//1个一维向量,表示一行都是xfloat4(floatx);
///4个一维向量 --> 4维向量float4(floatx,floaty,floatz,floatw);
//2个二维向量 --> 4维向量float4(float2 a,float2 b);
//1个二维向量+2个一维向量 --> 4维向量float4(float2 a,float b,float c);
float4(floata,float2 b,floatc);float4(floata,floatb,float2 c);
//1个三维向量+1个一维向量 --> 4维向量float4(float3 a,floatb);float4(floata,float3 b);
//1个四维向量 --> 4维向量float4(float4 x);
float3 类型向量的构造方式
1个float构成,表示一行都是这个值
3个float
1个float+1个float2(顺序可以任意组合)
1个float2
eg:
//float3类型向量的所有可能的构造的方式
//1个一维向量float3(floatx);
//3个一维向量float3(floatx,floaty,floatz);
//1个一维向量 + 1个二维向量float3(floata,float2 b);
/1个二维向量 + 1个一维向量float3(float2 a,floatb);
//1个三维向量float3(float3 x);
float2 类型向量的构造方式
1个float构成,表示一行都是这个值
2个float
1个float2
eg:
//float2类型向量的所有可能的构造方式
//1个一维向量float2(floatx);
//2个一维向量float2(floatx,floaty);
//1个二维向量float2(float2 x);
三,Metal的其他类型
1.纹理
纹理类型
纹理类型是一个句柄,指向一维/二维/三维纹理数据,而纹理数据对应一个纹理的某个level的mipmap的全部或者一部分
纹理的访问权限
在一个函数中描述纹理对象的类型
access枚举值由Metal定义,定义了纹理的访问权利enum class access {sample, read, write};,有以下3种访问权利,当没写access时,默认的access 就是sample
sample: 纹理对象可以被采样(即使用采样器去纹理中读取数据,相当于OpenGL ES的GLSL中sampler2D),采样一维这时使用 或者 不使用都可以从纹理中读取数据(即可读可写可采样)
read:不使用采样器,一个图形渲染函数或者一个并行计算函数可以读取纹理对象(即仅可读)
write:一个图形渲染函数 或者 一个并行计算可以向纹理对象写入数据(即可读可写)
定义纹理类型
描述一个纹理对象/类型,有以下三种方式,分别对应一维/二维/三维,
其中T代表泛型,设定了从纹理中读取数据 或是 写入时的颜色类型,T可以是half、float、short、int等
access表示纹理访问权限,当access没写时,默认是sample
texture1d<T, access a = access::sample>
texture2d<T, access a = access::sample>
texture3d<T, access a = access::sample>
eg:
//类型 变量 修饰符
/*
类型
- texture2d<float>,读取的数据类型是float,没写access,默认是sample
- texture2d<float,access::read>,读取的数据类型是float,读取的方式是read
- texture2d<float,access::write>,读取的数据类型是float,读取的方式是write
变量名
- imgA
- imgB
- imgC
修饰符
- [[texture(0)]] 对应纹理0
- [[texture(1)]] 对应纹理1
- [[texture(2)]] 对应纹理2
*/函数举例
void foo (texture2d<float> imgA[[texture(0)]],
texture2d<float,access::read> imgB[[texture(1)]],
texture2d<float,access::write> imgC[[texture(2)]])
{
//...
}
2.采样器
采样器类型决定了如何对一个纹理进行采样操作,在Metal框架中有一个对应着色器语言的采样器的对象MTLSamplerState,这个对象作为图形渲染着色器函数参数或是并行计算函数的参数传递,有以下几种状态:
coord:从纹理中采样时,纹理坐标是否需要归一化
enum class coord { normalized, pixel };
filter:纹理采样过滤方式,放大/缩小过滤方式
enum class filter { nearest, linear };
min_filter:设置纹理采样的缩小过滤方式
enum class min_filter { nearest, linear };
mag_filter:设置纹理采样的放大过滤方式
enum class mag_filter { nearest, linear };
s_address、t_address、r_address:设置纹理s、t、r坐标(对应纹理坐标的x、y、z)的寻址方式
s坐标:enum class s_address { clamp_to_zero, clamp_to_edge, repeat, mirrored_repeat };
t坐标:enum class t_address { clamp_to_zero, clamp_to_edge, repeat, mirrored_repeat };
r坐标:enum class r_address { clamp_to_zero, clamp_to_edge, repeat, mirrored_repeat };
address:设置所有纹理坐标的寻址方式
enum class address { clamp_to_zero, clamp_to_edge, repeat, mirrored_repeat };
mip_filter:设置纹理采样的mipMap过滤模式, 如果是none,那么只有一层纹理生效;
enum class mip_filter { none, nearest, linear };
作者:枫紫_6174
链接:https://www.jianshu.com/p/17baccd48e77
Xcode11,Transporter上传卡在——正在验证 APP - 正在通过App Store进行认证
1.当卡死在 “Authenticating with the iTunes store”
解决办法:
关闭上传,并打开命令行,依次调用这三行代码:
cd ~
mv .itmstransporter/ .old_itmstransporter/
"/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/itms/bin/iTMSTransporter"
`</pre>
先说结论,此方法有效,但是对于Xcode11来说Application Loader已经移除了,那么路径就要改变到Transporter下,所以需要修改最后一个命令。为什么有效呢,因为本质上iTMSTransporter是所有上传工具真正使用的可执行文件。所以Transporter下也会发现这个文件。
<span style="font-weight: bold; font-size: medium;">1.首先找到文件位置,反键显示包内容。</span>
![](https://upload-images.jianshu.io/upload_images/5276080-dd51fa3a174b994a.png?imageMogr2/auto-orient/strip|imageView2/2/w/828/format/webp)
<span style="font-weight: bold; font-size: medium;">2.将iTMSTransporter的路径找到</span>
![](https://upload-images.jianshu.io/upload_images/5276080-f664e303dd5b0547.png?imageMogr2/auto-orient/strip|imageView2/2/w/1033/format/webp)
<span style="font-weight: bold; font-size: medium;">3.执行以下命令</span>
<pre>`cd ~
mv .itmstransporter/ .old_itmstransporter/
"/Applications/Transporter.app/Contents/itms/bin/iTMSTransporter"`</pre>
有两个点可能会出问题
<span style="color: rgb(77, 128, 191);">3.1 rename .itmstransporter/ to .old_itmstransporter/.itmstransporter/: Directory not empty</span>
如果第二句命令报以上错误,输入以下命令
<pre>`mv .old_itmstransporter/ .itmstransporter/
mv .itmstransporter/ .old_itmstransporter/
3.2 no such file or directory: xxxxxxxx
如果第三句命令报以上错误,是因为直接复制我的路径,但是你的应用路径跟我的不一致,自己将iTMSTransporter的路径找到并拼接好。
接下来会出现[2020-01-15 18:08:13 CST] <main> INFO: Configuring logging…,然后就开始无尽的等待,如果长时间没有进展,建议切换4G网络开热点给电脑使用,说不定有奇效。
最后指令执行完会出现[2020-01-15 18:10:07 CST] <main> DBG-X: Returning 0
对于我来说,之后再去用Transporter上传,第一步正在通过App Store进行认证很快就过去了,然后在App Store验证时卡住了几分钟,接着出现了将数据发送到App Store时出错。
然后我看到了稍后重新启动决定多等待以下,结果过了大概3分钟,就开始上传了。
然后瞬间就上传成功了。至此我折腾了一个下午的上传IPA,终于结束了。
重大更新
如果一直命令一直卡着,也无法上传成功,可以试试下面的办法。
上传卡住的原因:
Transporter安装上第一次打开后,会在硬盘目录:/用户/你的电脑登录账号名/资源库/Caches/com.apple.amp.itmstransporter/目录下下载一些缓存文件,这些缓存文件没有下载完,或者下载失败没下载完时,使用Transporter去提交应用这个页面就会卡住或者这个页面很慢。
那么一直更新不成功的话,可以下载这个文件夹直接覆盖自己的原有com.apple.amp.itmstransporter文件夹,如果原本没有也直接复制进去相当于创建了。
步骤如下:
https://download.csdn.net/download/Walter_White/12207626
1.下载链接里的文件,把解压后的"com.apple.amp.itmstransporter"目录放到"/用户/你的电脑登录账号名/资源库/Caches/"目录下,覆盖你原有的"com.apple.amp.itmstransporter"目录。
2.将新的"com.apple.amp.itmstransporter"目录下/obr/2.0.0/目录下的repository.xml文件中的所有"Simpsons"修改为你自己电脑的登录账号名,否则Transporter执行时会在错误的路径下找资源文件。
3.再次尝试Transporter上传。
4.如果时间App Store认证时间超过两分钟,建议手机开4g热点,电脑连接后再上传试试。
转自:https://www.jianshu.com/p/c0d85c003b3e
收起阅读 »【iOS】一个简单的人脸跟踪Demo
1、
sessionView - 相机画面的容器View
self.detector - 脸部特征识别器
- (void)viewDidLoad {
[super viewDidLoad];
self.sessionView = [[UIView alloc] initWithFrame:self.view.bounds];
[self.view addSubview:self.sessionView];
self.faceView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"a"]];
self.faceView.frame = CGRectZero;
[self.view addSubview:self.faceView];
self.leftEyeView = [[UIView alloc] init];
self.leftEyeView.alpha = 0.4;
self.leftEyeView.backgroundColor = [UIColor greenColor];
[self.view addSubview:self.leftEyeView];
self.rightEyeView = [[UIView alloc] init];
self.rightEyeView.alpha = 0.4;
self.rightEyeView.backgroundColor = [UIColor yellowColor];
[self.view addSubview:self.rightEyeView];
self.mouthView = [[UIView alloc] init];
self.mouthView.alpha = 0.4;
self.mouthView.backgroundColor = [UIColor redColor];
[self.view addSubview:self.mouthView];
self.context = [CIContext context];
self.detector = [CIDetector detectorOfType:CIDetectorTypeFace context:self.context options:@{CIDetectorAccuracy:CIDetectorAccuracyHigh}];
}
2、点击屏幕任意地方打开相机
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 避免重复打开,首先关闭原先的session
[self.session stopRunning];
self.session = [[AVCaptureSession alloc] init];
// 移除原有的相机画面Layer
[self.layer removeFromSuperlayer];
NSError *error;
// Device
NSArray *devices = [AVCaptureDevice devices];
NSLog(@"devices = %@", devices);
AVCaptureDevice *defaultDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
// Input
AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:defaultDevice error:&error];
[self.session addInput:input];
// Output
AVCaptureVideoDataOutput *output = [[AVCaptureVideoDataOutput alloc] init];
[output setSampleBufferDelegate:(id)self queue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)];
[self.session addOutput:output];
// 开始捕获相机画面
[self.session startRunning];
// 将相机画面添加到容器View中
self.layer = [AVCaptureVideoPreviewLayer layerWithSession:self.session];
self.layer.frame = self.view.bounds;
[self.sessionView.layer addSublayer:self.layer];
}
3、脸部特征跟踪
// AVCaptureAudioDataOutputSampleBufferDelegate
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
// printf("%s\n", __func__);
// 1、获取当前帧图像
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
CIImage *image = [[CIImage alloc] initWithCVImageBuffer:imageBuffer];
CGFloat imageW = image.extent.size.width;
CGFloat imageH = image.extent.size.height;
2、对图像进行脸部特征识别
CIFeature *feature = [[self.detector featuresInImage:image] lastObject];
if (feature) {
if (self.leftEyeView.frame.size.width == 0) {
self.leftEyeView.frame = CGRectMake(0, 0, 20, 20);
}
if (self.rightEyeView.frame.size.width == 0) {
self.rightEyeView.frame = CGRectMake(0, 0, 20, 20);
}
if (self.mouthView.frame.size.width == 0) {
self.mouthView.frame = CGRectMake(0, 0, 20, 20);
}
NSLog(@"find");
CIFaceFeature *face = (CIFaceFeature *)feature;
dispatch_async(dispatch_get_main_queue(), ^{
self.faceView.frame = CGRectMake(face.bounds.origin.y / imageW * self.sessionView.frame.size.height,
face.bounds.origin.x / imageH * self.sessionView.frame.size.width,
face.bounds.size.width / imageH * self.sessionView.frame.size.width,
face.bounds.size.height / imageW * self.sessionView.frame.size.height);
self.leftEyeView.center = CGPointMake(face.leftEyePosition.y / imageW * self.sessionView.frame.size.height,
face.leftEyePosition.x / imageH * self.sessionView.frame.size.width);
self.rightEyeView.center = CGPointMake(face.rightEyePosition.y / imageW * self.sessionView.frame.size.height,
face.rightEyePosition.x / imageH * self.sessionView.frame.size.width);
self.mouthView.center = CGPointMake(face.mouthPosition.y / imageW * self.sessionView.frame.size.height,
face.mouthPosition.x / imageH * self.sessionView.frame.size.width);
});
}
}
大功告成
手机记得横过来,home键在右边
Demo地址:https://github.com/MagicBlind/Face-Detector
转自:https://www.jianshu.com/p/db37d32e895e
收起阅读 »iOS性能优化 — 三、安装包瘦身
瘦身指导原则
总体指导原则为:压缩资源、删除无用/重复资源、删除无用代码、通过编译选项进行优化。
常规瘦身方案
压缩资源
项目中资源包括图片、字符串、音视频等资源。由于项目中图片比较多,所以资源压缩一般会从图片入手。在把图片加入到项目中时候需要采用tinypng或者ImageOptim对图片进行压缩;另外,可以通知设计,对切图进行压缩处理再上传;不需要内嵌到项目中的图片可以改为动态下载。
png,jpg,gif可以替换成webp
动画图片可替换为lotties、APNG
小图或表情图可替换为iconFont
大图可替换为svg
删除无用/重复资源
删除无用的资源。项目中主要以删除图片为主:
图片用2x和3x图就可以,不要用1x图。
可以用LSUnusedResources搜索出未使用的图片然后删除之。注意:该软件搜索出来的图片有可能项目中还在用,删除之前需要在工程中先搜索下图片是否有使用再确认是否可以删除。
删除无用代码
删除无用类和库:可以用WBBladesForMac来分析,注意:通过字符串调用的类也会检测为无用类。
非常规瘦身方案
1、Strip :去除不必要的符号信息。
-Strip Linked Product 和 Strip Swift Symbols 设置为 YES,Deployment Postprocessing 设置为 NO,发布代码的时候也需要勾选 Strip Swift Symbols。
Strip Debug Symbols During Copy 和 Symbols Hidden by Default 在release下设为YES
Dead Code Stripping 设置为 YES
对于动态库,可用strip -x [动态库路径] 去除不必要的符号信息
2、Make Strings Read-Only设为YES。
3、Link-Time Optimization(LTO)release下设为 Incremental。WWDC2016介绍编译时会移除没有被调用的方法和代码,优化程序运行效率。
4、开启BitCode
5、去除异常支持。不能使用@try @catch,包只缩小0.1M,效果不显著。
Enable C++ Exceptions和Enable Objective-C Exceptions设为NO,Other C Flags添加-fno-exceptions
6、不生成debug symbols:不能生成dSYM,效果非常显著。
Generate debug symbols选项 release 设置为NO
脑图借鉴
转自:https://www.jianshu.com/p/369c909c1067
收起阅读 »iOS内存管理-深入解析自动释放池
主要内容:
一、Autorelease简介
iOS开发中的Autorelease机制是为了延时释放对象。自动释放的概念看上去很像ARC,但实际上这更类似于C语言中自动变量的特性。
自动变量:在超出变量作用域后将被废弃;
自动释放池:在超出释放池生命周期后,向其管理的对象实例的发送release
消息。
1.1 MRC下使用自动释放池
NSAutoreleasePool
对象,其生命周期就相当于C语言变量的作用域。对于所有调用过autorelease
方法的对象,在废弃NSAutoreleasePool
对象时,都将调用release
实例方法。用源代码表示如下://MRC环境下的测试:
//第一步:生成并持有释放池NSAutoreleasePool对象;
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
//第二步:调用对象的autorelease实例方法;
id obj = [[NSObject alloc] init];
[obj autorelease];
//第三步:废弃NSAutoreleasePool对象;
[pool drain]; //向pool管理的所有对象发送消息,相当于[obj release]
//obi已经释放,再次调用会崩溃(Thread 1: EXC_BAD_ACCESS (code=EXC_I386_GPFLT))
NSLog(@"打印obj:%@", obj);
理解NSAutoreleasePool
对象的生命周期,如下图所示:
1.2 ARC下使用自动释放池
NSAutoreleasePool
类也不能调用autorelease
方法,代替它们实现对象自动释放的是@autoreleasepool
块和__autoreleasing
修饰符。比较两种环境下的代码差异如下图:@autoreleasepool
块替换了NSAutoreleasePoool
类对象的生成、持有及废弃这一过程。而附有__autoreleasing
修饰符的变量替代了autorelease
方法,将对象注册到了Autoreleasepool
;由于ARC的优化,__autorelease
是可以被省略的,所以简化后的ARC代码如下://ARC环境下的测试:
@autoreleasepool {
id obj = [[NSObject alloc] init];
NSLog(@"打印obj:%@", obj);
}
显式使用__autoreleasing
修饰符的情况非常少见,这是因为ARC的很多情况下,即使是不显式的使用__autoreleasing
,也能实现对象被注册到释放池中。主要包括以下几种情况:
alloc/new/copy/mutableCopy
开始,如果不是则自动将返回对象注册到Autoreleasepool
;__weak
修饰符的变量时,实际上必定要访问注册到Autoreleasepool
的对象,即会自动加入Autoreleasepool
;__autoreleasing
修饰符,加入Autoreleasepool
注意:如果编译器版本为LLVM.3.0以上,即使ARC无效@autoreleasepool
块也能够使用;如下源码所示:
//MRC环境下的测试:
@autoreleasepool{
id obj = [[NSObject alloc] init];
[obj autorelease];
}
二、AutoRelease原理
2.1 使用@autoreleasepool{}
我们在main
函数中写入自动释放池相关的测试代码如下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"Hello, World!");
}
return 0;
}
为了探究释放池的底层实现,我们在终端使用clang -rewrite-objc + 文件名
命令将上述OC代码转化为C++源码:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */
{
__AtAutoreleasePool __autoreleasepool;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_3f_crl5bnj956d806cp7d3ctqhm0000gn_T_main_d37e0d_mi_0);
}//大括号对应释放池的作用域
return 0;
}
在经过编译器clang
命令转化后,我们看到的所谓的@autoreleasePool
块,其实对应着__AtAutoreleasePool
的结构体。
2.2 分析结构体__AtAutoreleasePool的具体实现
在源码中找到__AtAutoreleasePool
结构体的实现代码,具体如下:
extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);
extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
__AtAutoreleasePool
结构体包含了:构造函数、析构函数和一个边界对象;
构造函数内部调用:objc_autoreleasePoolPush()
方法,返回边界对象atautoreleasepoolobj
析构函数内部调用:objc_autoreleasePoolPop()
方法,传入边界对象atautoreleasepoolobj
分析main
函数中__autoreleasepool
结构体实例的生命周期是这样的:__autoreleasepool
是一个自动变量,其构造函数是在程序执行到声明这个对象的位置时调用的,而其析构函数则是在程序执行到离开这个对象的作用域时调用。所以,我们可以将上面main
函数的代码简化如下:
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ {
void *atautoreleasepoolobj = objc_autoreleasePoolPush();
NSLog((NSString *)&__NSConstantStringImpl__var_folders_kb_06b822gn59df4d1zt99361xw0000gn_T_main_d39a79_mi_0);
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
return 0;
}
2.3 objc_autoreleasePoolPush与objc_autoreleasePoolPop
进一步观察自动释放池构造函数与析构函数的实现,其实它们都只是对AutoreleasePoolPage
对应静态方法push
和pop
的封装
void *objc_autoreleasePoolPush(void) {
return AutoreleasePoolPage::push();
}
void objc_autoreleasePoolPop(void *ctxt) {
AutoreleasePoolPage::pop(ctxt);
}
2.4 理解AutoreleasePoolPage
AutoreleasePoolPage
是一个C++中的类,打开Runtime
的源码工程,在NSObject.mm
文件中可以找到它的定义,摘取其中的关键代码如下://大致在641行代码开始
class AutoreleasePoolPage {
# define EMPTY_POOL_PLACEHOLDER ((id*)1) //空池占位
# define POOL_BOUNDARY nil //边界对象(即哨兵对象)
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
static uint8_t const SCRIBBLE = 0xA3; // 0xA3A3A3A3 after releasing
static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
PAGE_MAX_SIZE; // must be multiple of vm page size
#else
PAGE_MAX_SIZE; // size and alignment, power of 2
#endif
static size_t const COUNT = SIZE / sizeof(id);
magic_t const magic; //校验AutoreleasePagePoolPage结构是否完整
id *next; //指向新加入的autorelease对象的下一个位置,初始化时指向begin()
pthread_t const thread; //当前所在线程,AutoreleasePool是和线程一一对应的
AutoreleasePoolPage * const parent; //指向父节点page,第一个结点的parent值为nil
AutoreleasePoolPage *child; //指向子节点page,最后一个结点的child值为nil
uint32_t const depth; //链表深度,节点个数
uint32_t hiwat; //数据容纳的一个上限
//......
};
其实,每个自动释放池都是是由若干个AutoreleasePoolPage
组成的双向链表结构,如下图所示:
AutoreleasePoolPage
中拥有parent
和child
指针,分别指向上一个和下一个page
;当前一个page
的空间被占满(每个AutorelePoolPage
的大小为4096字节)时,就会新建一个AutorelePoolPage
对象并连接到链表中,后来的 Autorelease对象也会添加到新的page
中;
另外,当next== begin()
时,表示AutoreleasePoolPage
为空;当next == end()
,表示AutoreleasePoolPage
已满。
2.5 理解哨兵对象/边界对象(POOL_BOUNDARY)的作用
在AutoreleasePoolPage
的源码中,我们很容易找到边界对象(哨兵对象)的定义:
iOS性能优化 — 四、内存泄露检测
上篇文章为大家讲解了安装包瘦身,这篇文章继续为大家讲解下内存泄露检测。
造成内存泄漏原因
常见循环引用及解决方案
怎么检测循环引用
造成内存泄漏原因
在用C/C++时,创建对象后未销毁,比如调用malloc后不free、调用new后不delete;
调用CoreFoundation里面的C方法后创建对对象后不释放。比如调用CGImageCreate不调用CGImageRelease;
循环引用。当对象A和对象B互相持有的时候,就会产生循环引用。常见产生循环引用的场景有在VC的cellForRowAtIndexPath方法中cell block引用self。
常见循环引用及解决方案
1) 在VC的cellForRowAtIndexPath方法中cell的block直接引用self或者直接以_形式引用属性造成循环引用。
cell.clickBlock = ^{
self.name = @"akon";
};
cell.clickBlock = ^{
_name = @"akon";
};
解决方案:把self改成weakSelf;
__weak typeof(self)weakSelf = self;
cell.clickBlock = ^{
weakSelf.name = @"akon";
};
2)在cell的block中直接引用VC的成员变量造成循环引用。
//假设 _age为VC的成员变量
@interface TestVC(){
int _age;
}
cell.clickBlock = ^{
_age = 18;
};
解决方案有两种:
用weak-strong dance
__weak typeof(self)weakSelf = self;
cell.clickBlock = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
strongSelf->age = 18;
};
把成员变量改成属性
//假设 _age为VC的成员变量
@interface TestVC()
@property(nonatomic, assign)int age;
@end
__weak typeof(self)weakSelf = self;
cell.clickBlock = ^{
weakSelf.age = 18;
};
3)delegate属性声明为strong,造成循环引用。
@interface TestView : UIView
@property(nonatomic, strong)id<TestViewDelegate> delegate;
@end
@interface TestVC()<TestViewDelegate>
@property (nonatomic, strong)TestView* testView;
@end
testView.delegate = self; //造成循环引用
解决方案:delegate声明为weak
@interface TestView : UIView
@property(nonatomic, weak)id<TestViewDelegate> delegate;
@end
4)在block里面调用super,造成循环引用。
cell.clickBlock = ^{
[super goback]; //造成循环应用
};
解决方案,封装goback调用
__weak typeof(self)weakSelf = self;
cell.clickBlock = ^{
[weakSelf _callSuperBack];
};
- (void) _callSuperBack{
[self goback];
}
5)block声明为strong
解决方案:声明为copy
6)NSTimer使用后不invalidate造成循环引用。
解决方案:
NSTimer用完后invalidate;
NSTimer分类封装
* (NSTimer *)ak_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
block:(void(^)(void))block
repeats:(BOOL)repeats{
return [self scheduledTimerWithTimeInterval:interval
target:self
selector:@selector(ak_blockInvoke:)
userInfo:[block copy]
repeats:repeats];
}
* (void)ak_blockInvoke:(NSTimer*)timer{
void (^block)(void) = timer.userInfo;
if (block) {
block();
}
}
怎么检测循环引用
静态代码分析。 通过Xcode->Product->Anaylze分析结果来处理;
动态分析。用MLeaksFinder或者Instrument进行检测。
转自:https://www.jianshu.com/p/f06f14800cf7
收起阅读 »Xcode12适配The linked library is missing one or more architectures required by this target问题
问题
升级到Xcode12后,运行Release模式后,会提示以下信息:
The linked library 'xxxx.a/Framework' is missing one or more architectures required by this target: armv7.
又或者
xxx/Pods/Target Support Files/Pods-xxx/Pods-xxx-frameworks.sh: line 128: ARCHS[@]: unbound variable
Command PhaseScriptExecution failed with a nonzero exit code
以上涉及架构问题
解决方案
在Target-Build Settings-Excluded Architectures中添加以下代码
EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64=arm64 arm64e armv7 armv7s armv6 armv8 EXCLUDED_ARCHS=$(inherited) $(EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_$(EFFECTIVE_PLATFORM_SUFFIX)__NATIVE_ARCH_64_BIT_$(NATIVE_ARCH_64_BIT))
转自:https://www.jianshu.com/p/81741aed39f7
收起阅读 »
iOS 使用NSSetUncaughtExceptionHandler收集Crash
在iOS程序崩溃时,一般我们是用Bugtags、Bugly、友盟等第三方收集崩溃,其实官方提供的NSUncaughtExceptionHandler来收集crash信息。实现方式如下:
自定义一个UncaughtExceptionHandler类,在.h中:
@interface CustomUncaughtExceptionHandler : NSObject
+ (void)setDefaultHandler;
+ (NSUncaughtExceptionHandler *)getHandler;
@end
复制代码
在.m中实现:
#import "CustomUncaughtExceptionHandler.h"
// 沙盒的地址
NSString * applicationDocumentsDirectory() {
return [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
}
// 崩溃时的回调函数
void UncaughtExceptionHandler(NSException * exception) {
NSArray * arr = [exception callStackSymbols];
NSString * reason = [exception reason]; // // 崩溃的原因 可以有崩溃的原因(数组越界,字典nil,调用未知方法...) 崩溃的控制器以及方法
NSString * name = [exception name];
NSString * url = [NSString stringWithFormat:@"crash报告\nname:%@\nreason:\n%@\ncallStackSymbols:\n%@",name,reason,[arr componentsJoinedByString:@"\n"]];
NSString * path = [applicationDocumentsDirectory() stringByAppendingPathComponent:@"crash.txt"];
// 将一个txt文件写入沙盒
[url writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:nil];
}
@implementation CustomUncaughtExceptionHandler
+ (void)setDefaultHandler {
NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);
}
+ (NSUncaughtExceptionHandler *)getHandler {
return NSGetUncaughtExceptionHandler();
}
@end
复制代码
这样我们就实现好了一个自定义UncaughtExceptionHandler类,接下来只需要在合适的地方获取crash文件以及传到服务器上去即可,如下所示:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
//崩溃日志
[CustomUncaughtExceptionHandler setDefaultHandler];
//获取崩溃日志,然后发送
NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
NSString *dataPath = [path stringByAppendingPathComponent:@"crash.txt"];
NSData *data = [NSData dataWithContentsOfFile:dataPath];
if (data != nil) {
//发送崩溃日志
NSLog(@"crash了:%@",data);
}
}
链接:https://juejin.cn/post/6953142642746064910
收起阅读 »
怎么获取到环信老版本的SDK和Demo
来到环信官网的下载页面:下载-即时通讯云-环信
找到想要下载的sdk,以iOS端为例,右键“SDK+Demo源码”,拷贝链接,然后修改链接里的版本号即可
例如:https://download-sdk.oss-cn-beijing.aliyuncs.com/downloads/iOS_IM_SDK_V3.7.4.zip
收起阅读 »
(IM)iOS端离线推送收不到怎么办?
离线推送收不到,按照下面步骤一步一步进行排查:
0、如果你的app之前可以收到离线推送,突然收不到了,那么先移步苹果开发者中心查看推送证书是否过期。如果过期了,需要重新制作证书,然后到环信管理后台(Console)将旧的删掉再上传新的。过期的一般会被封禁,需要联系环信进行解封操作。
1、首先已经按照环信的文档集成了离线推送:APNs离线推送
2、如果是iOS13及以上的系统,那么需要将IM SDK更新到3.6.4或以上版本。
如果更新后还不行那么退出登录、重启app、再登录试下。
初始化sdk成功之后打印版本号:
NSString *ver = [EMClient sharedClient].version;
3、测试APNs推送的时候,接收方的APP需要是杀死状态,需要用户长连接断开才会发APNs推送;
**所以直接上划杀死APP测试。**
4、要确保导出p12时使用的Mac和创建CertificateSigningRequest.certSigningRequest文件的Mac是同一台;导出证书的时候要直接点击导出,不要点击秘钥的内容导出;确认 APP ID 是否带有推送功能;
5、环信管理后台(Console)上传证书时填写的Bundle ID须与工程中的Bundle ID、推送证书的 APP ID 相同;选择的证书类型须与推送证书的环境一致;导出.p12文件需要设置密码,并在上传管理后台时传入;
6、工程中初始化SDK那里填的证书名与环信管理后台上传的证书名称必须是相同的;
7、测试环境测试,需要使用development环境的推送证书,Xcode直接真机运行;
正式环境测试,需要使用production环境的推送证书,而且要打包,打包时选择Ad Hoc,导出IPA安装到手机上。
8、APP杀死后可调用“获取单个用户”的rest接口,确认证书名称是否有绑定(正常情况下,登录成功后会绑定上推送证书,绑定后会显示推送证书名称);还需要确认绑定的证书名称和管理后台上传的证书名称是否一致。
接口文档:获取单个用户
如果没绑定上,那么退出登录、重启app、重新登录再试下。
如果证书名称不一致,改正过来后重新登录试下。
9、如果以上都确认无误,可以联系环信排查。需提供以下信息(请勿遗漏,以免反复询问耽误时间):
appkey、devicetoken、bundle id、证书的.p12文件、证书名称、证书密码、收不到推送的环信id、测试的环境(development or production)、消息id、消息的内容和发送时间
消息id要在消息发送成功后获取,如图:
收起阅读 »
iOS 唤起APP之Universal Link(通用链接)
iOS 9之前,一直使用的是URL Schemes技术来从外部对App进行跳转,但是iOS系统中进行URL Schemes跳转的时候如果没有安装App,会提示Cannot open Page的提示,而且当注册有多个scheme相同的时候,目前没有办法区分,但是从iOS 9起可以使用Universal Links技术进行跳转页面,这是一种体验更加完美的解决方案
什么是Universal Link(通用链接)
Universal Link是Apple在iOS 9推出的一种能够方便的通过传统HTTPS链接来启动APP的功能。如果你的应用支持Universal Link,当用户点击一个链接时可以跳转到你的网站并获得无缝重定向到对应的APP,且不需要通过Safari浏览器。如果你的应用不支持的话,则会在Safari中打开该链接
支持Universal Link(通用链接)
先决条件:必须有一个支持HTTPS的域名,并且拥有该域名下上传到根目录的权限(为了上传Apple指定文件)
集成步骤
1、开发者中心配置
找到对应的App ID,在Application Services列表里有Associated Domains一条,把它变为Enabled就可以了
2、工程配置
targets->Capabilites->Associated Domains,在其中的Domains中填入你想支持的域名,必须以applinks:为前缀,如:applinks:domain
3、配置指定文件
创建一个内容为json格式的文件,苹果将会在合适的时候,从我们在项目中填入的域名请求这个文件。这个文件名必须为apple-app-site-association,切记没有后缀名,文件内容大概是这样子:
{ 4、上传该文件 5、代码中的相关支持 Universal Link(通用链接)注意点 Universal Link跨域 当我们的App在设备上第一次运行时,如果支持Associated Domains功能,那么iOS会自动去GET定义的Domain下的apple-app-site-association文件 服务器上apple-app-site-association的更新不会让iOS本地的apple-app-site-association同步更新,即iOS只会在App第一次启动时请求一次,以后除非App更新或重新安装,否则不会在每次打开时请求apple-app-site-association Universal Link的好处 之前的Custom URL scheme是自定义的协议,因此在没有安装该app的情况下是无法直接打开的。而Universal Links本身就是一个能够指向web页面或者app内容页的标准web link,因此能够很好的兼容其他情况 作者:72行代码
“applinks”: {
“apps”: [],
“details”: [
{
“appID”: “9JA89QQLNQ.com.apple.wwdc”,
“paths”: [ “/wwdc/news/“, “/videos/wwdc/2015/“]
},
{
“appID”: “ABCD1234.com.apple.wwdc”,
“paths”: [ ““ ]
}
]
}
}
复制代码appID:组成方式是TeamID.BundleID。如上面的9JA89QQLNQ就是teamId。登陆开发者中心,在Account -> Membership里面可以找到Team ID
paths:设定你的app支持的路径列表,只有这些指定路径的链接,才能被app所处理。*的写法代表了可识别域名下所有链接
上传该文件到你的域名所对应的根目录或者.well-known目录下,这是为了苹果能获取到你上传的文件。上传完后,先访问一下,看看是否能够获取到,当你在浏览器中输入这个文件链接后,应该是直接下载apple-app-site-association文件
当点击某个链接,可以直接进我们的app,但是我们的目的是要能够获取到用户进来的链接,根据链接来展示给用户相应的内容,我们需要在工程里实现AppDelegate对应的方法:
// NSUserActivityTypeBrowsingWeb 由Universal Links唤醒的APP
if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]){
} NSURL *webpageURL = userActivity.webpageURL;
NSString *host = webpageURL.host;
if ([host isEqualToString:@"api.r2games.com.cn"]){
//进行我们的处理
NSLog(@"TODO....");
}else{
NSLog(@"openurl");
[[UIApplication sharedApplication] openURL:webpageURL options:nil completionHandler:nil];
// [[UIApplication sharedApplication] openURL:webpageURL];
}
return YES;
}
复制代码苹果为了方便开发者,提供了一个网页验证我们编写的这个apple-app-site-association是否合法有效
Universal Link有跨域问题,Universal Link必须要求跨域,如果不跨域,就不会跳转(iOS 9.2之后的改动)
假如当前网页的域名是A,当前网页发起跳转的域名是B,必须要求B和A是不同域名才会触发Universal Link,如果B和A是相同域名,只会继续在当前WebView里面进行跳转,哪怕你的Universal Link一切正常,根本不会打开App
Universal Link请求apple-app-site-association时机
Universal links是从服务器上查询是哪个app需要被打开,因此不存在Custom URL scheme那样名字被抢占、冲突的情况
Universal links支持从其他app中的UIWebView中跳转到目标app
提供Universal link给别的app进行app间的交流时,对方并不能够用这个方法去检测你的app是否被安装(之前的custom scheme URL的canOpenURL方法可以)
链接:https://juejin.cn/post/6844903988526055437
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »
iOS Instruments使用
一、Instruments介绍
Instruments 一个很灵活的、强大的工具,是性能分析、动态跟踪 和分析OS X以及iOS代码的测试工具,用它可以极为方便收集关于一个或多个系统进程的性能和行为的数据,并能及时随着时间跟踪而产生的数据,并检查所收集的数据,还可以广泛收集不同类型的数据.也可以追踪程序运行的过程,这样instrument就可以帮助我们了解用户的应用程序和操作系统的行为。
总结一下instrument能做的事情:
1. Instruments是用于动态调追踪和分析OS X和iOS的代码的性能分析和测试工具;instrument还可以:
2.Instruments支持多线程的调试;
3.可以用Instruments去录制和回放,图形用户界面的操作过程
4.可将录制的图形界面操作和Instruments保存为模板,供以后访问使用。
1.追踪代码中的(甚至是那些难以复制的)问题;
2.分析程序的性能;
3.实现程序的自动化测试;
4.部分实现程序的压力测试;
5.执行系统级别的通用问题追踪调试;
6.使你对程序的内部运行过程更加了解。
打开方式:Xcode -> Open Developer Tool -> Instruments
其中比较常用的有四种:
1.Allocations:用来检查内存分配,跟踪过程的匿名虚拟内存和堆的对象提供类名和可选保留/释放历史
2.Leaks:一般的查看内存使用情况,检查泄漏的内存,并提供了所有活动的分配和泄漏模块的类对象分配统计信息以及内存地址历史记录
3.Time Profiler:分析代码的执行时间,执行对系统的CPU上运行的进程低负载时间为基础采样
4.Zombies:检查是否访问了僵尸对象
其他的:
Blank:创建一个空的模板,可以从Library库中添加其他模板
Activity Monitor:显示器处理的CPU、内存和网络使用情况统计
Automation:用JavaScript语言编写,主要用于分析应用的性能和用户行为,模仿/击发被请求的事件,利用它可以完成对被测应用的简单的UI测试及相关功能测试
Cocoa Layout:观察约束变化,找出布局代码的问题所在。
Core Animation:用来检测Core Animation性能的,给我们提供了周期性的FPS,并且考虑到了发生在程序之外的动画,界面滑动FPS可以进行测试
Core Data:监测读取、缓存未命中、保存等操作,能直观显示是否保存次数远超实际需要
Energy Diagnostic :用于Xcode下的Instruments来分析手机电量消耗的。(必须是真机才有电量)
GPU Driver :可以测量GPU的利用率,同样也是一个很好的来判断和GPU相关动画性能的指示器。它同样也提供了类似Core Animtaion那样显示FPS的工具。
Network:分析应用程序如何使用TCP / IP和UDP / IP连接使用连接仪器。就是检查手机网速的。(这个最好是真机)
二、Allocations(分配)
1.内存分类:
Leaked memory:泄漏的内存,如为对象A申请了内存空间,之后再也没用到A,也没有释放A导致内存泄漏(野指针。。。)
Abandoned memory:被遗弃的内存,如循环引用,递归不断申请内存而导致的内存泄漏
Cached memory:缓存的内存
2.Abandoned memory
其中内存泄漏我们可以用Leaks
,野指针可以用Zombies
(僵尸对象),而在这里我们就可以用Allocations
来检测Abandoned memory
的内存。
即我们采用Generational Analysis的方法来分析,反复进入退出某一场景,查看内存的分配与释放情况,以定位哪些对象是属于Abandoned Memory的范畴。
在Allocations工具中,有专门的Generational Analysis设置,如下:
我们可以在程序运行时,在进入某个模块前标记一个Generation,这样会生成一个快照。然后进入、退出,再标记一个Generation,如下图:
在详情面板中我们可以看到两个Generation间内存的增长情况,其中就可能存在潜在的被遗弃的对象,如下图:
其中growth就是我们增长的内存,GenerationA是程序启动到进入该场景增长的内存,GenerationB就是第二次进入该场景所增长的内存,查看子类可以发现有两个管理类造成了Abandoned memory
。
3.设置Generations
使用instrument测试内存泄露 工具 Allocations 测试是否内存泄露 使用标记,可以更省事省力的测试页面是否有内存泄露
1)设置Generations
2)选择mark generation
3)使用方法 在进入测试页面之前,mark一下----->进入页面----->退出----->mark------>进入------->退出------->mark------>进入如此往复5、6次,就可以看到如下结果
这种情况下是内存有泄露,看到每次的增量都是好几百K或者上M的,都是属于内存有泄露的,这时候就需要检测下代码一般情况
100K以下都属于正常范围,growth表示距离你上次mark的增量
三、Leaks(泄漏)
1.内存溢出和内存泄漏的区别
内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出
内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
memory leak会最终会导致out of memory!
在前面的ALLcations里面我们提到过内存泄漏就是应该释放而没有释放的内存。而内存泄漏分为两种:Leaked Memory 和 Abandoned Memory。前面我们讲到了如何找到Abandoned Memory被遗忘的内存,现在我们研究的就是Leaked Memory。
以发生的方式来分类,内存泄漏可以分为4类:
常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。
隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。
影响:从用户使用程序的角度来看,内存泄漏本身不会产生什么危害,作为一般的用户,根本感觉不到内存泄漏的存在。真正有危害的是内存泄漏的堆积,这会最终消耗尽系统所有的内存。从这个角度来说,一次性内存泄漏并没有什么危害,因为它不会堆积,而隐式内存泄漏危害性则非常大,因为较之于常发性和偶发性内存泄漏它更难被检测到。
下边我们介绍Instruments里面的Leaked的用法,首先打开Leaked,跑起工程来,点击要测试的页面,如果有内存泄漏,会出现下图中的红色的❌。然后按照后边的步骤进行修复即可
上面的旧版的样式,下面的是新版的样式,基本操作差不多
在详情面板选中显示的若干条中的一条,双击,会自动跳到内存泄露代码处,然后点击右上角 Xcode 图标进行修改。
下图是对Leaked页面进一步的理解:
内存泄漏动态分析技巧:
1.在 Display Settings 界面建议把 Snapshot Interval (snapʃɒt, 数据快照)间隔时间设置为10秒,勾选Automatic Snapshotting,Leaks 会自动进行内存捕捉分析。(新版本直接在底部修改)
2.熟练使用 Leaks 后会对内存泄漏判断更准确,在可能导致泄漏的操作里,在你怀疑有内存泄漏的操作前和操作后,可以点击 Snapshot Now 进行手动捕捉。
3.开始时如果设备性能较好,可以把自动捕捉间隔设置为 5 秒钟。
4.使用ARC的项目,一般内存泄漏都是 malloc、自定义结构、资源引起的,多注意这些地方进行分析。
5.开启ARC后,内存泄漏的原因,开启了ARC并不是就不会存在内存问题,苹果有句名言:ARC is only for NSObject。
注:如果你的项目使用了ARC,随着你的操作,不断开启或关闭视图,内存可能持续上升,但这不一定表示存在内存泄漏,ARC释放的时机是不固定的
这里对 Display Settings中 的 Call tree
选项做一下说明 [官方user guide翻译]:
Separate By Thread:线程分离,只有这样才能在调用路径中能够清晰看到占用CPU最大的线程.每个线程应该分开考虑。只有这样你才能揪出那些大量占用CPU的"重"线程,按线程分开做分析,这样更容易揪出那些吃资源的问题线程。特别是对于主线程,它要处理和渲染所有的接口数据,一旦受到阻塞,程序必然卡顿或停止响应。
Invert Call Tree:从上到下跟踪堆栈信息.这个选项可以快捷的看到方法调用路径最深方法占用CPU耗时(这意味着你看到的表中的方法,将已从第0帧开始取样,这通常你是想要的,只有这样你才能看到CPU中花费时间最深的方法),比如FuncA{FunB{FunC}},勾选后堆栈以C->B->A把调用层级最深的C显示最外面.反向输出调用树。把调用层级最深的方法显示在最上面,更容易找到最耗时的操作。
Hide Missing Symbols:如果dSYM无法找到你的APP或者调用系统框架的话,那么表中将看到调用方法名只能看到16进制的数值,勾选这个选项则可以隐藏这些符号,便于简化分析数据.
Hide System Libraries:表示隐藏系统的函数,调用这个就更有用了,勾选后耗时调用路径只会显示app耗时的代码,性能分析普遍我们都比较关系自己代码的耗时而不是系统的.基本是必选项.注意有些代码耗时也会纳入系统层级,可以进行勾选前后前后对执行路径进行比对会非常有用.因为通常你只关心cpu花在自己代码上的时间不是系统上的,隐藏系统库文件。过滤掉各种系统调用,只显示自己的代码调用。隐藏缺失符号。如果 dSYM 文件或其他系统架构缺失,列表中会出现很多奇怪的十六进制的数值,用此选项把这些干扰元素屏蔽掉,让列表回归清爽。
Show Obj-C Only:只显示oc代码 ,如果你的程序是像OpenGl这样的程序,不要勾选侧向因为他有可能是C++的
Flatten Recursion:递归函数, 每个堆栈跟踪一个条目,拼合递归。将同一递归函数产生的多条堆栈(因为递归函数会调用自己)合并为一条。
Top Functions:找到最耗时的函数或方法。 一个函数花费的时间直接在该函数中的总和,以及在函数调用该函数所花费的时间的总时间。因此,如果函数A调用B,那么A的时间报告在A花费的时间加上B.花费的时间,这非常真有用,因为它可以让你每次下到调用堆栈时挑最大的时间数字,归零在你最耗时的方法。
四、Time Profiler(时间分析器)
用来检测app中每个方法所用的时间,并且可以排序,并查找出哪些函数占用了大量时间。
使用Time Profile前有两点需要注意的地方:
1、一定要使用真机调试
在开始进行应用程序性能分析的时候,一定要使用真机。因为模拟器运行在Mac上,然而Mac上的CPU往往比iOS设备要快。相反,Mac上的GPU和iOS设备的完全不一样,模拟器不得已要在软件层面(CPU)模拟设备的GPU,这意味着GPU相关的操作在模拟器上运行的更慢,尤其是使用CAEAGLLayer来写一些OpenGL的代码时候,这就导致模拟器性能数据和用户真机使用性能数据相去甚远
2、应用程序一定要使用发布配置
在发布环境打包的时候,编译器会引入一系列提高性能的优化,例如去掉调试符号或者移除并重新组织代码。另iOS引入一种"Watch Dog"[看门狗]机制,不同的场景下,“看门狗”会监测应用的性能,如果超出了该场景所规定的运行时间,“看门狗”就会强制终结这个应用的进程。开发者可以crashlog看到对应的日志,但Xcode在调试配置下会禁用"Watch Dog"
五、Zombies(僵尸)
1.概念
2.原理
我们将僵尸对象“复活”的目的:僵尸对象就是让已经释放了的对象重新复活,便于调试;是为了让已经释放了的对象在被再次访问时能够输出一些错误信息。其实这里的“复活”并不是真的复活,而是强行不死:这么说吧 相当于 他的RC=0的时候 系统再强行让他RC=1,顺便打上一个标记 zoom,等到你去掉那个沟以后 系统会把带有标记zoom的对象RC=0。
3.用法
六、扩展
C语言: 当我们声明1个指针变量,没有为这个指针变量赋初始值.这个指针变量的值是1个垃圾指 指向1块随机的内存空间。
OC语言: 指针指向的对象已经被回收掉了.这个指针就叫做野指针.
僵尸对象: 1个已经被释放的对象 就叫做僵尸对象.
使用野指针访问僵尸对象.有的时候会出问题,有的时候不会出问题.
当野指针指向的僵尸对象所占用的空间还没有分配给别人的时候, - 这个时候其实是可以访问的.
因为对象的数据还在.
当野指针指向的对象所占用的空间分配给了别人的时候 这个时候访问就会出问题.
所以,你不要通过1个野指针去访问1个僵尸对象.
虽然可以通过野指针去访问已经被释放的对象,但是我们不允许这么做.
僵尸对象检测.
默认情况下. Xcode不会去检测指针指向的对象是否为1个僵尸对象. 能访问就访问 不能访问就报错.
可以开启Xcode的僵尸对象检测.
那么就会在通过指针访问对象的时候,检测这个对象是否为1个僵尸对象 如果是僵尸对象 就会报错.
为什么不默认开启僵尸对象检测呢?
因为一旦开启,每次通过指针访问对象的时候.都会去检查指针指向的对象是否为僵尸对象.
那么这样的话 就影响效率了.
如何避免僵尸对象报错.
当1个指针变为野指针以后. 就把这个指针的值设置为nil
僵尸对象无法复活.
当1个对象的引用计数器变为0以后 这个对象就被释放了.
就无法取操作这个僵尸对象了. 所有对这个对象的操作都是无效的.
因为一旦对象被回收 对象就是1个僵尸对象 而访问1个僵尸对象 是没有意义.
摘自:https://blog.csdn.net/weixin_41963895/article/details/107231347
收起阅读 »iOS-事件传递&&响应机制(二)
如果想让某个view不能处理事件(或者说,事件传递到某个view那里就断了),那么可以通过刚才提到的三种方式。比如,设置其userInteractionEnabled = NO;那么传递下来的事件就会由该view的父控件处理。
例如,不想让蓝色的view接收事件,那么可以设置蓝色的view的userInteractionEnabled = NO;那么点击黄色的view或者蓝色的view所产生的事件,最终会由橙色的view处理,橙色的view就会成为最合适的view。
所以,不管视图能不能处理事件,只要点击了视图就都会产生事件,关键在于该事件最终是由谁来处理!也就是说,如果蓝色视图不能处理事件,点击蓝色视图产生的触摸事件不会由被点击的视图(蓝色视图)处理!
注意:如果设置父控件的透明度或者hidden,会直接影响到子控件的透明度和hidden。如果父控件的透明度为0或者hidden = YES,那么子控件也是不可见的!
3.3.如何寻找最合适的view
应用如何找到最合适的控件来处理事件?
1.首先判断主窗口(keyWindow)自己是否能接受触摸事件
2.触摸点是否在自己身上
3.从后往前遍历子控件,重复前面的两个步骤(首先查找数组中最后一个元素)
4.如果没有符合条件的子控件,那么就认为自己最合适处理
详述:
1.主窗口接收到应用程序传递过来的事件后,首先判断自己能否接手触摸事件。如果能,那么在判断触摸点在不在窗口自己身上
2.如果触摸点也在窗口身上,那么窗口会从后往前遍历自己的子控件(遍历自己的子控件只是为了寻找出来最合适的view)
3.遍历到每一个子控件后,又会重复上面的两个步骤(传递事件给子控件,1.判断子控件能否接受事件,2.点在不在子控件上)
4.如此循环遍历子控件,直到找到最合适的view,如果没有更合适的子控件,那么自己就成为最合适的view。
找到最合适的view后,就会调用该view的touches方法处理具体的事件。所以,只有找到最合适的view,把事件传递给最合适的view后,才会调用touches方法进行接下来的事件处理。找不到最合适的view,就不会调用touches方法进行事件处理。
注意:之所以会采取从后往前遍历子控件的方式寻找最合适的view只是为了做一些循环优化。因为相比较之下,后添加的view在上面,降低循环次数。
3.3.1.寻找最合适的view底层剖析
两个重要的方法:
hitTest:withEvent:方法
pointInside方法
3.3.1.1.hitTest:withEvent:方法
什么时候调用?
只要事件一传递给一个控件,这个控件就会调用他自己的hitTest:withEvent:方法
作用
寻找并返回最合适的view(能够响应事件的那个最合适的view)
注 意
:不管这个控件能不能处理事件,也不管触摸点在不在这个控件上,事件都会先传递给这个控件,随后再调用hitTest:withEvent:方法
1.正因为hitTest:withEvent:方法可以返回最合适的view,所以可以通过重写hitTest:withEvent:方法,返回指定的view作为最合适的view。
2.不管点击哪里,最合适的view都是hitTest:withEvent:方法中返回的那个view。
3.通过重写hitTest:withEvent:,就可以拦截事件的传递过程,想让谁处理事件谁就处理事件。
事件传递给谁,就会调用谁的hitTest:withEvent:方法。注 意
:如果hitTest:withEvent:方法中返回nil,那么调用该方法的控件本身和其子控件都不是最合适的view,也就是在自己身上没有找到更合适的view。那么最合适的view就是该控件的父控件。
所以事件的传递顺序是这样的:
产生触摸事件->UIApplication事件队列->[UIWindow hitTest:withEvent:]->返回更合适的view->[子控件 hitTest:withEvent:]->返回最合适的view
事件传递给窗口或控件的后,就调用hitTest:withEvent:方法寻找更合适的view。所以是,先传递事件,再根据事件在自己身上找更合适的view。
不管子控件是不是最合适的view,系统默认都要先把事件传递给子控件,经过子控件调用子控件自己的hitTest:withEvent:方法验证后才知道有没有更合适的view。即便父控件是最合适的view了,子控件的hitTest:withEvent:方法还是会调用,不然怎么知道有没有更合适的!即,如果确定最终父控件是最合适的view,那么该父控件的子控件的hitTest:withEvent:方法也是会被调用的。
技巧:想让谁成为最合适的view就重写谁自己的父控件的hitTest:withEvent:方法返回指定的子控件,或者重写自己的hitTest:withEvent:方法 return self。但是,建议在父控件的hitTest:withEvent:中返回子控件作为最合适的view!
原因在于在自己的hitTest:withEvent:方法中返回自己有时候会出现问题。因为会存在这么一种情况:当遍历子控件时,如果触摸点不在子控件A自己身上而是在子控件B身上,还要要求返回子控件A作为最合适的view,采用返回自己的方法可能会导致还没有来得及遍历A自己,就有可能已经遍历了点真正所在的view,也就是B。这就导致了返回的不是自己而是触摸点真正所在的view。所以还是建议在父控件的hitTest:withEvent:中返回子控件作为最合适的view!
例如:whiteView有redView和greenView两个子控件。redView先添加,greenView后添加。如果要求无论点击那里都要让redView作为最合适的view(把事件交给redView来处理)那么只能在whiteView的hitTest:withEvent:方法中return self.subViews[0];这种情况下在redView的hitTest:withEvent:方法中return self;是不好使的!
// 这里redView是whiteView的第0个子控件
#import "redView.h"
@implementation redView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
return self;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
NSLog(@"red-touch");
}@end
// 或者
#import "whiteView.h"
@implementation whiteView
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
return self.subviews[0];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
NSLog(@"white-touch");
}
@end
特殊情况:
谁都不能处理事件,窗口也不能处理。
重写window的hitTest:withEvent:方法return nil
只能有窗口处理事件。
控制器的view的hitTest:withEvent:方法return nil或者window的hitTest:withEvent:方法return self
return nil的含义:
hitTest:withEvent:中return nil的意思是调用当前hitTest:withEvent:方法的view不是合适的view,子控件也不是合适的view。如果同级的兄弟控件也没有合适的view,那么最合适的view就是父控件。
寻找最合适的view底层剖析之hitTest:withEvent:方法底层做法
#import "WYWindow.h"
@implementation WYWindow
// 什么时候调用:只要事件一传递给一个控件,那么这个控件就会调用自己的这个方法
// 作用:寻找并返回最合适的view
// UIApplication -> [UIWindow hitTest:withEvent:]寻找最合适的view告诉系统
// point:当前手指触摸的点
// point:是方法调用者坐标系上的点
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
// 1.判断下窗口能否接收事件
if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
// 2.判断下点在不在窗口上
// 不在窗口上
if ([self pointInside:point withEvent:event] == NO) return nil;
// 3.从后往前遍历子控件数组
int count = (int)self.subviews.count;
for (int i = count - 1; i >= 0; i--) {
// 获取子控件
UIView *childView = self.subviews[i];
// 坐标系的转换,把窗口上的点转换为子控件上的点
// 把自己控件上的点转换成子控件上的点
CGPoint childP = [self convertPoint:point toView:childView];
UIView *fitView = [childView hitTest:childP withEvent:event];
if (fitView) {
// 如果能找到最合适的view
return fitView;
}
}
// 4.没有找到更合适的view,也就是没有比自己更合适的view
return self;
}
// 作用:判断下传入过来的点在不在方法调用者的坐标系上
// point:是方法调用者坐标系上的点
//- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
//{
// return NO;
//}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
NSLog(@"%s",__func__);
}
@end
hit:withEvent:方法底层会调用pointInside:withEvent:方法判断点在不在方法调用者的坐标系上。
3.3.1.2.pointInside:withEvent:方法
pointInside:withEvent:方法判断点在不在当前view上(方法调用者的坐标系上)如果返回YES,代表点在方法调用者的坐标系上;返回NO代表点不在方法调用者的坐标系上,那么方法调用者也就不能处理事件。
3.3.2.练习
屏幕上现在有一个viewA,viewA有一个subView叫做viewB,要求触摸viewB时,viewB会响应事件,而触摸viewA本身,不会响应该事件。如何实现?
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
UIView *view = [super hitTest:point withEvent:event];
if (view == self) {
return nil;
}
return view;
}
(四)事件的响应
4.1.触摸事件处理的整体过程
1>用户点击屏幕后产生的一个触摸事件,经过一系列的传递过程后,会找到最合适的视图控件来处理这个事件
2>找到最合适的视图控件后,就会调用控件的touches方法来作具体的事件处理touchesBegan…touchesMoved…touchedEnded…3>这些touches方法的默认做法是将事件顺着响应者链条向上传递(也就是touch方法默认不处理事件,只传递事件),将事件交给上一个响应者进行处理
4.2.响应者链条示意图
响应者链条:
在iOS程序中无论是最后面的UIWindow还是最前面的某个按钮,它们的摆放是有前后关系的,一个控件可以放到另一个控件上面或下面,那么用户点击某个控件时是触发上面的控件还是下面的控件呢,这种先后关系构成一个链条就叫“响应者链”。也可以说,响应者链是由多个响应者对象连接起来的链条。在iOS中响应者链的关系可以用下图表示
响应者对象:能处理事件的对象,也就是继承自UIResponder的对象
作用:能很清楚的看见每个响应者之间的联系,并且可以让一个事件多个对象处理。
如何判断上一个响应者
1> 如果当前这个view是控制器的view,那么控制器就是上一个响应者
2> 如果当前这个view不是控制器的view,那么父控件就是上一个响应者
响应者链的事件传递过程:
1>如果当前view是控制器的view,那么控制器就是上一个响应者,事件就传递给控制器;如果当前view不是控制器的view,那么父视图就是当前view的上一个响应者,事件就传递给它的父视图
2>在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理
3>如果window对象也不处理,则其将事件或消息传递给UIApplication对象
4>如果UIApplication也不能处理该事件或消息,则将其丢弃
事件处理的整个流程总结:
1.触摸屏幕产生触摸事件后,触摸事件会被添加到由UIApplication管理的事件队列中(即,首先接收到事件的是UIApplication)。
2.UIApplication会从事件队列中取出最前面的事件,把事件传递给应用程序的主窗口(keyWindow)。
3.主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件。(至此,第一步已完成)
4.最合适的view会调用自己的touches方法处理事件
5.touches默认做法是把事件顺着响应者链条向上抛。
touches的默认做法:#import "WYView.h"
@implementation WYView
//只要点击控件,就会调用touchBegin,如果没有重写这个方法,自己处理不了触摸事件
// 上一个响应者可能是父控件
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
// 默认会把事件传递给上一个响应者,上一个响应者是父控件,交给父控件处理
[super touchesBegan:touches withEvent:event];
// 注意不是调用父控件的touches方法,而是调用父类的touches方法
// super是父类 superview是父控件
}
@end
事件的传递与响应:
1、当一个事件发生后,事件会从父控件传给子控件,也就是说由UIApplication -> UIWindow -> UIView -> initial view,以上就是事件的传递,也就是寻找最合适的view的过程。
2、接下来是事件的响应。首先看initial view能否处理这个事件,如果不能则会将事件传递给其上级视图(inital view的superView);如果上级视图仍然无法处理则会继续往上传递;一直传递到视图控制器view controller,首先判断视图控制器的根视图view是否能处理此事件;如果不能则接着判断该视图控制器能否处理此事件,如果还是不能则继续向上传 递;(对于第二个图视图控制器本身还在另一个视图控制器中,则继续交给父视图控制器的根视图,如果根视图不能处理则交给父视图控制器处理);一直到 window,如果window还是不能处理此事件则继续交给application处理,如果最后application还是不能处理此事件则将其丢弃
3、在事件的响应中,如果某个控件实现了touches...方法,则这个事件将由该控件来接受,如果调用了[super touches….];就会将事件顺着响应者链条往上传递,传递给上一个响应者;接着就会调用上一个响应者的touches….方法
如何做到一个事件多个对象处理:
因为系统默认做法是把事件上抛给父控件,所以可以通过重写自己的touches方法和父控件的touches方法来达到一个事件多个对象处理的目的。
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
// 1.自己先处理事件...
NSLog(@"do somthing...");
// 2.再调用系统的默认做法,再把事件交给上一个响应者处理
[super touchesBegan:touches withEvent:event];
}
事件处理的整个流程总结:
1.触摸屏幕产生触摸事件后,触摸事件会被添加到由UIApplication管理的事件队列中(即,首先接收到事件的是UIApplication)。
2.UIApplication会从事件队列中取出最前面的事件,把事件传递给应用程序的主窗口(keyWindow)。
iOS-事件传递&&响应机制(一)
前言:
按照时间顺序,事件的生命周期:
事件的产生和传递(事件如何从父控件传递到子控件并寻找到最合适的view、寻找最合适的view的底层实现、拦截事件的处理)->找到最合适的view后事件的处理(touches方法的重写,也就是事件的响应)
重点和难点是:
1.如何寻找最合适的view
2.寻找最合适的view的底层实现(hitTest:withEvent:底层实现)
(一)iOS中的事件
iOS中的事件可以分为3大类型:
1.触摸事件
2.加速计事件
3.远程控制事件
这里我们只讨论iOS中的触摸事件。
1.1.响应者对象(UIResponder)
学习触摸事件首先要了解一个比较重要的概念-响应者对象(UIResponder)。在iOS中不是任何对象都能处理事件,只有继承了UIResponder的对象才能接受并处理事件,我们称之为“响应者对象”。以下都是继承自UIResponder的,所以都能接收并处理事件。
UIApplication
UIWindows
UIViewController
UIView
因为UIResponder中提供了以下4个对象方法来处理触摸事件。UIResponder内部提供了以下方法来处理事件触摸事件
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
加速计事件
- (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
- (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;
远程控制事件
- (void)remoteControlReceivedWithEvent:(UIEvent *)event;
(二)事件的处理
// UIView是UIResponder的子类,可以覆盖下列4个方法处理不同的触摸事件
// 一根或者多根手指开始触摸view,系统会自动调用view的下面方法
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
// 一根或者多根手指在view上移动,系统会自动调用view的下面方法(随着手指的移动,会持续调用该方法)
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
// 一根或者多根手指离开view,系统会自动调用view的下面方法
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
// 触摸结束前,某个系统事件(例如电话呼入)会打断触摸过程,系统会自动调用view的下面方法
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
// 提示:touches中存放的都是UITouch对象
需要注意的是:以上四个方法是由系统自动调用的,所以可以通过重写该方法来处理一些事件。
1.如果两根手指同时触摸一个view,那么view只会调用一次touchesBegan:withEvent:方法,touches参数中装着2个UITouch对象
2.如果这两根手指一前一后分开触摸同一个view,那么view会分别调用2次touchesBegan:withEvent:方法,并且每次调用时的touches参数中只包含一个UITouch对象
3.重写以上四个方法,如果是处理UIView的触摸事件。必须要自定义UIView子类继承自UIView。因为苹果不开源,没有把UIView的.m文件提 供给我们。我们只能通过子类继承父类,重写子类方法的方式处理UIView的触摸事件(注意:我说的是UIView触摸事件而不是说的 UIViewController的触摸事件)。
4.如果是处理UIViewController的触摸事件,那么在控制器的.m文件中直接重写那四个方法即可!
/************************自定义UIView的.h.m文件************************/
#import
@interface WYView : UIView
@end
#import "WYView.h"
@implementation WYView
// 开始触摸时就会调用一次这个方法
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
NSLog(@"摸我干啥!");
}
// 手指移动就会调用这个方法
// 这个方法调用非常频繁
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
NSLog(@"移动过程中持续调用!");
}
// 手指离开屏幕时就会调用一次这个方法
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
NSLog(@"手放开还能继续玩耍!");
}
@end
/**************************控制器的.m文件*************************/
#import "ViewController.h"
#import "WYView.h"
@interface ViewController ()
@end@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 创建自定义view
WYView *touchView = [[WYView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
// 背景颜色
touchView.backgroundColor = [UIColor redColor];
// 添加到父控件
[self.view addSubview:touchView];
}
@end
注 意:有人认为,我要是处理控制器的自带的view的事件就不需要自定义UIView子类继承于UIView,因为可以在viewController.m 文件中重写touchBegan:withEvent:方法,但是,我们此处讨论的是处理UIView的触摸事件,而不是处理 UIViewController的触摸事件。你如果是在viewController.m文件中重写touchBegan:withEvent:方法,相当于处理的是viewController的触摸事件,因为viewController也是继承自UIResponder,所以会给人一种错觉。
所以,还是那句话,想处理UIView的触摸事件,必须自定义UIView子类继承自UIView。
2.1.UIView的拖拽
那么,如何实现UIView的拖拽呢?也就是让UIView随着手指的移动而移动。
- 重写touchsMoved:withEvent:方法
此时需要用到参数touches,下面是UITouch的属性和方法:
NS_CLASS_AVAILABLE_IOS(2_0) @interface UITouch : NSObject
@property(nonatomic,readonly) NSTimeInterval timestamp;
@property(nonatomic,readonly) UITouchPhase phase;
@property(nonatomic,readonly) NSUInteger tapCount; // touch down within a certain point within a certain amount of time
// majorRadius and majorRadiusTolerance are in points
// The majorRadius will be accurate +/- the majorRadiusTolerance
@property(nonatomic,readonly) CGFloat majorRadius NS_AVAILABLE_IOS(8_0);
@property(nonatomic,readonly) CGFloat majorRadiusTolerance NS_AVAILABLE_IOS(8_0);
@property(nullable,nonatomic,readonly,strong) UIWindow *window;
@property(nullable,nonatomic,readonly,strong) UIView *view;
@property(nullable,nonatomic,readonly,copy) NSArray *gestureRecognizers NS_AVAILABLE_IOS(3_2);
- (CGPoint)locationInView:(nullable UIView *)view;
- (CGPoint)previousLocationInView:(nullable UIView *)view;
// Force of the touch, where 1.0 represents the force of an average touch
@property(nonatomic,readonly) CGFloat force NS_AVAILABLE_IOS(9_0);
// Maximum possible force with this input mechanism
@property(nonatomic,readonly) CGFloat maximumPossibleForce NS_AVAILABLE_IOS(9_0);
2.1.1.UITouch对象
当用户用一根手指触摸屏幕时,会创建一个与手指相关的UITouch对象
一根手指对应一个UITouch对象
如果两根手指同时触摸一个view,那么view只会调用一次touchesBegan:withEvent:方法,touches参数中装着2个UITouch对象
如果这两根手指一前一后分开触摸同一个view,那么view会分别调用2次touchesBegan:withEvent:方法,并且每次调用时的touches参数中只包含一个UITouch对象
保存着跟手指相关的信息,比如触摸的位置、时间、阶段
当手指移动时,系统会更新同一个UITouch对象,使之能够一直保存该手指在的触摸位置
当手指离开屏幕时,系统会销毁相应的UITouch对象
提 示:iPhone开发中,要避免使用双击事件!
2.1.1.2.UITouch的属性
触摸产生时所处的窗口
@property(nonatomic,readonly,retain) UIWindow *window;
触摸产生时所处的视图
@property(nonatomic,readonly,retain) UIView *view
;
短时间内点按屏幕的次数,可以根据tapCount判断单击、双击或更多的点击
@property(nonatomic,readonly) NSUInteger tapCount;
记录了触摸事件产生或变化时的时间,单位是秒
@property(nonatomic,readonly) NSTimeInterval timestamp;
当前触摸事件所处的状态
@property(nonatomic,readonly) UITouchPhase phase;
2.1.1.3.UITouch的方法
(CGPoint)locationInView:(UIView *)view;
// 返回值表示触摸在view上的位置
// 这里返回的位置是针对view的坐标系的(以view的左上角为原点(0, 0))
// 调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置
(CGPoint)previousLocationInView:(UIView *)view;
// 该方法记录了前一个触摸点的位置
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
// 想让控件随着手指移动而移动,监听手指移动
// 获取UITouch对象
UITouch *touch = [touches anyObject];
// 获取当前点的位置
CGPoint curP = [touch locationInView:self];
// 获取上一个点的位置
CGPoint preP = [touch previousLocationInView:self];
// 获取它们x轴的偏移量,每次都是相对上一次
CGFloat offsetX = curP.x - preP.x;
// 获取y轴的偏移量
CGFloat offsetY = curP.y - preP.y;
// 修改控件的形变或者frame,center,就可以控制控件的位置
// 形变也是相对上一次形变(平移)
// CGAffineTransformMakeTranslation:会把之前形变给清空,重新开始设置形变参数
// make:相对于最原始的位置形变
// CGAffineTransform t:相对这个t的形变的基础上再去形变
// 如果相对哪个形变再次形变,就传入它的形变
self.transform = CGAffineTransformTranslate(self.transform, offsetX, offsetY);}
(三)iOS中的事件的产生和传递
3.1.事件的产生
发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件队列中,为什么是队列而不是栈?因为队列的特点是FIFO,即先进先出,先产生的事件先处理才符合常理,所以把事件添加到队列。
UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口(keyWindow)。
主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件,这也是整个事件处理过程的第一步。
找到合适的视图控件后,就会调用视图控件的touches方法来作具体的事件处理。
3.2.事件的传递
触摸事件的传递是从父控件传递到子控件
也就是UIApplication->window->寻找处理事件最合适的view
注 意: 如果父控件不能接受触摸事件,那么子控件就不可能接收到触摸事件
应用如何找到最合适的控件来处理事件?
1.首先判断主窗口(keyWindow)自己是否能接受触摸事件
2.判断触摸点是否在自己身上
3.子控件数组中从后往前遍历子控件,重复前面的两个步骤(所谓从后往前遍历子控件,就是首先查找子控件数组中最后一个元素,然后执行1、2步骤)
4.view,比如叫做fitView,那么会把这个事件交给这个fitView,再遍历这个fitView的子控件,直至没有更合适的view为止。
5.如果没有符合条件的子控件,那么就认为自己最合适处理这个事件,也就是自己是最合适的view。
UIView不能接收触摸事件的三种情况:
1.不允许交互:userInteractionEnabled = NO
2.隐藏:如果把父控件隐藏,那么子控件也会隐藏,隐藏的控件不能接受事件
3.透明度:如果设置一个控件的透明度<0.01,会直接影响子控件的透明度。alpha:0.0~0.01为透明。
注 意
:默认UIImageView不能接受触摸事件,因为不允许交互,即userInteractionEnabled = NO。所以如果希望UIImageView可以交互,需要设置UIImageView的userInteractionEnabled = YES。
总结一下
1.点击一个UIView或产生一个触摸事件A,这个触摸事件A会被添加到由UIApplication管理的事件队列中(即,首先接收到事件的是UIApplication)。
2.UIApplication会从事件对列中取出最前面的事件(此处假设为触摸事件A),把事件A传递给应用程序的主窗口(keyWindow)。
3.窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件。(至此,第一步已完成)
摘自链接:https://blog.csdn.net/wywinstonwy/article/details/105293525
iOS-异步绘制原理
在 UIView 中有一个 CALayer 的属性,负责 UIView 具体内容的显示。具体过程是系统会把 UIView 显示的内容(包括 UILabel 的文字,UIImageView 的图片等)绘制在一张画布上,完成后倒出图片赋值给 CALayer 的 contents 属性,完成显示。
这其中的工作都是在主线程中完成的,这就导致了主线程频繁的处理 UI 绘制的工作,如果要绘制的元素过多,过于频繁,就会造成卡顿。
那么是否可以将复杂的绘制过程放到后台线程中执行,从而减轻主线程负担,来提升 UI 流畅度呢?
答案是可以的,系统给我们留下的异步绘制的口子,请看下面的流程图,它是我们进行基本绘制的基础。
UIView 调用 setNeedsDisplay 方法其实是调用其 layer 属性的同名方法,这时 layer 并不会立刻调用 display 方法,而是要等到当前 runloop 即将结束的时候调用 display,进入到绘制流程。在 UIView 中 layer.delegate 就是 UIView 本身,UIView 并没有实现 displayLayer: 方法,所以进入系统的绘制流程,我们可以通过实现 displayLayer: 方法来进行异步绘制。
有了上面的异步绘制原理流程图,我们可以得到一个实现异步绘制的初步思路:
在“异步绘制入口”去开辟子线程,然后在子线程中实现和系统类似的绘制流程。
二、系统绘制流程
要实现异步绘制,我们首先要了解系统的绘制流程,看下面一张流程图:
三、异步绘制流程
我们看一幅时序图
#import "AsyncDrawLabel.h"
#import <CoreText/CoreText.h>
@implementation AsyncDrawLabel
- (void)setText:(NSString *)text {
_text = text;
[self.layer setNeedsDisplay];
}
- (void)setFont:(UIFont *)font {
_font = font;
[self.layer setNeedsDisplay];
}
- (void)displayLayer:(CALayer *)layer {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
__block CGSize size;
dispatch_sync(dispatch_get_main_queue(), ^{
size = self.bounds.size;
});
UIGraphicsBeginImageContextWithOptions(size, NO, UIScreen.mainScreen.scale);
CGContextRef context = UIGraphicsGetCurrentContext();
[self draw:context size:size];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
dispatch_async(dispatch_get_main_queue(), ^{
self.layer.contents = (__bridge id)image.CGImage;
});
});
}
- (void)draw:(CGContextRef)context size:(CGSize)size {
// 将坐标系上下翻转,因为底层坐标系和 UIKit 坐标系原点位置不同。
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, size.height); // 原点为左下角
CGContextScaleCTM(context, 1, -1);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(0, 0, size.width, size.height));
NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc]initWithString:self.text];
[attrStr addAttribute:NSFontAttributeName value:self.font range:NSMakeRange(0, self.text.length)];
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrStr);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attrStr.length), path, NULL);
CTFrameDraw(frame, context);
}
@end
AsyncDrawLabel 是一个继承 UIView 的类,其 Label 的文本绘制功能需要我们自己实现。
我们在 - (void)displayLayer:(CALayer *)layer 方法中异步在全局队列中创建上下文环境然后使用 - (void)draw:(CGContextRef)context size:(CGSize)size 方法进行文本的简单绘制,再回到主线程为 self.layer.contents 赋值。从而完成了一个简单的异步绘制。
当然这样的绘制的问题是,如果绘制数量较多,绘制频繁,会阻塞全局队列,因为全局队列中还有一些系统提交的任务需要执行,可能会对其造成影响。
YYAsyncLayer
我们需要更加优化的方式去管理异步绘制的线程和执行流程,使用 YYAsyncLayer 可以让我们把注意力放在具体的绘制(需要我们做的是上面代码中 - draw: size: 做的事情),而不需要考虑线程的管理,绘制的时机等,大大提高绘制的效率以及我们编程的速度。
YYAsyncLayer 的主要流程如下
在主线程的 RunLoop 中注册一个 observer,它的优先级要比系统的 CATransaction 低,保证系统先做完必须的工作。
把需要异步绘制的操作集中起来。比如设置字体、颜色、背景色等,不是设置一个就绘制一个,而是把它们集中起来,RunLoop 会在 observer 需要的时机通知统一处理。
处理时机到时,执行异步绘制,并在主线程中把绘制结果传递给 layer.contents。
流程图如下:
使用 YYAsyncLayer 的代码:
#import "AsyncDrawLabel.h"
#import <YYAsyncLayer.h>
#import <CoreText/CoreText.h>
@interface AsyncDrawLabel ()<YYAsyncLayerDelegate>
@end
@implementation AsyncDrawLabel
+ (Class)layerClass {
return YYAsyncLayer.class;
}
- (void)setText:(NSString *)text {
_text = text.copy;
[self commitTransaction];
}
- (void)setFont:(UIFont *)font {
_font = font;
[self commitTransaction];
}
- (void)layoutSubviews {
[super layoutSubviews];
[self commitTransaction];
}
- (void)contentsNeedUpdated {
[self.layer setNeedsDisplay];
}
- (void)commitTransaction {
[[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit];
}
// 在这里创建异步绘制的任务
- (YYAsyncLayerDisplayTask *)newAsyncDisplayTask {
YYAsyncLayerDisplayTask *task = [YYAsyncLayerDisplayTask new];
task.willDisplay = ^(CALayer * _Nonnull layer) {
};
task.display = ^(CGContextRef _Nonnull context, CGSize size, BOOL (^ _Nonnull isCancelled)(void)) {
if (isCancelled() || self.text.length == 0) {
return;
}
// 在这里进行异步绘制
[self draw:context size:size];
};
task.didDisplay = ^(CALayer * _Nonnull layer, BOOL finished) {
if (finished) {
} else {
}
};
return task;
}
- (void)draw:(CGContextRef)context size:(CGSize)size {
// 将坐标系上下翻转,因为底层坐标系和 UIKit 坐标系原点位置不同。
CGContextSetTextMatrix(context, CGAffineTransformIdentity);
CGContextTranslateCTM(context, 0, size.height); // 原点为左下角
CGContextScaleCTM(context, 1, -1);
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, CGRectMake(0, 0, size.width, size.height));
NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc]initWithString:self.text];
[attrStr addAttribute:NSFontAttributeName value:self.font range:NSMakeRange(0, self.text.length)];
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrStr);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attrStr.length), path, NULL);
CTFrameDraw(frame, context);
}
@end
原文链接:https://blog.csdn.net/wywinstonwy/article/details/105660643
收起阅读 »iOS-视图&图像相关
Auto Layout 原理
Auto Layout是一种全新的布局方式,它采用一系列约束(constraints)来实现自动布局,当你的屏幕尺寸发生变化或者屏幕发生旋转时,可以不用添加代码来保持原有布局不变,实现视图的自动布局。
所谓约束,通常是定义了两个视图之间的关系(当然你也可以一个视图自己跟自己设定约束)。如下图就是一个约束的例子,当然要确定一个视图的位置,跟基于frame一样,也是需要确定视图的横纵坐标以及宽度和高度的,只是,这个横纵坐标和宽度高度不再是写死的数值,而是根据约束计算得来,从而达到自动布局的效果。
UIView之drawRect: & layoutSubviews的作用和机制
drawRect 调用机制
1、调用时机:loadView ->ViewDidload ->drawRect:
2、如果在UIView初始化时没有设置rect大小,将直接导致drawRect:不被自动调用。
3、通过设置contentMode属性值为UIViewContentModeRedraw。那么将在每次设置或更改frame
的时候自动调用drawRect:
。
4、直接调用setNeedsDisplay,或者setNeedsDisplayInRect:触发drawRect:,但是有个前提条件是:view当前的rect不能为nil
5、该方法在调用sizeThatFits后被调用,所以可以先调用sizeToFit计算出size。然后系统自动调用drawRect:方法。
这里简单说一下sizeToFit和sizeThatFit:
sizeToFit:会计算出最优的 size 而且会改变自己的size
sizeThatFits:会计算出最优的 size 但是不会改变 自己的 size
注意事项
layoutSubviews
这个方法是用来对subviews重新布局
,默认没有做任何事情,需要子类进行重写。
当我们在某个类的内部调整子视图位置时,需要调用。
反过来的意思就是说:如果你想要在外部设置subviews的位置,就不要重写。
对subview重新布局
②、- (void)setNeedsLayout;
将视图标记为需要重新布局,
这个方法会在系统runloop的下一个周期自动调用layoutSubviews。
③、- (void)layoutIfNeeded;
如果有需要刷新的
标记
,立即调用layoutSubviews进行布局(如果没有标记,不会调用layoutSubviews)这里注意一个点:标记,没有标记,即使我们掉了该函数也不起作用
。如果要立即刷新,要先调用[view setNeedsLayout],把标记设为需要布局,然后马上调用[view layoutIfNeeded],实现布局.
在视图第一次显示之前,标记总是“需要刷新”的,可以直接调用[view layoutIfNeeded]
这里有必要描述下三者之间的关系:
在没有外界干预的情况下,一个view的frame或者bounds发生变化时,系统会先去标记flag这个view,等下一次渲染时机到来时(也就是runloop的下一次循环),会去按照最新的布局去重新布局视图。setNeedLayout
就是给这个view添加一个标记,告诉系统下一次渲染时机需要重新布局这个视图。layoutIfNeed
就是告诉系统,如果已经设置了flag,那不用等待下个渲染时机到来,立即重新渲染。前提是设置了flag。
而layoutSubviews
则是由系统去调用,不需要我们主动调用,我们只需要调用layoutIfNeed
,告诉系统是否立即执行重新布局的操作。
layoutSubviews调用时机
1、init初始化不会触发layoutSubviews。这里需要补充一点:
2、addSubview会触发layoutSubviews。(当然这里frame为0,是不会调用的,同上面的drawrect:一样)
3、设置view的Frame会触发layoutSubviews,(当然前提是frame的值设置前后发生了变化。)
4、滚动一个UIScrollView会触发layoutSubviews。
5、旋转屏幕会触发父UIView上的layoutSubviews事件。(这个我们开发中会经常遇到,比如屏幕旋转时,为了界面美观我们需要修改子view的frame,那就会在layoutSubview中做相应的操作)
6、改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件。
7、直接调用setLayoutSubviews。(Apple是不建议这么做的)
layoutSubview是布局相关,而drawRect则是负责绘制。因此从调用时序上来讲,layoutSubviews要早于drawRect:函数。
摘自链接:https://www.jianshu.com/p/2ab322e1c7d4
iOS底层系列:Category
前言
Category是我们平时用到的比较多的一种技术,比如说给某个类增加方法,添加成员变量,或者用Category优化代码结构。
我们通过下面这几个问题作为切入点,结合runtime的源码,探究一下Category的底层原理。
我们在Category中,可以直接添加方法,而且我们也都知道,添加的方法会合并到本类当中,同时我们也可以声明属性,但是此时的属性没有功能,也就是不能存值,这就类似于Swift中的计算属性,如果我们想让这个属性可以储存值,就要用runtime的方式,动态的添加。
探究
1. Category为什么能添加方法不能添加成员变量
首先我们先创建一个Person类,然后创建一个Person+Run的Category,并在Person+Run中实现-run方法。
我们可以使用命令行对Person+Run.m进行编译
xcrun -sdk iphonesimulator clang -rewrite-objc Person+Run.m
得到一个Person+Run.cpp文件,在文件的底部,可以找到这样一个结构体
struct _category_t {
const char *name;
struct _class_t *cls;
const struct _method_list_t *instance_methods;
const struct _method_list_t *class_methods;
const struct _protocol_list_t *protocols;
const struct _prop_list_t *properties;
};
这些字段几乎都是见名知意了。
每一个Category都会编译然后存储在一个_category_t类型的变量中
static struct _category_t _OBJC_$_CATEGORY_Person_$_Run __attribute__ ((used, section ("__DATA,__objc_const"))) =
{
"Person",
0, // &OBJC_CLASS_$_Person,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Run,
0,
0,
0,
};
因为我们的Person+Run里面只有一个实例方法,从上述代码中来看,也只有对应的位置传值了。
通过这个_category_t的结构结构我们也可以看出,属性存储在_prop_list_t,这里并没有类中的objc_ivar_list结构体,所以Category的_category_t结构体中根本没有储存ivar的地方,所以不能添加成员变量。
如果我们在分类中手动为成员变量添加了set和get方法之后,也可以调用,但实际上是没有内存来储值的,这就好像Swift中的计算属性,只起到了计算的作用,就相当于是两个方法(set和get),但是并不能拥有真用的内存来存储值。
举个例子
@property (copy, nonatomic) NSString * name;
下面这个声明如果实在类中,系统会默认帮我们声明一个成员变量_name, 在.h中声明setName和name两个方法,并提供setName和name方法的默认实现。
如果是在Category中,只相当于声明setName和name两个方法,没有实现也没有_name。
2. Category的方法是何时合并到类中的
大家都知道Category分类肯定是我们的应用启动是,通过运行时特性加载的,但是这个加载过程具体的细节就要结合runtime的源码来分析了。
runtime源码太多了,我们先通过大概浏览代码来定位实现功能的相关位置。
我从objc-runtime-new.mm中找到了下面这个方法。
void attachLists(List* const * addedLists, uint32_t addedCount)
而且他的注释一些的很清楚,修复类的方法,协议和变量列表,关联还未关联的分类。
然后我们继续找,就找到了我们需要的这个方法。
void attachLists(List* const * addedLists, uint32_t addedCount)
我们从其中摘出一段代码来分析就可以解决我们的问题了。
// many lists -> many lists
uint32_t oldCount = array()->count;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
array()->count = newCount;
memmove(array()->lists + addedCount, array()->lists,
oldCount * sizeof(array()->lists[0]));
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
在调用此方法之前我们所有的分类会被方法一个list里面(每一个分类都是一个元素),然后再调用attachLists方法,我们可以看到,在realloc的时候传进一个newCount,这是因为要增加分类中的方法,所以需要对之前的数组扩容,在扩容结束后先调用了memmove方法,在调用memcopy,大家可以上网查一下这两个方法具体的区别,这里简单一说,其实完成的效果都是把后面的内存的内容拷贝到前面内存中去,但是memmove可以处理内存重叠的问题。
其实也就是首先将原来数组中的每个元素先往后移动(我们要添加几个元素,就移动几位),因为移动后的位置,其实也是数组自己的内存空间,所以存在重叠问题,直接移动会导致元素丢失的问题,所以用memmove(会检测是否有内存重叠)。
移动完之后,把我们储存分类中方法的list中的元素移动到数组前面位置。
过程就是这样子了,其实我们第三个问题就顺便解决完了。
3. Category方法和类中方法的执行顺序
上面其实说到了,类中原来的方法是要往后面移动的,分类的方法添加到前面的位置,而且调用方法的时候是在list中遍历查找,所以我们调用方法的时候,肯定会先调用到Category中的方法,但是这并不是覆盖,因为我们的原方法还在,只是这中机制保证了如果分类中有重写类的方法,会被优先查找到。
4. +load和+initialize的区别
对于这个问题我们从两个角度出发分析,调用方式和调用时刻。
+load
简单的举一个例子,我们创建一个Person类,然后重写+load方法,然后为Person新建两个Category,都分别实现+load。
@implementation Person
+ (void)load {
NSLog(@"Person - load");
}
@end
@implementation Person (Test1)
+ (void)load {
NSLog(@"Person Test1 - load");
}
@end
@implementation Person (Test2)
+ (void)load {
NSLog(@"Person Test2 - load");
}
@end
当我们进行项目的时候,会得到下面的打印结果。
2020-09-14 09:34:41.900161+0800 Category[4533:53426] Person - load
2020-09-14 09:34:41.900629+0800 Category[4533:53426] Person Test1 - load
2020-09-14 09:34:41.900700+0800 Category[4533:53426] Person Test2 - load
我们并没有使用这个Person类和他的Category,所以应该是项目运行后,runtime在加载类和分类的时候,就会调用+load方法。
我们从源码中找到下面这个方法
void load_images(const char *path __unused, const struct mach_header *mh)
方法中的最后一行调用了call_load_methods(),这个call_load_methods()中就是实现了+load的调用方式。
下面是call_load_methods()函数的实现 ,大家简单浏览一遍
void call_load_methods(void)
{
static bool loading = NO;
bool more_categories;
loadMethodLock.assertLocked();
// Re-entrant calls do nothing; the outermost call will finish the job.
if (loading) return;
loading = YES;
void *pool = objc_autoreleasePoolPush();
do {
// 1. Repeatedly call class +loads until there aren't any more
while (loadable_classes_used > 0) {
call_class_loads();
}
// 2. Call category +loads ONCE
more_categories = call_category_loads();
// 3. Run more +loads if there are classes OR more untried categories
} while (loadable_classes_used > 0 || more_categories);
objc_autoreleasePoolPop(pool);
loading = NO;
}
从源码中很清楚的可以看到, 先调用call_class_loads(), 再调用call_category_loads(),这就说明了在调用所有的+load方法时,实现调用了所有类的+load方法,再去调用分类中的+load方法。
然后我们在进入到call_class_loads()函数中
static void call_class_loads(void)
{
int i;
// Detach current loadable list.
struct loadable_class *classes = loadable_classes;
int used = loadable_classes_used;
loadable_classes = nil;
loadable_classes_allocated = 0;
loadable_classes_used = 0;
// Call all +loads for the detached list.
for (i = 0; i < used; i++) {
Class cls = classes[i].cls;
load_method_t load_method = (load_method_t)classes[i].method;
if (!cls) continue;
if (PrintLoading) {
_objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
}
(*load_method)(cls, @selector(load));
}
// Destroy the detached list.
if (classes) free(classes);
}
从中间的循环中可以看出,是取到了每个类的+load函数的指针,直接通过指针调用了这个函数。 call_category_loads()函数中体现出来的Category的+load方法的调用,也是同理。
同时这也解答了我们的另一个疑惑,那就是为什么总是先调用类的+load,在调用Category的+load。
思考:如果存在继承的情况,+load又会是怎样的调用顺序呢?
从上面call_class_loads()函数中可以看到有一个list:loadable_classes,我们猜测这里面应该就是存放着我们所有的类,因为下面的循环是从0开始循环,所以我们要研究所有的类的+load方法的执行顺序,就要看这个list中的类的顺序是怎么样的。
我们从个源码中可以找到这样一个方法,prepare_load_methods,在其实现中调用了schedule_class_load方法,我们看一下schedule_class_load的源码
static void schedule_class_load(Class cls)
{
if (!cls) return;
ASSERT(cls->isRealized()); // _read_images should realize
if (cls->data()->flags & RW_LOADED) return;
// Ensure superclass-first ordering
schedule_class_load(cls->superclass);
add_class_to_loadable_list(cls);
cls->setInfo(RW_LOADED);
}
从源码中schedule_class_load(cls->superclass);这一句中可以看出,递归调用自己本身,并且传入自己的父类,结果递归之后,才调用add_class_to_loadable_list,这就说明父类总是在子类前面加入到list当中,所有在调用一个类的+load方法之前,肯定要先调用其父类的+load方法。
那如果是其他没有继承关系的类呢,这就跟编译顺序有关系了,大家可以自己尝试验证一下。
小结:
+load方法会在runtime加载类和分类时调用
每个类和分类的+load方法之后调用一次
调用顺序:先调用类的+load
+initialize
+initialize的调用是不同的,如果某一个类我们没有使用过,他的+initialize方法是不会调用的,到我们使用这个类(调用了类的某个方法)的时候,才会触发+initialize方法的调用。
@implementation Person
+ (void)initialize {
NSLog(@"Person - initialize");
}
@end
@implementation Person (Test1)
+ (void)initialize {
NSLog(@"Person Test1 - initialize");
}
@end
@implementation Person (Test2)
+ (void)initialize {
NSLog(@"Person Test2 - initialize");
}
@end
当我们执行[Person alloc];的时候,才会走+initialize方法,而且执行的Category中的+initialize:
2020-09-14 10:40:23.579623+0800 Category[9134:94173] Person Test2 - initialize
这个我们之前已经说过了,Category的方法会添加list的前面,所以会先被找到并且执行,所以我们猜测+initialize的执行是走的正常的消息机制,objc_msgSend。
由于objc_msgSend实现并没有完全开源,都是汇编代码,所以我们需要换一个思路来研究源码。
objc_msgSend本质是什么?以调用实例方法为例,其实就是通过isa指针找到该类,然后寻找方法,找到之后调用。如果没有找到则通过superClass找到父类,继续查找方法。上面的例子中,我们仅仅是调用了一个alloc方法,但是也执行了+initialize方法,所以我们猜测+initialize会在查找方法的时候调用到。通过这个思路,我们定位到了class_getInstanceMethod()函数(class_getInstanceMethod函数就是在类中查找某个sel时候调用的),在这个函数中,又调用了IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
在该函数中我们可以找到下面这段代码
if ((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized()) {
initializeNonMetaClass (_class_getNonMetaClass(cls, inst));
}
可以看出如果类还没有执行+initialize 就会先执行,我们再看一下if语句中的initializeNonMetaClass函数,他会先拿到superClass,执行superClass的+initialize
supercls = cls->superclass;
if (supercls && !supercls->isInitialized()) {
initializeNonMetaClass(supercls);
}
这就是存在继承的情况,为什么会先执行父类的+initialize。
大总结
调用方式:
load是根据函数地址直接调用
initialize是通过消息机制objc_msgSend调用调用时刻:
load是在runtime加载类和分类时调用(只会调用一次)
initialize是在类第一次收到消息时调用个,默认没有继承的情况下每个类只会initialize一次(父类的initialize可能会被执行多次)调用顺序
load
先调用类的load:先编译的类先调用,子类调用之前,先调用父类的
在调用Category的load:先编译的先调用initialize
先初始化父类
在初始化子类(初始化子类可能调用父类的initialize)
补充
上面总结的时候说到父类的initialize会被执行多次,什么情况下会被执行多次,为什么?举个例子:
@implementation Person
+ (void)initialize {
NSLog(@"Person - initialize");
}
@end
@implementation Student
@end
Student类继承Person类,并且只有父类Person中实现了+initialize,Student类中并没有实现
此时我们调用[Student alloc];, 会得到如下的打印。
2020-09-14 11:31:55.377569+0800 Category[11483:125034] Person - initialize
2020-09-14 11:31:55.377659+0800 Category[11483:125034] Person - initialize
Person的+initialize被执行了两次,但这两个意义是不同的,第一次执行是因为在调用子类的+initialize方法之前必须先执行父类的了+initialize,所以会打印一次。当要执行子类的+initialize时,通过消息机制,student类中并没有找到实现的+initialize的实现,所以要通过superClass指针去到父类中继续查找,因为父类中实现了+initialize,所以才会有了第二次的打印。
结尾
本文的篇幅略长,笔者按照自己的思路和想法写完了此文,陈述过程不一定那么调理和完善,大家在阅读过程中发现问题,可以留言交流。
感谢阅读。
转自:https://www.jianshu.com/p/141b04e376d4
收起阅读 »iOS --常见崩溃和防护(二)
接上一章。。。。。。。
三、NSNotification Crash
当一个对象添加了notification之后,如果dealloc的时候,仍然持有notification,就会出现NSNotification类型的crash。NSNotification类型的crash多产生于程序员写代码时候犯疏忽,在NSNotificationCenter添加一个对象为observer之后,忘记了在对象dealloc的时候移除它。
NSNotification Crash的防护原理很简单, 利用method swizzling hook NSObject的dealloc函数,再对象真正dealloc之前先调用一下:[[NSNotificationCenter defaultCenter] removeObserver:self],即可。
#import
/**
当一个对象添加了notification之后,如果dealloc的时候,仍然持有notification,就会出现NSNotification类型的crash。
iOS9之后专门针对于这种情况做了处理,所以在iOS9之后,即使开发者没有移除observer,Notification crash也不会再产生了
*/
NS_ASSUME_NONNULL_BEGIN
@interface NSObject (NSNotificationCrash)
+ (void)xz_enableNotificationProtector;
@end
NS_ASSUME_NONNULL_END
#import "NSObject+NSNotificationCrash.h"
#import "NSObject+XZSwizzle.h"
#import
static const char *isNSNotification = "isNSNotification";
@implementation NSObject (NSNotificationCrash)
+ (void)xz_enableNotificationProtector {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSObject *objc = [[NSObject alloc] init];
[objc xz_instanceSwizzleMethod:@selector(addObserver:selector:name:object:) replaceMethod:@selector(xz_addObserver:selector:name:object:)];
// 在ARC环境下不能显示的@selector dealloc。
[objc xz_instanceSwizzleMethod:NSSelectorFromString(@"dealloc") replaceMethod:NSSelectorFromString(@"xz_dealloc")];
});
}
- (void)xz_addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject {
// 添加标志位,在delloc中只有isNSNotification是YES,才会移除通知
[observer setIsNSNotification:YES];
[self xz_addObserver:observer selector:aSelector name:aName object:anObject];
}
- (void)setIsNSNotification:(BOOL)yesOrNo {
objc_setAssociatedObject(self, isNSNotification, @(yesOrNo), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (BOOL)isNSNotification {
NSNumber *number = objc_getAssociatedObject(self, isNSNotification);;
return [number boolValue];
}
/**
如果一个对象从来没有添加过通知,那就不要remove操作
*/
- (void)xz_dealloc
{
if ([self isNSNotification]) {
NSLog(@"CrashProtector: %@ is dealloc,but NSNotificationCenter Also exsit",self);
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
[self xz_dealloc];
}
@end
四、NSTimer Crash 防护
定义一个抽象类,NSTimer实例强引用抽象类,而在抽象类中,弱引用target,这样target和NSTimer之间的关系也就是弱引用了,意味着target可以自由的释放,从而解决了循环引用的问题。
具体方式:
1、定义一个抽象类,抽象类中弱引用target。
iOS --常见崩溃和防护(一)
iOS 的崩溃
我们常见的crash有哪些呢?
1.unrecognized selector crash (没找到对应的函数)
2.KVO crash :(KVO的被观察者dealloc时仍然注册着KVO导致的crash,添加KVO重复添加观察者或重复移除观察者 )
3.NSNotification crash:(当一个对象添加了notification之后,如果dealloc的时候,仍然持有notification)
4.NSTimer类型crash:(需要在合适的时机invalidate 定时器,否则就会由于定时器timer强引用target的关系导致 target不能被释放,造成内存泄露,甚至在定时任务触发时导致crash)
5.Container类型crash:(数组,字典,常见的越界,插入,nil)
6.野指针类型的crash
7.非主线程刷UI类型:(在非主线程刷UI将会导致app运行crash)……
如何防护crash
一、unrecognized selector crash
unrecognized selector类型的crash,通常是因为一个对象调用了一个不属于它方法的方法导致的。而我们可以从方法调用的过程中,寻找到避免程序崩溃的突破口。
方法调用的过程是哪样的呢?
方法调用的过程--调用实例方法
1.在对象的<缓存方法列表> 中去找要调用的方法,找到直接执行其实现。
2.对象的<缓存方法列表> 里没找到,就去<类的方法列表>里找,找到了就执行其实现。
3.还没找到,说明这个类自己没有了,就会通过isa去向其父类里执行1、2。
4.如果找到了根类还没找到,那么就是没有了,会转向一个拦截调用的方法,可以自己在拦截调用方法里面做一些处理。
5.如果没有在拦截调用里做处理,那么就会报错崩溃。
方法调用的过程--调用类方法
1.在类的<缓存方法列表> 中去找要调用的方法,找到直接执行其实现。
2.类的<缓存方法列表> 里没找到,就去里找,找到了就执行其实现。
3.还没找到,说明这个类自己没有了,就会通过isa去meta类的父类里执行1、2。
4.如果找到了根meta类还没找到,那么就是没有了,会转向一个拦截调用的方法,可以自己在拦截调用方法里面做一些处理。
5.如果没有在拦截调用里做处理,那么就会报错崩溃。
从上面的方法调用过程可以看出,在找不到调用的方法程序崩溃之前,我们可以通过重写NSObject方法进行拦截调用,阻止程序的crash。这里面就用到了消息的转发机制:
runtime提供了3种方式去补救:
1:调用resolveInstanceMethod给个机会让类添加这个实现这个函数
2:调用forwardingTargetForSelector让别的对象去执行这个函数
3:调用forwardInvocation(函数执行器)灵活的将目标函数以及其他形式执行。
第一步:为类动态的创建一个消息接受类。
第二步:为类动态为桩类添加对应的Selector,用一个通用的返回0的函数来实现该SEL的IMP
第三步:将消息直接转发到这个消息接受类类对象上。
解决方法:
1、创建一个消息接受类。(继承至NSObject)
当调用方法的消息转发给该类后,该类也没有这个方法,回调用resolveInstanceMethod:方法,在消息接受类中重写方法,返回YES,表明该消息已经处理,这样就不会崩溃了。
重写的resolveInstanceMethod:方法中一定要有动态添加方法的处理,不然会继续走消息转发的流程,从而造成死循环。
#import
@interface XZUnrecognizedSelectorSolveObject : NSObject
@property (nonatomic, weak) NSObject *objc;
@end
#Import "XZUnrecognizedSelectorSolveObject.h"
#import
@interface XZUnrecognizedSelectorSolveObject ()
@end
@implementation XZUnrecognizedSelectorSolveObject
+ (BOOL)resolveInstanceMethod:(SEL)sel {
// 如果没有动态添加方法的话,还会调用forwardingTargetForSelector:方法,从而造成死循环
class_addMethod([self class], sel, (IMP)addMethod, "v@:@");
return YES;
}
id addMethod(id self, SEL _cmd) {
NSLog(@"WOCrashProtector: unrecognized selector: %@", NSStringFromSelector(_cmd));
return 0;
}
@end
2、为NSObject添加分类,拦截NSObject的forwardingTargetForSelector:方法。
实现原理:在分类中自定义一个xz_forwardingTargetForSelector:方法,然后替换掉系统的forwardingTargetForSelector:方法
#import
NS_ASSUME_NONNULL_BEGIN
@interface NSObject (SelectorCrash)
+ (void)xz_enableSelectorProtector;
@end
NS_ASSUME_NONNULL_END
#import "NSObject+SelectorCrash.h"
#import
#import "NSObject+XZSwizzle.h"
#Import "XZUnrecognizedSelectorSolveObject.h"
@implementation NSObject (SelectorCrash)
+ (void)xz_enableSelectorProtector {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSObject *object = [[NSObject alloc] init];
[object xz_instanceSwizzleMethod:@selector(forwardingTargetForSelector:) replaceMethod:@selector(xz_forwardingTargetForSelector:)];
});
}
- (id)xz_forwardingTargetForSelector:(SEL)aSelector {
// 判断某个类是否有某个实例方法,有则返回YES,否则返回NO
if (class_respondsToSelector([self class], @selector(forwardInvocation:))) {
// 有forwardInvocation实例方法
IMP impOfNSObject = class_getMethodImplementation([NSObject class], @selector(forwardInvocation:));
IMP imp = class_getMethodImplementation([self class], @selector(forwardInvocation:));
if (imp != impOfNSObject) {
return nil;
}
}
// 新建桩类转发消息
XZUnrecognizedSelectorSolveObject *solveObject = [XZUnrecognizedSelectorSolveObject new];
solveObject.objc = self;
return solveObject;
}
@end
交换方法代码 如下:
#import
NS_ASSUME_NONNULL_BEGIN
@interface NSObject (XZSwizzle)
/**
对类方法进行拦截并替换
@param originalSelector 原有类方法
@param replaceSelector 自定义类替换方法
*/
+ (void)xz_classSwizzleMethod:(SEL _Nonnull)originalSelector replaceMethod:(SEL _Nonnull)replaceSelector;
/**
对类方法进行拦截并替换
@param kClass 具体的类
@param originalSelector 原有类方法
@param replaceSelector 自定义类替换方法
*/
+ (void)xz_classSwizzleMethodWithClass:(Class _Nonnull)kClass orginalMethod:(SEL _Nonnull)originalSelector replaceMethod:(SEL _Nonnull)replaceSelector;
/**
对实例方法进行拦截并替换
@param originalSelector 原有实例方法
@param replaceSelector 自定义实例替换方法
*/
- (void)xz_instanceSwizzleMethod:(SEL _Nonnull)originalSelector replaceMethod:(SEL _Nonnull)replaceSelector;
/**
对实例方法进行拦截并替换
@param kClass 具体的类
@param originalSelector 原有实例方法
@param replaceSelector 自定义实例替换方法
*/
- (void)xz_instanceSwizzleMethodWithClass:(Class _Nonnull)kClass orginalMethod:(SEL _Nonnull)originalSelector replaceMethod:(SEL _Nonnull)replaceSelector;
@end
NS_ASSUME_NONNULL_END
#import "NSObject+XZSwizzle.h"
#import
@implementation NSObject (XZSwizzle)
/**
对类方法进行拦截并替换
@param originalSelector 类原有方法
@param replaceSelector 自定义替换方法
*/
+ (void)xz_classSwizzleMethod:(SEL _Nonnull)originalSelector replaceMethod:(SEL _Nonnull)replaceSelector {
Class class = [self class];
[self xz_classSwizzleMethodWithClass:class orginalMethod:originalSelector replaceMethod:replaceSelector];
}
/**
对类方法进行拦截并替换
@param kClass 具体的类
@param originalSelector 原有类方法
@param replaceSelector 自定义类替换方法
*/
+ (void)xz_classSwizzleMethodWithClass:(Class _Nonnull)kClass orginalMethod:(SEL _Nonnull)originalSelector replaceMethod:(SEL _Nonnull)replaceSelector {
// Method中包含IMP函数指针,通过替换IMP,使SEL调用不同函数实现
Method originalMethod = class_getClassMethod(kClass, originalSelector);
Method replaceMethod = class_getClassMethod(kClass, replaceSelector);
// 获取MetaClass (交换、添加等类方法需要用metaClass)
Class metaClass = objc_getMetaClass(NSStringFromClass(kClass).UTF8String);
// class_addMethod:如果发现方法已经存在,会失败返回,也可以用来做检查用,我们这里是为了避免源方法没有实现的情况;如果方法没有存在,我们则先尝试添加被替换的方法的实现
BOOL didAddMethod = class_addMethod(metaClass, originalSelector, method_getImplementation(replaceMethod), method_getTypeEncoding(replaceMethod));
if (didAddMethod) {
// 添加成功(原方法未实现,为防止crash,需要将刚添加的原方法替换)
class_replaceMethod(metaClass, replaceSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
// 添加失败(原本就有原方法, 直接交换两个方法)
method_exchangeImplementations(originalMethod, replaceMethod);
}
}
/**
对实例方法进行拦截并替换
@param originalSelector 原有实例方法
@param replaceSelector 自定义实例替换方法
*/
- (void)xz_instanceSwizzleMethod:(SEL _Nonnull)originalSelector replaceMethod:(SEL _Nonnull)replaceSelector {
Class class = [self class];
[self xz_instanceSwizzleMethodWithClass:class orginalMethod:originalSelector replaceMethod:replaceSelector];
}
/**
对实例方法进行拦截并替换
@param kClass 具体的类
@param originalSelector 原有实例方法
@param replaceSelector 自定义实例替换方法
*/
- (void)xz_instanceSwizzleMethodWithClass:(Class _Nonnull)kClass orginalMethod:(SEL _Nonnull)originalSelector replaceMethod:(SEL _Nonnull)replaceSelector {
Method originalMethod = class_getInstanceMethod(kClass, originalSelector);
Method replaceMethod = class_getInstanceMethod(kClass, replaceSelector);
// class_addMethod:如果发现方法已经存在,会失败返回,也可以用来做检查用,我们这里是为了避免源方法没有实现的情况;如果方法没有存在,我们则先尝试添加被替换的方法的实现
BOOL didAddMethod = class_addMethod(kClass, originalSelector, method_getImplementation(replaceMethod), method_getTypeEncoding(replaceMethod));
if (didAddMethod) {
// 添加成功(原方法未实现,为防止crash,需要将刚添加的原方法替换)
class_replaceMethod(kClass, replaceSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
// 添加失败(原本就有原方法, 直接交换两个方法)
method_exchangeImplementations(originalMethod, replaceMethod);
}
}
@end
调用:
二、KVO Crash
一个被观察的对象上有若干个观察者,每个观察者又有若干条keypath。如果观察者和keypathx的数量一多,很容易不清楚被观察的对象整个KVO关系,导致被观察者在dealloc的时候,仍然残存着一些关系没有被注销,同时还会导致KVO注册者和移除观察者不匹配的情况发生。尤其是多线程的情况下,导致KVO重复添加观察者或者移除观察者的情况,这种类似的情况通常发生的比较隐蔽,很难从代码的层面上排查。
解决方法:
1:如果出现KVO重复添加观察或者移除观察者(KVO注册者不匹配的)情况,delegate,可以直接阻止这些非正常的操作。
2:被观察对象dealloc之前,可以通过delegate自动将与自己有关的KVO关系都注销掉,避免了KVO的被观察者dealloc时仍然注册着KVO导致的crash
具体方式:
#import
#import "XZKVOProxy.h"
NS_ASSUME_NONNULL_BEGIN
@interface NSObject (KVOCrash)
@property (nonatomic, strong) XZKVOProxy * _Nullable KVOProxy; // 自定义的kvo关系的代理
@end
NS_ASSUME_NONNULL_END
#import "NSObject+KVOCrash.h"
#import "XZKVOProxy.h"
#import
#pragma mark - NSObject + KVOCrash
static void *NSObjectKVOProxyKey = &NSObjectKVOProxyKey;
@implementation NSObject (KVOCrash)
- (XZKVOProxy *)KVOProxy {
id proxy = objc_getAssociatedObject(self, NSObjectKVOProxyKey);
if (nil == proxy) {
proxy = [XZKVOProxy kvoProxyWithObserver:self];
self.KVOProxy = proxy;
}
return proxy;
}
- (void)setKVOProxy:(XZKVOProxy *)proxy
{
objc_setAssociatedObject(self, NSObjectKVOProxyKey, proxy, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
2、在自定义代理类中建立一个map来维护KVO整个关系
#import
typedef void (^XZKVONitificationBlock)(id _Nullable observer, id _Nullable object, NSDictionary * _Nullable change);
/**
KVO配置类
用于存储KVO里面的相关设置参数
*/
@interface XZKVOInfo : NSObject
//- (instancetype _Nullable)initWithObserver:(id _Nonnull)object keyPath:(NSString * _Nullable)keyPath options:(NSKeyValueObservingOptions)options context:(void * _Nullable)context block:(XZKVONitificationBlock _Nonnull )block;
@end
NS_ASSUME_NONNULL_BEGIN
/**
KVO管理类
用于管理object添加和移除的消息,(通过Map进行KVO之间的关系)(字典应该也可以)
*/
@interface XZKVOProxy : NSObject
@property (nullable, nonatomic, weak, readonly) id observer;
+ (instancetype)kvoProxyWithObserver:(nullable id)observer;
- (void)xz_observer:(id _Nullable)object keyPath:(NSString * _Nullable)keyPath options:(NSKeyValueObservingOptions)options context:(void * _Nullable)context block:(XZKVONitificationBlock)block;
- (void)xz_unobserver:(id _Nullable)object keyPath:(NSString * _Nullable)keyPath;
- (void)xz_unobserver:(id _Nullable)object;
- (void)xz_unobserverAll;
@end
NS_ASSUME_NONNULL_END
#import "XZKVOProxy.h"
#import
@interface XZKVOInfo ()
{
@public
__weak id _object; // 观察对象
NSString *_keyPath;
NSKeyValueObservingOptions _options;
SEL _action;
void *_context;
XZKVONitificationBlock _block;
}
@end
@implementation XZKVOInfo
- (instancetype _Nullable)initWithObserver:(id _Nonnull)object
keyPath:(NSString * _Nullable)keyPath
options:(NSKeyValueObservingOptions)options
context:(void * _Nullable)context {
return [self initWithObserver:object keyPath:keyPath options:options block:NULL action:NULL context:context];
}
- (instancetype _Nullable)initWithObserver:(id _Nonnull)object
keyPath:(NSString * _Nullable)keyPath
options:(NSKeyValueObservingOptions)options
context:(void * _Nullable)context
block:(XZKVONitificationBlock)block {
return [self initWithObserver:object keyPath:keyPath options:options block:block action:NULL context:context];
}
- (instancetype _Nullable)initWithObserver:(id _Nonnull)object
keyPath:(NSString * _Nullable)keyPath
options:(NSKeyValueObservingOptions)options
block:(_Nullable XZKVONitificationBlock)block
action:(_Nullable SEL)action
context:(void * _Nullable)context {
if (self = [super init]) {
_object = object;
_block = block;
_keyPath = [keyPath copy];
_options = options;
_action = action;
_context = context;
}
return self;
}
@end
/**
此类用来管理混乱的KVO关系
让被观察对象持有一个KVO的delegate,所有和KVO相关的操作均通过delegate来进行管理,delegate通过建立一张map来维护KVO整个关系
好处:
不会crash如果出现KVO重复添加观察者或重复移除观察者(KVO注册观察者与移除观察者不匹配)的情况,delegate可以 1.直接阻止这些非正常的操作。
crash 2.被观察对象dealloc之前,可以通过delegate自动将与自己有关的KVO关系都注销掉,避免了KVO的被观察者dealloc时仍然注册着KVO导致的crash。
👇:
重复添加观察者不会crash,即不会走@catch
多次添加对同一个属性观察的观察者,系统方法内部会强应用这个观察者,同理即可remove该观察者同样次数。
*/
@interface XZKVOProxy ()
{
pthread_mutex_t _mutex;
NSMapTable *> *_objectInfoMap;///< map来维护KVO整个关系
}
@end
@implementation XZKVOProxy
+ (instancetype)kvoProxyWithObserver:(nullable id)observer {
return [[self alloc] initWithObserver:observer];
}
- (instancetype)initWithObserver:(nullable id)observer {
if (self = [super init]) {
_observer = observer;
_objectInfoMap = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPointerPersonality valueOptions:NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPointerPersonality capacity:0];
}
return self;
}
/**
加锁、解锁
*/
- (void)lock {
pthread_mutex_lock(&_mutex);
}
- (void)unlock {
pthread_mutex_unlock(&_mutex);
}
/**
添加、删除 观察者
*/
- (void)xz_observer:(id _Nullable)object keyPath:(NSString * _Nullable)keyPath options:(NSKeyValueObservingOptions)options context:(void * _Nullable)context block:(XZKVONitificationBlock)block {
// 断言
NSAssert(0 != keyPath.length && NULL != block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPath, block);
if (nil == object || 0 == keyPath.length || NULL == block) {
return;
}
// 将观察的信息转成info对象
// self即kvoProxy是观察者;object是被观察者
XZKVOInfo *info = [[XZKVOInfo alloc] initWithObserver:self keyPath:keyPath options:options context:context block:block];
if (info) {
// 将info以key-value的形式存储到map中。key是被观察对象;value是观察信息的集合。
// 加锁
[self lock];
NSMutableSet *infos = [_objectInfoMap objectForKey:object];
BOOL _isExisting = NO;
for (XZKVOInfo *existingInfo in infos) {
if ([existingInfo->_keyPath isEqualToString:info->_keyPath]) {
// 观察者已存在
_isExisting = YES;
break;
}
}
if (_isExisting == YES) {
// 解锁
[self unlock];
return;
}
// // check for info existence
// XZKVOInfo *existingInfo = [infos member:info];
// if (nil != existingInfo) {
// // observation info already exists; do not observe it again
//
// // 解锁
// [self unlock];
// return;
// }
// 不存在
if (infos == nil) {
// 创建set,并将set添加进Map里
infos = [NSMutableSet set];
[_objectInfoMap setObject:infos forKey:object];
}
// 将要添加的KVOInfo添加进set里面
[infos addObject:info];
// 解锁
[self unlock];
// 将 kvoProxy 作为观察者;添加观察者
[object addObserver:self forKeyPath:info->_keyPath options:info->_options context:info->_context];
}
}
- (void)xz_unobserver:(id _Nullable)object keyPath:(NSString * _Nullable)keyPath {
// 将观察的信息转成info对象
// self即kvoProxy是观察者;object是被观察者
XZKVOInfo *info = [[XZKVOInfo alloc] initWithObserver:self keyPath:keyPath options:0 context:nil];
// 加锁
[self lock];
// 从map中获取object对应的KVOInfo集合
NSMutableSet *infos = [_objectInfoMap objectForKey:object];
BOOL _isExisting = NO;
for (XZKVOInfo *existingInfo in infos) {
if ([existingInfo->_keyPath isEqualToString:info->_keyPath]) {
// 观察者已存在
_isExisting = YES;
info = existingInfo;
break;
}
}
if (_isExisting == YES) {
// 存在
[infos removeObject:info];
// remove no longer used infos
if (0 == infos.count) {
[_objectInfoMap removeObjectForKey:object];
}
// 解锁
[self unlock];
// 移除观察者
[object removeObserver:self forKeyPath:info->_keyPath context:info->_context];
} else {
// 解锁
[self unlock];
}
// XZKVOInfo *registeredInfo = [infos member:info];
//
// if (nil != registeredInfo) {
// [infos removeObject:registeredInfo];
//
// // remove no longer used infos
// if (0 == infos.count) {
// [_objectInfoMap removeObjectForKey:object];
// }
//
// // 解锁
// [self unlock];
//
//
// // 移除观察者
// [object removeObserver:self forKeyPath:registeredInfo->_keyPath context:registeredInfo->_context];
// } else {
// // 解锁
// [self unlock];
// }
}
- (void)xz_unobserver:(id _Nullable)object {
// 加锁
[self lock];
// 从map中获取object对应的KVOInfo集合
NSMutableSet *infos = [_objectInfoMap objectForKey:object];
[_objectInfoMap removeObjectForKey:object];
// 解锁
[self unlock];
// 批量移除观察者
for (XZKVOInfo *info in infos) {
// 移除观察者
[object removeObserver:self forKeyPath:info->_keyPath context:info->_context];
}
}
- (void)xz_unobserverAll {
if (_objectInfoMap) {
// 加锁
[self lock];
// copy一份map,防止删除数据异常冲突
NSMapTable *objectInfoMaps = [_objectInfoMap copy];
[_objectInfoMap removeAllObjects];
// 解锁
[self unlock];
// 移除全部观察者
for (id object in objectInfoMaps) {
NSSet *infos = [objectInfoMaps objectForKey:object];
if (!infos || infos.count == 0) {
continue;
}
for (XZKVOInfo *info in infos) {
[object removeObserver:self forKeyPath:info->_keyPath context:info->_context];
}
}
}
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
// NSAssert(context, @"missing context keyPath:%@ object:%@ change:%@", keyPath, object, change);
NSLog(@"%@",keyPath);
NSLog(@"%@",object);
NSLog(@"%@",change);
NSLog(@"%@",context);
// 从map中获取object对应的KVOInfo集合
NSMutableSet *infos = [_objectInfoMap objectForKey:object];
BOOL _isExisting = NO;
XZKVOInfo *info;
for (XZKVOInfo *existingInfo in infos) {
if ([existingInfo->_keyPath isEqualToString:keyPath]) {
// 观察者已存在
_isExisting = YES;
info = existingInfo;
break;
}
}
if (_isExisting == YES && info) {
XZKVOProxy *proxy = info->_object;
id observer = proxy.observer;
XZKVONitificationBlock block = info->_block;
if (block) {
block(observer, object, change);
}
}
}
- (void)dealloc {
// 移除所有观察者
[self xz_unobserverAll];
// 销毁mutex
pthread_mutex_destroy(&_mutex);
}
@end
iOS - 剖析性能优化相关
性能优化的几个点:
1.卡顿优化
在了解卡顿优化相关的前头,首先要了解 CPU 和 GPU。
CPU(Central Processing Unit,中央处理器)
对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制(Core Graphics)都是通过 CPU 来做的。
GPU(Graphics Processing Unit,图形处理器)
纹理的渲染、
所要显示的信息一般是通过 CPU 计算或者解码,经过 CPU 的数据交给 GPU 渲染,渲染的工作在帧缓存的地方完成,然后从帧缓存读取数据到视频控制器上,最终显示在屏幕上。
在 iOS 中有双缓存机制,有前帧缓存、后帧缓存,这样渲染的效率很高。
屏幕成像原理
卡顿成因
解决办法
按照 60fps 的刷帧率,每隔 16ms 就会有一次 VSync 信号产生。那么针对 CPU 和 GPU 有以下优化方案:
尽量用轻量级的对象 如:不用处理事件的 UI 控件可以考虑使用 CALayer; 不要频繁地调用 UIView
的相关属性 如:frame、bounds、transform 等;尽量提前计算好布局,在有需要的时候一次性调整对应属性,不要多次修改; Autolayout
会比直接设置 frame 消耗更多的 CPU 资源;图片的 size 和 UIImageView 的 size 保持一致; 控制线程的最大并发数量; 耗时操作放入子线程;如文本的尺寸计算、绘制,图片的解码、绘制等;
- 尽量避免短时间内大量图片显示;
- GPU 能处理的最大纹理尺寸是 4096 * 4096,超过这个尺寸就会占用 CPU 资源,所以纹理不能超过这个尺寸;
- 尽量减少透视图的数量和层次;
- 减少透明的视图(alpha < 1),不透明的就设置 opaque 为 YES;
- 尽量避免离屏渲染;
离屏渲染
在 OpenGL 中,GPU 有两种渲染方式:
On-Screen Rendering:当前屏幕渲染,在当前用于显示的屏幕缓冲区进行渲染操作;
Off-Screen Rendering:离屏渲染,在当前屏幕缓冲区外开辟新的缓冲区进行渲染操作;
离屏渲染消耗性能的原因:
离屏渲染的整个过程,需要多次切换上下文环境,先是从当前屏幕(On-Screen)切换到离屏(Off-Screen),渲染结束后,将离屏缓冲区的渲染结果显示到屏幕上,上下文环境从离屏切换到当前屏幕,这个过程会造成性能的消耗。
光栅化, layer.shouldRasterize = YES
遮罩, layer.mask
圆角,同时设置 layer.masksToBounds = YES
,layer.cornerRadius > 0
- 可以用 CoreGraphics 绘制裁剪圆角
阴影
- 如果设置了
layer.shadowPath
不会产生离屏渲染
卡顿检测
耗电优化
1.CPU 处理;
2.网络请求;
3.定位;
4.图像渲染;
优化思路
1.尽可能降低 CPU、GPU 功耗;
2.少用定时器;
3.优化 I/O 操作;
尽量不要频繁写入小数据,最好一次性批量写入;
读写大量重要数据时,可以用 dispatch_io,它提供了基于 GCD 的异步操作文件的 API,使用该 API 会优化磁盘访问;
数据量大时,用数据库管理数据;
4.网络优化;
减少、压缩网络数据(JSON 比 XML 文件性能更高); 若多次网络请求结果相同,尽量使用缓存; 使用断点续传,否则网络不稳定时可能多次传输相同的内容; 网络不可用时,不进行网络请求; 让用户可以取消长时间运行或者速度很慢的网络操作,设置合适的超时时间; 批量传输,如下载视频,不要传输很小的数据包,直接下载整个文件或者大块下载,然后慢慢展示;
5.定位优化
如果只是需要快速确定用户位置,用 CLLocationManager
的requestLocation
方法定位,定位完成后,定位硬件会自动断电;若不是导航应用,尽量不要实时更新位置,并为完毕就关掉定位服务; 尽量降低定位精度,如不要使用精度最高的 KCLLocationAccuracyBest
;需要后台定位时,尽量设置 pausesLocationUpdatesAutomatically
为 YES,若用户不怎么移动的时候,系统会自暂停位置更新;
启动优化
App 的启动分为两种:冷启动(Cold Launch) 和热启动(Warm Launch)。
前者表示从零开始启动 App,后者表示 App 已经存在内存中,在后台依然活着,再次点击图标启动 App。
App 启动的优化主要是针对冷启动的优化,通过添加环境变量可以打印出 App 的启动时间分析:Edit Scheme -> Run -> Arguments -> Environment Variables 添加 DYLD_PRINT_STATISTICS
设置为 1。
这里打印的是在执行 main
函数之前的耗时信息,若想打印更详细的信息则添加环境变量为:DYLD_PRINT_STATISTICS_DETAILS
设置为 1。
App 冷启动
冷启动可分为三个阶段:dyld 阶段、Runtime 阶段、main 阶段。
第一个阶段就是处理程序的镜像的阶段,第二个阶段是加载本程序的类、分类信息等等的 Runtime 阶段,最后是调用 main 函数阶段。
dyld
启动 App 时,dyld 会装载 App 的可执行文件,同时会递归加载所有依赖的动态库,当 dyld 把可执行文件、动态库都装载完毕后,会通知 Runtime 进行做下一步的处理。
Runtime
map_images
进行可执行文件的内容解析和处理,再 load_images
中调用 call_load_methods
调用所有 Class 和 Category 的 load
方法,然后进行 objc 结构的初始化(注册类、初始化类对象等)。然后调用 C++ 静态初始化器和 __attribute_((constructor))
修饰的函数,到此为止,可执行文件的和动态库中所有的符号(类、协议、方法等)都已经按照格式加载到内存中,被 Runtime 管理。main
在 Runtime 阶段完成后,dyld 会调用 main 函数,接下来是 UIApplication 函数,AppDelegate 的 application: didFinishLaunchingWithOptions:
函数。
启动优化思路
针对不同的阶段,有不同的优化思路:
dyld
1.减少动态库、合并动态库,定期清理不必要的动态库;
2.减少类、分类的数量,减少 Selector 的数量,定期清理不必要的类、分类;
3.减少 C++ 虚函数数量;
4.Swift 开发尽量使用 struct;
虚函数和 Java 中的抽象函数有点类似,但区别是,基类定义的虚函数,子类可以实现也可以不实现,而抽象函数子类一定要实现。
Runtime
用 inilialize 方法和 dispatch_once 取代所有的 __attribute_((constructor))、C++ 静态构造器、以及 Objective-C 中的 load 方法;
main
将一些耗时操作延迟执行,不要全部都放在 finishLaunching 方法中;
安装包瘦身
安装包(ipa)主要由可执行文件和资源文件组成,若不管理妥善则会造成安装包体积越来越大,所以针对资源优化我们可以将资源采取无损压缩,去除没用的资源。
对于可执行文件的瘦身,我们可以:
1.从编译器层面优化
1.Strip Linked Product、Make Strings Read-Only、Symbols Hidden by Default 设置为 YES
2.去掉异常支持,Enable C++ Exceptions、Enable Objective-C Exceptions 设置为 NO,Other C Flags 添加 -fno-exceptions;
3.利用 AppCode,检测未使用代码检测:菜单栏 -> Code -> Inspect Code;
4.编写 LLVM 插件检测重复代码、未调用代码;
5.通过生成 LinkMap 文件检测;
LinkMap
Build Setting -> LD_MAP_FILE_PATH: 设置文件路径 ,Build Setting -> LD_GENERSTE_MAP_FILE -> YES
运行程序可看到:
打开可看见各种信息:
我们可根据这个信息针对某个类进行优化。
摘自链接:https://www.jianshu.com/p/fe566ec32d28
iOS Universal Link(点击链接跳转到APP)
Universe Link跳转流程
步骤
1.登录苹果开发者中心 选择对应的appid ☑️勾选 Associated Domains 此处标记的Team ID 和 bundle ID 后面文件会用到
2. 用text 创建 apple-app-site-association 文件 去掉后缀!!!!!
3.打开xcode 工程 配置下图文件
4.在appdelegate 里面 回调接收url 获取链接里面的参数
5.最重要的一步来了!!!!!
用txt 把创建好的 apple-app-site-association 给后台 开发人员 将此文件 放到服务器的根目录下面 例如 https://www.baidu.com/apple-app-site-association
重点!!!!!!!! 必须用https
iOS--图形图像渲染原理
引言
作为程序员,我们或多或少知道可视化应用程序都是由 CPU 和 GPU 协作执行的。那么我们就先来了解一下两者的基本概念:
1.CPU(Central Processing Unit):现代计算机的三大核心部分之一,作为整个系统的运算和控制单元。CPU 内部的流水线结构使其拥有一定程度的并行计算能力。
2.GPU(Graphics Processing Unit):一种可进行绘图运算工作的专用微处理器。GPU 能够生成 2D/3D 的图形图像和视频,从而能够支持基于窗口的操作系统、图形用户界面、视频游戏、可视化图像应用和视频播放。GPU 具有非常强的并行计算能力。
这时候可能会产生一个问题:CPU 难道不能代替 GPU 来进行图形渲染吗?答案当然是肯定的,不过在看了下面这个视频就明白为什么要用 GPU 来进行图形渲染了。
GPU CPU 模拟绘图视频
使用 GPU 渲染图形的根本原因就是:速度。GPU 的并行计算能力使其能够快速将图形结果计算出来并在屏幕的所有像素中进行显示。
那么像素是如何绘制在屏幕上的?计算机将存储在内存中的形状转换成实际绘制在屏幕上的对应的过程称为 渲染。渲染过程中最常用的技术就是 光栅化。
关于光栅化的概念,以下图为例,假如有一道绿光与存储在内存中的一堆三角形中的某一个在三维空间坐标中存在相交的关系。那么这些处于相交位置的像素都会被绘制到屏幕上。当然这些三角形在三维空间中的前后关系也会以遮挡或部分遮挡的形式在屏幕上呈现出来。一句话总结:
光栅化就是将数据转化成可见像素的过程。
GPU 则是执行转换过程的硬件部件。由于这个过程涉及到屏幕上的每一个像素,所以 GPU 被设计成了一个高度并行化的硬件部件。
下面,我们来简单了解一下 GPU 的历史。
GPU 历史
GPU 还未出现前,PC 上的图形操作是由 视频图形阵列(VGA,Video Graphics Array) 控制器完成。VGA 控制器由连接到一定容量的DRAM上的存储控制器和显示产生器构成。
1997 年,VGA 控制器开始具备一些 3D 加速功能,包括用于 三角形生成、光栅化、纹理贴图 和 阴影。
2000 年,一个单片处图形处理器继承了传统高端工作站图形流水线的几乎每一个细节。因此诞生了一个新的术语 GPU 用来表示图形设备已经变成了一个处理器。
随着时间的推移,GPU 的可编程能力愈发强大,其作为可编程处理器取代了固定功能的专用逻辑,同时保持了基本的 3D 图形流水线组织。
近年来,GPU 增加了处理器指令和存储器硬件,以支持通用编程语言,并创立了一种编程环境,从而允许使用熟悉的语言(包括 C/C++)对 GPU 进行编程。
如今,GPU 及其相关驱动实现了图形处理中的 OpenGL 和 DirectX 模型,从而允许开发者能够轻易地操作硬件。OpenGL 严格来说并不是常规意义上的 API,而是一个第三方标准(由 khronos 组织制定并维护),其严格定义了每个函数该如何执行,以及它们的输出值。至于每个函数内部具体是如何实现的,则由 OpenGL 库的开发者自行决定。实际 OpenGL 库的开发者通常是显卡的生产商。DirectX 则是由 Microsoft 提供一套第三方标准。
GPU 图形渲染流水线
GPU 图形渲染流水线的主要工作可以被划分为两个部分:
把 3D 坐标转换为 2D 坐标
把 2D 坐标转变为实际的有颜色的像素
GPU 图形渲染流水线的具体实现可分为六个阶段,如下图所示。
顶点着色器(Vertex Shader)
形状装配(Shape Assembly),又称 图元装配
几何着色器(Geometry Shader)
光栅化(Rasterization)
片段着色器(Fragment Shader)
测试与混合(Tests and Blending)
第一阶段,顶点着色器。该阶段的输入是 顶点数据(Vertex Data) 数据,比如以数组的形式传递 3 个 3D 坐标用来表示一个三角形。顶点数据是一系列顶点的集合。顶点着色器主要的目的是把 3D 坐标转为另一种 3D 坐标,同时顶点着色器可以对顶点属性进行一些基本处理。
第二阶段,形状(图元)装配。该阶段将顶点着色器输出的所有顶点作为输入,并将所有的点装配成指定图元的形状。图中则是一个三角形。图元(Primitive) 用于表示如何渲染顶点数据,如:点、线、三角形。
第三阶段,几何着色器。该阶段把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状。例子中,它生成了另一个三角形。
第四阶段,光栅化。该阶段会把图元映射为最终屏幕上相应的像素,生成片段。片段(Fragment) 是渲染一个像素所需要的所有数据。
第五阶段,片段着色器。该阶段首先会对输入的片段进行 裁切(Clipping)。裁切会丢弃超出视图以外的所有像素,用来提升执行效率。
第六阶段,测试与混合。该阶段会检测片段的对应的深度值(z 坐标),判断这个像素位于其它物体的前面还是后面,决定是否应该丢弃。此外,该阶段还会检查 alpha 值( alpha 值定义了一个物体的透明度),从而对物体进行混合。因此,即使在片段着色器中计算出来了一个像素输出的颜色,在渲染多个三角形的时候最后的像素颜色也可能完全不同。
关于混合,GPU 采用如下公式进行计算,并得出最后的颜色。
R = S + D * (1 - Sa)
关于公式的含义,假设有两个像素 S(source) 和 D(destination),S 在 z 轴方向相对靠前(在上面),D 在 z 轴方向相对靠后(在下面),那么最终的颜色值就是 S(上面像素) 的颜色 + D(下面像素) 的颜色 * (1 - S(上面像素) 颜色的透明度)。
上述流水线以绘制一个三角形为进行介绍,可以为每个顶点添加颜色来增加图形的细节,从而创建图像。但是,如果让图形看上去更加真实,需要足够多的顶点和颜色,相应也会产生更大的开销。为了提高生产效率和执行效率,开发者经常会使用 纹理(Texture) 来表现细节。纹理是一个 2D 图片(甚至也有 1D 和 3D 的纹理)。纹理一般可以直接作为图形渲染流水线的第五阶段的输入。
最后,我们还需要知道上述阶段中的着色器事实上是一些程序,它们运行在 GPU 中成千上万的小处理器核中。这些着色器允许开发者进行配置,从而可以高效地控制图形渲染流水线中的特定部分。由于它们运行在 GPU 中,因此可以降低 CPU 的负荷。着色器可以使用多种语言编写,OpenGL 提供了 GLSL(OpenGL Shading Language) 着色器语言。
GPU 存储系统
早期的 GPU,不同的着色器对应有着不同的硬件单元。如今,GPU 流水线则使用一个统一的硬件来运行所有的着色器。此外,nVidia 还提出了 CUDA(Compute Unified Device Architecture) 编程模型,可以允许开发者通过编写 C 代码来访问 GPU 中所有的处理器核,从而深度挖掘 GPU 的并行计算能力。
下图所示为 GPU 内部的层级结构。最底层是计算机的系统内存,其次是 GPU 的内部存储,然后依次是两级 cache:L2 和 L1,每个 L1 cache 连接至一个 流处理器(SM,stream processor)。
SM L1 Cache 的存储容量大约为 16 至 64KB。
GPU L2 Cache 的存储容量大约为几百 KB。
GPU 的内存最大为 12GB。
GPU 上的各级存储系统与对应层级的计算机存储系统相比要小不少。
此外,GPU 内存并不具有一致性,也就意味着并不支持并发读取和并发写入。
GPU 流处理器
下图所示为 GPU 中每个流处理器的内部结构示意图。每个流处理器集成了一个 L1 Cache。顶部是处理器核共享的寄存器堆。
CPU-GPU 异构系统
至此,我们大致了解了 GPU 的工作原理和内部结构,那么实际应用中 CPU 和 GPU 又是如何协同工作的呢?
下图所示为两种常见的 CPU-GPU 异构架构。
左图是分离式的结构,CPU 和 GPU 拥有各自的存储系统,两者通过 PCI-e 总线进行连接。这种结构的缺点在于 PCI-e 相对于两者具有低带宽和高延迟,数据的传输成了其中的性能瓶颈。目前使用非常广泛,如PC、智能手机等。
右图是耦合式的结构,CPU 和 GPU 共享内存和缓存。AMD 的 APU 采用的就是这种结构,目前主要使用在游戏主机中,如 PS4。
注意,目前很多 SoC 都是集成了CPU 和 GPU,事实上这仅仅是在物理上进行了集成,并不意味着它们使用的就是耦合式结构,大多数采用的还是分离式结构。耦合式结构是在系统上进行了集成。
在存储管理方面,分离式结构中 CPU 和 GPU 各自拥有独立的内存,两者共享一套虚拟地址空间,必要时会进行内存拷贝。对于耦合式结构,GPU 没有独立的内存,与 GPU 共享系统内存,由 MMU 进行存储管理。
图形应用程序调用 OpenGL 或 Direct3D API 功能,将 GPU 作为协处理器使用。API 通过面向特殊 GPU 优化的图形设备驱动向 GPU 发送命令、程序、数据。
GPU 资源管理模型
下图所示为分离式异构系统中 GPU 的资源管理模型示意图。
MMIO(Memory-Mapped I/O)
CPU 通过 MMIO 访问 GPU 的寄存器状态。
通过 MMIO 传送数据块传输命令,支持 DMA 的硬件可以实现块数据传输。
GPU Context
上下文表示 GPU 的计算状态,在 GPU 中占据部分虚拟地址空间。多个活跃态下的上下文可以在 GPU 中并存。
CPU Channel
来自 CPU 操作 GPU 的命令存储在内存中,并提交至 GPU channel 硬件单元。
每个 GPU 上下文可拥有多个 GPU Channel。每个 GPU 上下文都包含 GPU channel 描述符(GPU 内存中的内存对象)。
每个 GPU Channel 描述符存储了channel 的配置,如:其所在的页表。
每个 GPU Channel 都有一个专用的命令缓冲区,该缓冲区分配在 GPU 内存中,通过 MMIO 对 CPU 可见。
GPU 页表
GPU 上下文使用 GPU 页表进行分配,该表将虚拟地址空间与其他地址空间隔离开来。
GPU 页表与 CPU 页表分离,其驻留在 GPU 内存中,物理地址位于 GPU 通道描述符中。
通过 GPU channel 提交的所有命令和程序都在对应的 GPU 虚拟地址空间中执行。
GPU 页表将 GPU 虚拟地址不仅转换为 GPU 设备物理地址,还转换为主机物理地址。这使得 GPU 页面表能够将 GPU 存储器和主存储器统一到统一的 GPU 虚拟地址空间中,从而构成一个完成的虚拟地址空间。
PFIFO Engine
PFIFO 是一个提交 GPU 命令的特殊引擎。
PFIFO 维护多个独立的命令队列,即 channel。
命令队列是带有 put 和 get 指针的环形缓冲器。
PFIFO 引擎会拦截多有对通道控制区域的访问以供执行。
GPU 驱动使用一个通道描述符来存储关联通道的设置。
BO
缓冲对象(Buffer Object)。一块内存,可以用来存储纹理,渲染对象,着色器代码等等。
CPU-GPU 工作流
下图所示为 CPU-GPU 异构系统的工作流,当 CPU 遇到图像处理的需求时,会调用 GPU 进行处理,主要流程可以分为以下四步:
1.将主存的处理数据复制到显存中
2.CPU 指令驱动 GPU
3.GPU 中的每个运算单元并行处理
4.GPU 将显存结果传回主存
屏幕图像显示原理
介绍屏幕图像显示的原理,需要先从 CRT 显示器原理说起,如下图所示。CRT 的电子枪从上到下逐行扫描,扫描完成后显示器就呈现一帧画面。然后电子枪回到初始位置进行下一次扫描。为了同步显示器的显示过程和系统的视频控制器,显示器会用硬件时钟产生一系列的定时信号。当电子枪换行进行扫描时,显示器会发出一个水平同步信号(horizonal synchronization),简称 HSync;而当一帧画面绘制完成后,电子枪回复到原位,准备画下一帧前,显示器会发出一个垂直同步信号(vertical synchronization),简称 VSync。显示器通常以固定频率进行刷新,这个刷新率就是 VSync 信号产生的频率。虽然现在的显示器基本都是液晶显示屏了,但其原理基本一致。
下图所示为常见的 CPU、GPU、显示器工作方式。CPU 计算好显示内容提交至 GPU,GPU 渲染完成后将渲染结果存入帧缓冲区,视频控制器会按照 VSync 信号逐帧读取帧缓冲区的数据,经过数据转换后最终由显示器进行显示。
最简单的情况下,帧缓冲区只有一个。此时,帧缓冲区的读取和刷新都都会有比较大的效率问题。为了解决效率问题,GPU 通常会引入两个缓冲区,即 双缓冲机制。在这种情况下,GPU 会预先渲染一帧放入一个缓冲区中,用于视频控制器的读取。当下一帧渲染完毕后,GPU 会直接把视频控制器的指针指向第二个缓冲器。
双缓冲虽然能解决效率问题,但会引入一个新的问题。当视频控制器还未读取完成时,即屏幕内容刚显示一半时,GPU 将新的一帧内容提交到帧缓冲区并把两个缓冲区进行交换后,视频控制器就会把新的一帧数据的下半段显示到屏幕上,造成画面撕裂现象,如下图:
为了解决这个问题,GPU 通常有一个机制叫做垂直同步(简写也是 V-Sync),当开启垂直同步后,GPU 会等待显示器的 VSync 信号发出后,才进行新的一帧渲染和缓冲区更新。这样能解决画面撕裂现象,也增加了画面流畅度,但需要消费更多的计算资源,也会带来部分延迟。
摘自:http://chuquan.me/2018/08/26/graphics-rending-principle-gpu
收起阅读 »iOS中的emoji表情处理
emoji在社交类APP很常用,比如发动态,圈子,还有回复评论,还有会话
后台在处理emoji的态度,直接就是不处理,所以我们需要对emoji包括中文,数字,还有特殊字符进行编码还有解码
//编码
NSString *uniStr = [NSString stringWithUTF8String:[_barrageText.text UTF8String]];
NSData *uniData = [uniStr dataUsingEncoding:NSNonLossyASCIIStringEncoding];
NSString *goodStr = [[NSString alloc] initWithData:uniData encoding:NSUTF8StringEncoding] ;
NSLog(@"---编码--->[%@]",goodStr);
//解码
const char *jsonString = [goodStr UTF8String]; // goodStr 服务器返回的 json
NSData *jsonData = [NSData dataWithBytes:jsonString length:strlen(jsonString)];
NSString *goodMsg1 = [[NSString alloc] initWithData:jsonData encoding:NSNonLossyASCIIStringEncoding];
NSLog(@"---解码--->[%@]",goodMsg1);
2017-05-15 10:16:17.858 DFRomwe[650:153981] ---编码--->[hello\ud83d\ude18\ud83d\ude18world\u4e16\u754chaha\ud83d\ude17]
2017-05-15 10:16:17.859 DFRomwe[650:153981] ---解码--->[hello😘😘world世界haha😗]
总想着事情就能这么轻松解决!!!
可是,然后,呵呵呵,你不去了解一下东西,还是不行的
果然,后台不作处理的情况下,如果返回JSON这就不行了,因为会默认带有转义字符: *** "\" *** 会导致下面这个情况:
//在这里以😀表情为例,😀的Unicode编码为U+1F604,UTF-16编码为:\ud83d\ude04
NSString * emojiUnicode = @"\U0001F604";
NSLog(@"emojiUnicode:%@",emojiUnicode);
//如果直接输入\ud83d\ude04会报错,加了转义后不会报错,但是会输出字符串\ud83d\ude04,而不是😀
NSString * emojiUTF16 = @"\\ud83d\\ude04";
NSLog(@"emojiUTF16:%@",emojiUTF16);
//转换
emojiUTF16 = [NSString stringWithCString:[emojiUTF16 cStringUsingEncoding:NSUTF8StringEncoding] encoding:NSNonLossyASCIIStringEncoding];
NSLog(@"emojiUnicode2:%@",emojiUTF16);
输出:
emojiUnicode:😄
emojiUnicode1:\ud83d\ude04
emojiUnicode2:😄
果断百度另外的方法
//解码
- (NSString *)decodeEmoji{
NSString *tepStr1 ;
if ([self containsString:@"\\u"]) {
tepStr1 = [self stringByReplacingOccurrencesOfString:@"\\u"withString:@"\U"];
}else{
tepStr1 = [self stringByReplacingOccurrencesOfString:@"\u"withString:@"\U"];
}
NSString *tepStr2 = [tepStr1 stringByReplacingOccurrencesOfString:@"""withString:@"\""];
NSString *tepStr3 = [[@""" stringByAppendingString:tepStr2]stringByAppendingString:@"""];
NSData *tepData = [tepStr3 dataUsingEncoding:NSUTF8StringEncoding];
NSString *axiba = [NSPropertyListSerialization propertyListWithData:tepData options:NSPropertyListMutableContainers format:NULL error:NULL];
return [axiba stringByReplacingOccurrencesOfString:@"\r\n"withString:@"\n"];
}
//编码
- (NSString *)encodeEmoji{
NSUInteger length = [self length];
NSMutableString *s = [NSMutableString stringWithCapacity:0];
for (int i = 0;i < length; i++){
unichar _char = [self characterAtIndex:i];
//判断是否为英文和数字
if (_char <= '9' && _char >='0'){
[s appendFormat:@"%@",[self substringWithRange:NSMakeRange(i,1)]];
}else if(_char >='a' && _char <= 'z'){
[s appendFormat:@"%@",[self substringWithRange:NSMakeRange(i,1)]];
}else if(_char >='A' && _char <= 'Z')
{
[s appendFormat:@"%@",[self substringWithRange:NSMakeRange(i,1)]];
}else{
[s appendFormat:@"\\"];
[s appendFormat:@"\\u%x",[self characterAtIndex:i]];
}
}
return s;
}
这是从JSON解码与编码,其实原理也很简单:
A :就是把多余的转义斜杠扔掉,
B :然后Unicode转utf-8;
C :然后utf-8转Unicode;
这里我写了一个NSString的一个分类:#import "NSString+Emoji.h"
还添加了一些方法:
//判断是否存在emoji表情:因为emoji表情室友Unicode编码区间的
+ (BOOL)stringContainsEmoji:(NSString *)string
{
__block BOOL returnValue = NO;
[string enumerateSubstringsInRange:NSMakeRange(0, [string length])
options:NSStringEnumerationByComposedCharacterSequences
usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) {
const unichar hs = [substring characterAtIndex:0];
if (0xd800 <= hs && hs <= 0xdbff) {
if (substring.length > 1) {
const unichar ls = [substring characterAtIndex:1];
const int uc = ((hs - 0xd800) * 0x400) + (ls - 0xdc00) + 0x10000;
if (0x1d000 <= uc && uc <= 0x1f77f) {
returnValue = YES;
}
}
} else if (substring.length > 1) {
const unichar ls = [substring characterAtIndex:1];
if (ls == 0x20e3) {
returnValue = YES;
}
} else {
if (0x2100 <= hs && hs <= 0x27ff) {
returnValue = YES;
} else if (0x2B05 <= hs && hs <= 0x2b07) {
returnValue = YES;
} else if (0x2934 <= hs && hs <= 0x2935) {
returnValue = YES;
} else if (0x3297 <= hs && hs <= 0x3299) {
returnValue = YES;
} else if (hs == 0xa9 || hs == 0xae || hs == 0x303d || hs == 0x3030 || hs == 0x2b55 || hs == 0x2b1c || hs == 0x2b1b || hs == 0x2b50) {
returnValue = YES;
}
}
}];
return returnValue;
}
//判断是否存在中文
//因为要保证之前的utf-8的数据也能显示
- (BOOL)includeChinese
{
for(int i=0; i< [self length];i++)
{
int a =[self characterAtIndex:i];
if( a >0x4e00&& a <0x9fff){
return YES;
}
}
return NO;
}
//判断是否以中文开头
- (BOOL)JudgeChineseFirst{
//是否以中文开头(unicode中文编码范围是0x4e00~0x9fa5)
int utfCode = 0;
void *buffer = &utfCode;
NSRange range = NSMakeRange(0, 1);
//判断是不是中文开头的,buffer->获取字符的字节数据 maxLength->buffer的最大长度 usedLength->实际写入的长度,不需要的话可以传递NULL encoding->字符编码常数,不同编码方式转换后的字节长是不一样的,这里我用了UTF16 Little-Endian,maxLength为2字节,如果使用Unicode,则需要4字节 options->编码转换的选项,有两个值,分别是NSStringEncodingConversionAllowLossy和NSStringEncodingConversionExternalRepresentation range->获取的字符串中的字符范围,这里设置的第一个字符 remainingRange->建议获取的范围,可以传递NULL
BOOL b = [self getBytes:buffer maxLength:2 usedLength:NULL encoding:NSUTF16LittleEndianStringEncoding options:NSStringEncodingConversionExternalRepresentation range:range remainingRange:NULL];
if (b && (utfCode >= 0x4e00 && utfCode <= 0x9fa5))
return YES;
else
return NO;
}
iOS .a与framework打包以及shell自动合并
静态库打包的流程:
.a打包
将提前准备的项目文件及项目资源导入到SDK制作工程中
添加New Header Phase
将制作静态库需要的.h文件添加到Project中,将静态库调用的头文件添加到Public中
静态库打包bundle文件>由于演示制作的静态库包含图片和xib文件,因此为了规范,我们需要把图片和xib文件添加到bundle中,如图添加给静态库添加bundle资源包
创建好之后,将图片和xib文件添加到Copy Bundle Resources中
由于.bundle文件属于macOX类型,所以我们需要改一些配置来适配iOS,如图所示
TARGETS ->选择bundle -> Build Settings ->Base SDK ->选择Latest iOS (iOS 11.2)
设置Build Setting 中的COMBINE_HIDPI_IMAEGS 为NO,否则Bundle中的图片就是tiff格式了。
作为资源包,仅仅需要编译就好,无需安装相关配置,设置Skip Install为YES,同样需要删除安装路径Installation Dirctory的值
到此为止bundle文件的设置完成
找到源文件路径,如下图所示,到此静态库制作完成,将.libStaticSDK.a和source.bundle和头文件StaticSDK.h导入到项目中即可使用
找到源文件路径
3、合并静态库真机和模拟器文件
我们在制作静态库的时候,编译会产两个.a文件,一个适用于模拟器的,一个是用于真机的,为了开发方便我们可以使用终端命令将.a文件进行合并lipo -create XXX/模拟器.a路径 XXX/真机.a路径 -output 合并后的文件名称.a
4、注意点,由于资源文件在Bundle文件中因此在使用时需注意,以下我举两个例子,一个是加载图片,一个是加载xib文件
对于使用了Cocoapod导入第三方的xcode工程来讲 需要在Podfile中 做如下修改 之后 pod install
需要同时对住工程target 和Framework的target 配置pod环境
2.build Setting 设置
选择工程文件>target第一项>Build Setting>搜索linking,然后几个需要设置的选项都显现出来,首先是Dead Code Stripping设置为NO,网上对此项的解释如下,大致意思是如果开启此项就会对代码中的”dead”、”unreachable”的代码过滤,不过这个开关是否关闭,似乎没有多大影响,不过为了完整还原framework中的代码,将此项关闭也未曾不可。
The resulting executable will not include any “dead” or unreachable code
然后将Link With Standard Libraries关闭,我想可能是为了避免重复链接
最后将Mach-O Type设为Static Library,framework可以是动态库也可以是静态库,对于系统的framework是动态库,而用户制作的framework只能是静态库。
开始将下图中的build Active Architecture only选项设为YES,导致其编译时只生成当前机器的框架,将其设置为NO后,发现用模拟器编译后生成的framework同时包含x86_64和i386架构。不过这个无所谓,我们之后会使用编译脚本,脚本会将所有的架构全包含
分别编译
show in finder 如下
Debug-iphoneos 为Debug模式下真机使用的
Debug-iphonesimulator 为Debug模式下模拟器使用的
Release -iphoneos 为Release模式下真机使用的
Release-iphonesimulator 为Release模式下模拟器使用的
下面的合并和.a一样操作
下面介绍自动shell脚本合并
1:生成脚本target
2.target设置
1.添加target依赖Target Dependencies 选中需要打包的framework + 选择New Run Script Phase 出现 Run Scirpt
2.设置脚本路径
可以在命令行里设置
也可以直接将脚本粘贴在这里
# 取得项目名字(get project name)
FMK_NAME=${PROJECT_NAME}
# 取得生成的静态库文件路径 (get framework path)
INSTALL_DIR=${SRCROOT}/Products/${FMK_NAME}.framework
# 设置真机和模拟器生成的静态库路径 (set devcie framework and simulator framework path)
WRK_DIR=build
DEVICE_DIR=${WRK_DIR}/Release-iphoneos/${FMK_NAME}.framework
SIMULATOR_DIR=${WRK_DIR}/Release-iphonesimulator/${FMK_NAME}.framework
# 模拟器和真机编译 (device and simulator build)
xcodebuild -configuration "Release" -target "${FMK_NAME}" -sdk iphoneos clean build
xcodebuild -configuration "Release" -target "${FMK_NAME}" -sdk iphonesimulator clean build
# 删除临时文件 (delete temp file)
if [ -d "${INSTALL_DIR}" ]
then
rm -rf "${INSTALL_DIR}"
fi
mkdir -p "${INSTALL_DIR}"
# 拷贝真机framework文件到生成路径下 (copy device file to product path)
cp -R "${DEVICE_DIR}/" "${INSTALL_DIR}/"
# 合并生成,替换真机framework里面的二进制文件,并且打开 (merger and open)
lipo -create "${DEVICE_DIR}/${FMK_NAME}" "${SIMULATOR_DIR}/${FMK_NAME}" -output "${INSTALL_DIR}/${FMK_NAME}"
echo "${DEVICE_DIR}/${FMK_NAME}"
echo "${SIMULATOR_DIR}/${FMK_NAME}"
rm -rf "${WRK_DIR}"
open "${INSTALL_DIR}"
摘自作者:Cooci
原贴链接:https://www.jianshu.com/p/bf1cc6ac7d17
腾讯iOS面试题一分析
网络相关:
1. 项目使用过哪些网络库?用过ASIHttp库嘛
AFNetworking、ASIHttpRequest、Alamofire(swift)
1、AFN的底层实现基于OC的NSURLConnection和NSURLSession
2、ASI的底层实现基于纯C语言的CFNetwork框架
3、因为NSURLConnection和NSURLSession是在CFNetwork之上的一层封装,因此ASI的运行性能高于AFN
2. 断点续传怎么实现的?
需要怎么设置断点续传就是从文件上次中断的地方开始重新下载或上传数据。要实现断点续传 , 服务器必须支持(这个很重要,一个巴掌是拍不响的,如果服务器不支持,那么客户端写的再好也没用)。总结:断点续传主要依赖于 HTTP 头部定义的 Range 来完成。有了 Range,应用可以通过 HTTP 请求获取失败的资源,从而来恢复下载该资源。当然并不是所有的服务器都支持 Range,但大多数服务器是可以的。Range 是以字节计算的,请求的时候不必给出结尾字节数,因为请求方并不一定知道资源的大小。
// 1 指定下载文件地址 URLString
// 2 获取保存的文件路径 filePath
// 3 创建 NSURLRequest
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:URLString]];
unsigned long long downloadedBytes = 0;
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
// 3.1 若之前下载过 , 则在 HTTP 请求头部加入 Range
// 获取已下载文件的 size
downloadedBytes = [self fileSizeForPath:filePath];
// 验证是否下载过文件
if (downloadedBytes > 0) {
// 若下载过 , 断点续传的时候修改 HTTP 头部部分的 Range
NSMutableURLRequest *mutableURLRequest = [request mutableCopy];
NSString *requestRange =
[NSString stringWithFormat:@"bytes=%", downloadedBytes];
[mutableURLRequest setValue:requestRange forHTTPHeaderField:@"Range"];
request = mutableURLRequest;
}
}
// 4 创建 AFHTTPRequestOperation
AFHTTPRequestOperation *operation
= [[AFHTTPRequestOperation alloc] initWithRequest:request];
// 5 设置操作输出流 , 保存在第 2 步的文件中
operation.outputStream = [NSOutputStream
outputStreamToFileAtPath:filePath append:YES];
// 6 设置下载进度处理 block
[operation setDownloadProgressBlock:^(NSUInteger bytesRead,
long long totalBytesRead, long long totalBytesExpectedToRead) {
// bytesRead 当前读取的字节数
// totalBytesRead 读取的总字节数 , 包含断点续传之前的
// totalBytesExpectedToRead 文件总大小
}];
// 7 设置 success 和 failure 处理 block
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation
*operation, id responseObject) {
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
}];
// 8 启动 operation
[operation start];
3. HTTP请求 什么时候用post、get、put ?GET方法:对这个资源的查操作
GET参数通过URL传递,POST放在Request body中。
GET请求会被浏览器主动cache,而POST不会,除非手动设置
GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。
Get 请求中有非 ASCII 字符,会在请求之前进行转码,POST不用,因为POST在Request body中,通过 MIME,也就可以传输非 ASCII 字符。
一般我们在浏览器输入一个网址访问网站都是GET请求
HTTP的底层是TCP/IP。HTTP只是个行为准则,而TCP才是GET和POST怎么实现的基本。GET/POST都是TCP链接。GET和POST能做的事情是一样一样的。但是请求的数据量太大对浏览器和服务器都是很大负担。所以业界有了不成文规定,(大多数)浏览器通常都会限制url长度在2K个字节,而(大多数)服务器最多处理64K大小的url。
GET产生一个TCP数据包;POST产生两个TCP数据包。对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。
在网络环境好的情况下,发一次包的时间和发两次包的时间差别基本可以无视。而在网络环境差的情况下,两次包的TCP在验证数据包完整性上,有非常大的优点。但并不是所有浏览器都会在POST中发送两次包,Firefox就只发送一次。
PUT和POS都有更改指定URI的语义.但PUT被定义为idempotent的方法,POST则不是.idempotent的方法:如果一个方法重复执行
多次,产生的效果是一样的,那就是idempotent的。也就是说:
PUT请求:如果两个请求相同,后一个请求会把第一个请求覆盖掉。(所以PUT用来改资源)
Post请求:后一个请求不会把第一个请求覆盖掉。(所以Post用来增资源)
4. HTTP建立断开连接的时候为什么要 三次握手、四次挥手?
因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。
client请求连接,Serve发送确认连接,client回复确认连接 ==>连接建立
但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。
注意:
client两个等待,FIN_Wait 和 Time_WaitTIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态>。虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。
client请求断开,Server收到断开请求,server发送断开,client回复断开确认 ==>连接断
5. 项目中的数据存储都有哪些,iOS中有哪些数据存储方法,什么时候用?
文件
NSUserDefaults
数据库4、KeyChain5、iCloud
文件
沙盒
Plist
NSKeyedArchiver归档 / NSKeyedUnarchiver解档
NSUserDefaults
数据库
SQLite3
FMDB
Core Data
6、MVVM如何实现绑定?
MVVM 的实现可以采用KVO进行数据绑定,也可以采用RAC。其实还可以采用block、代理(protocol)实现。
MVVM比起MVC最大的好处就是可以实现自动绑定,将数据绑定在UI组件上,当UI中的值发生变化时,那么它对应的模型中也跟随着发生变化,这就是双向绑定机制,原因在于它在视图层和数据模型层之间实现了一个绑定器,绑定器可以管理两个值,它一直监听组件UI的值,只要发生变化,它将会把值传输过去改变model中的值。绑定器比较灵活,还可以实现单向绑定。
实际开发中的做法:
让Controller拥有View和ViewModel属性,VM拥有Model属性;Controller或者View来接收ViewModel发送的Model改变的通知
用户的操作点击或者Controller的视图生命周期里面让ViewModel去执行请求,请求完成后ViewModel将返回数据模型化并保存,从而更新了Model;Controller和View是属于V部分,即实现V改变M(V绑定M)。如果不需要请求,这直接修改Model就是了。
第2步中的Model的改变,VM是知道的(因为持有关系),只需要Model改变后发一个通知;Controller或View接收到通知后(一般是Controller先接收再赋值给View),根据这个新Model去改变视图就完成了M改变V(M绑定V) 。使用RAC(RactiveCocoa)框架实现绑定可以简单到一句话概括:ViewModel中创建好请求的信号RACSignal, Controller中订阅这个信号,在ViewModel完成请求后订阅者调用sendNext:方法,Controller里面订阅时写的block就收到回调了。
7、block 和 通知的区别
通知:
一对多
Block:
通常拿来OC中的block和swift中的闭包来比较.
block注重的是过程
block会开辟内存,消耗比较大,delegate则不会
block防止循环引用,要用弱引用
Delegate:
代理注重的是过程,是一对一的,对于一个协议就只能用一个代理,更适用于多个回调方法(3个以上),block则适用于1,2个回调时
8、进程间通信方式?线程间通信?
URL scheme
这个是iOS APP通信最常用到的通信方式,APP1通过openURL的方法跳转到APP2,并且在URL中带上想要的参数,有点类似HTTP的get请求那样进行参数传递。这种方式是使用最多的最常见的,使用方法也很简单只需要源APP1在info.plist中配置LSApplicationQueriesSchemes,指定目标App2的scheme;然后再目标App2的info.plist 中配置好URLtypes,表示该App接受何种URL scheme的唤起。Keychain
iOS 系统的keychain是一个安全的存储容器,它本质上就是一个sqlite数据库,它的位置存储在/private/var/Keychains/keychain-2.db,不过它索八坪村的所有数据都是经过加密的,可以用来为不同的APP保存敏感信息,比如用户名,密码等。iOS系统自己也用keychain来保存VPN凭证和WiFi密码。它是独立于每个APP的沙盒之外的,所以即使APP被删除之后,keychain里面的信息依然存在
10、UIPasteBoard
是剪切板功能,因为iOS 的原生空间UItextView,UItextfield,UIwebView ,我们在使用时如果长按,就回出现复制、剪切、选中、全选、粘贴等功能,这个就是利用系统剪切板功能来实现的。
11、UIDocumentInteractionController
uidocumentinteractioncontroller 主要是用来实现同设备上APP之间的贡献文档,以及文档预览、打印、发邮件和复制等功能。
12、Local socket
原理:一个APP1在本地的端口port1234 进行TCP的bind 和 listen,另外一个APP2在同一个端口port1234发起TCP的connect连接,这样就可以简历正常的TCP连接,进行TCP通信了,然后想传什么数据就可以传什么数据了、
13、AirDrop
通过 Airdrop实现不同设备的APP之间文档和数据的分享
14、UIActivityViewController
iOS SDK 中封装好的类在APP之间发送数据、分享数据和操作数据
15、APP Groups
APP group用于同一个开发团队开发的APP之间,包括APP和extension之间共享同一份读写空间,进行数据共享。同一个团队开发的多个应用之间如果能直接数据共享,大大提高用户体验
线程间通信的体现
1 .一个线程传递数据给另一个线程
2 .在一个线程中执行完特定任务后,转到另一个线程继续执行任务复制代码线程间通信常用的方法
1、NSThread可以先将自己的当前线程对象注册到某个全局的对象中去,这样相互之间就可以获取对方的线程对象,然后就可以使用下面的方法进行线程间的通信了,由于主线程比较特殊,所以框架直接提供了在主线程执行的方法>
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg
waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
2、 GCD一个线程传递数据给另一个线程,如
{ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"donwload---%@", [NSThread currentThread]);
// 1.子线程下载图片 //耗时操作
NSURL *url = [NSURL URLWithString:@"http://d.jpg"];
NSData *data = [NSData dataWithContentsOfURL:url];
UIImage *image = [UIImage imageWithData:data];
// 2.回到主线程设置图片
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"setting---%@ %@", [NSThread currentThread], image);
[self.button setImage:image forState:UIControlStateNormal];
});
});
16、如何检测应用卡顿问题?
NSRunLoop调用方法主要就是在kCFRunLoopBeforeSources和kCFRunLoopBeforeWaiting之间,还有kCFRunLoopAfterWaiting之后,也就是如果我们发现这两个时间内耗时太长,那么就可以判定出此时主线程卡顿。
链接:https://www.jianshu.com/p/7484830d9d74
iOS 头条一面 面试题
1、如何高效的切圆角?
切圆角共有以下三种方案:
cornerRadius + masksToBounds:适用于单个视图或视图不在列表上且量级较小的情况,会导致离屏渲染。
CAShapeLayer+UIBezierPath:会导致离屏渲染,性能消耗严重,不推荐使用。
Core Graphics:不会导致离屏渲染,推荐使用。
2、什么是隐式动画和显式动画?
隐式动画指的是改变属性值而产生的默认的过渡动画(如background、cornerRadius等),不需要初始化任何类,系统自己处理的动画属性;显式动画是指自己创建一个动画对象并附加到layer上,如 CAAnimation、CABasicAnimation、CAKeyframeAnimation。
3、UIView 和 CALayer 的区别?
UIView 是 CALayer 的 delegate,UIView 可以响应事件,而 CALayer 则不能。
4、离屏渲染?
iOS 在不进行预合成的情况下不会直接在屏幕上绘制该图层,这意味着 CPU 和 GPU 必须先准备好屏幕外上下文,然后才能在屏幕上渲染,这会造成更多时间时间和更多的内存的消耗。
5、Objective - C 是否支持方法重载(overloading)?
不支持。方法重载(overloading):允许创建多项名称相同但输入输出类型或个数不同的方法。
// 这两个方法名字是不一样的,虽然都是writeToFile开头
-(void) writeToFile:(NSString *)path fromInt:(int)anInt;
-(void) writeToFile:(NSString *)path fromString:(NSString *)aString;
注:Swift 是支持的。
func testFunc() {}
func testFunc(num: Int) {}
6、KVC 的应用场景及注意事项
KVC(key-Value coding) 键值编码,指iOS开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。不需要调用明确的存取方法,这样就可以在运行时动态访问和修改对象的属性,而不是在编译时确定。
它的四个主要方法:
- (nullable id)valueForKey:(NSString *)key; //直接通过Key来取值
- (void)setValue:(nullable id)value forKey:(NSString *)key; //通过Key来设值
- (nullable id)valueForKeyPath:(NSString *)keyPath; //通过KeyPath来取值
- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; //通过KeyPath来设值
应用场景:
动态取值和设值
访问和改变私有变量
修改控件的内部属性
注意事项:
key 不要传 nil,会导致崩溃,可以通过重写setNilValueForKey:来避免。
传入不存在的 key 也会导致崩溃,可以通过重写valueForUndefinedKey:来避免。
7、如何异步下载多张小图最后合成一张大图?
使用Dispatch Group追加block到Global Group Queue,这些block如果全部执行完毕,就会执行Main Dispatch Queue中的结束处理的block。
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, queue, ^{ /*加载图片1 */ });
dispatch_group_async(group, queue, ^{ /*加载图片2 */ });
dispatch_group_async(group, queue, ^{ /*加载图片3 */ });
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 合并图片
});
8、NSTimer 有什么注意事项?在 dealloc 中调用[timer invalidate];会避免循环引用吗?
时间延后。如果 timer 处于耗时较长的 runloop 中,或者当前 runloop 处于不监视 timer 的 mode 时(如 scrollView 滑动时)。它在下次 runloop 才会触发,所以可能会导致比预期时间要晚。
循环引用。target 强引用 timer,timer 强引用 target。
时间延后
使用 dispatch_source_t来提高时间精度。
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
if (timer) {
dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, interval * NSEC_PER_SEC), interval * NSEC_PER_SEC, (1ull * NSEC_PER_SEC) / 10);
dispatch_source_set_event_handler(timer, block);
dispatch_resume(timer);
}
循环引用
在 dealloc 中调用 [timer invalidate];不会避免循环引用。因为 timer 会对 target 进行强引用,所以在 timer 没被释放之前,根本不会走 target 的 dealloc 方法。
可以通过以下几种方法来避免:
如果 iOS 10 及以上,可以使用nit(timeInterval:repeats:block:)。target 不再强引用 timer。记得在 dealloc 中调用 [timer invalidate];,否则会造成内存泄漏。
timer = Timer(timeInterval: 1.0, repeats: true, block: { [weak self] (timer) in
self?.timerFunc()
})
使用中间件的方式来避免循环引用。
// 定义
@implementation WeakTimerTarget
{
__weak target;
SEL selector;
}
- (void)timerDidFire:(NSTimer *)timer {
if(target) {
[target performSelector:selector withObject:timer];
} else{
[timer invalidate];
}
}
@end
// 使用
WeakTimerTarget *target = [[WeakTimerTarget alloc] initWithTarget:self selector:@selector(tick)];
timer = [NSTimer scheduledTimerWithTimeInterval:30.0 target:target selector:@selector(timerDidFire:) ...];
9、对 property 的理解
@property = ivar + getter + setter;
10、Notification 的注意事项
在哪个线程发送通知,就在哪个线程接受通知。
11、Runloop的理解
一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出,通常的代码逻辑 是这样的:
function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}
12、对 OC 中 Class 的源码理解?其中 cache 的理解?
Class 的底层用 struct 实现,源码如下:
struct _class_t {
struct _class_t *isa;
struct _class_t *superclass;
void *cache;
void *vtable;
struct _class_ro_t *ro;
};
Cache用于缓存最近使用的方法。一个类只有一部分方法是常用的,每次调用一个方法之后,这个方法就被缓存到cache中,下次调用时 runtime 会先在 cache 中查找,如果 cache 中没有,才会去 methodList 中查找。以此提升性能。
13、项目优化做了哪些方面?
删除无用资源文件及代码
在合适的地方加缓存
耗时长的代码异步执行
14、如何一劳永逸的检测包的裂变(检测包的大小)?
这个不知道,希望了解的朋友可以在评论区指出来。
15、实现一个判断 IP 地址是否合法的方法
func isIPAddress(str: String) -> Bool {
guard !str.isEmpty else { return false }
var isIPAddress = false
let coms = str.components(separatedBy: ".")
for com in coms {
if let intCom = Int(com), intCom >= 0, intCom <= 255 {
isIPAddress = true
} else {
isIPAddress = false
return isIPAddress
}
}
return isIPAddress
}
转自:https://www.jianshu.com/p/62c525efe496
收起阅读 »iOS底层-isa
分析消息的走态
Root class (class)其实就是NSObject,NSObject是没有超类的,所以Root class(class)的superclass指向nil。
每个Class都有一个isa指针指向唯一的Meta class
Root class(meta)的superclass指向Root class(class),也就是NSObject,形成一个回路。
每个Meta class的isa指针都指向Root class (meta)。
Root class (meta)的isa指针都指向自己
这里我记录一个重要的点:
1.对象方法存在类里面
2.类方法存在元类里面
3.元类的方法存在根元类
这是非常重要的,如果我们没有捋清楚,就无法得知我们的消息接受者!!!
isa 又是什么?
所谓isa指针,在OC中对象的声明是这样的
typedef struct objc_object {
Class isa;
} *id;
对象本身是一个带有指向其类别isa指针的结构体。
当向一个对象发送消息的时候,实际上是通过isa在对象的类别中找到相应的方法。我们知道OC中除了实例方法之外还有类方法,那么类别是否也是个对象呢?
typedef struct objc_class *Class;
struct objc_class {
Class isa;
Class super_class;
/* followed by runtime specific details... */
};
从上面类别的结构看来,类别也是一个对象,它拥有一个指向其父类的指针,和一个isa指针。当一个类别使用类方法时,类别作为一个对象同样会使用isa指针找到类方法的实现。这时,isa指向的就是这个类别的元类。
也就是说
元类是类别的类。
所有的类方法都储存在元类当中。
众所周知Objective-C(以下简称OC)中的消息机制。消息的接收者可以是一个对象,也可以是一个类。那么这两种情况要是统一为一种情况不是更方便吗?苹果当然早就想到了,这也正是元类的用处。苹果统一把消息接收者作为对象。等等,这是说,类也是对象?yes,就是这样。就是说,OC中所有的类都一种对象。由一个类实例化来的对象叫实例对象,这好理解,那么,类作为对象(称之为类对象),又是什么类的对象?当然也容易猜到,就是今天的主题——元类(Metaclass)。现在到给元类下定义的时候了:元类就是类对象所属的类。所以,实例对象是类的实例,类作为对象又是元类的实例。已经说了,OC中所有的类都一种对象,所以元类也是对象,那么元类是什么的实例呢?答曰:根元类,根元类是其自身的实例
摘自作者:Cooc
原贴链接:https://www.jianshu.com/p/2d1fdb76ed57
收起阅读 »
iOS面试必背的算法面试题
1、实现二分查找算法
int binarySearchWithoutRecursion(int array[], int low, int high, int target) {
while (low <= high) {
int mid = low + (high - low) / 2;
if (array[mid] > target) {
high = mid - 1;
} else if (array[mid] < target) {
low = mid + 1;
} else {
//找到目标
return mid;
}
}
return -1;
}
递归实现
int binarySearch(const int arr[], int low, int high, int target)
{
int mid = low + (high - low) / 2;
if(low > high) {
return -1;
} else{
if(arr[mid] == target) {
return mid;
} else if(arr[mid] > target) {
return binarySearch(arr, low, mid-1, target);
} else {
return binarySearch(arr, mid+1, high, target);
}
}
}
2、 对以下一组数据进行降序排序(冒泡排序)。“24,17,85,13,9,54,76,45,5,63”
int main(int argc, char *argv[]) {
int array[10] = {24, 17, 85, 13, 9, 54, 76, 45, 5, 63};
int num = sizeof(array)/sizeof(int);
for(int i = 0; i < num - 1; i++) {
int exchanged = 0;
for(int j = 0; j < num - 1 - i; j++) {
if(array[j] < array[j+1]) {
array[j] = array[j]^array[j+1];
array[j+1] = array[j+1]^array[j];
array[j] = array[j]^array[j+1];
exchanged = 1;
}
}
if (exchanged == 0) {
break;
}
}
for(int i = 0; i < num; i++) {
printf("%d ", array[i]);
}
}
3、 对以下一组数据进行升序排序(选择排序)。“86, 37, 56, 29, 92, 73, 15, 63, 30, 8”
void sort(int a[],int n)
{
int i, j, min;
for(i = 0; i < n - 1; i++) {
min = i;
for(j = i + 1; j < n; j++) {
if(a[min] > a[j]) {
min = j;
}
}
if(min != i) {
a[i] = a[i] ^ a[min];
a[min] = a[min] ^ a[i];
a[i] = a[i] ^ a[min];
}
}
}
int main(int argc, const char * argv[]) {
int numArr[10] = {86, 37, 56, 29, 92, 73, 15, 63, 30, 8};
sort(numArr, 10);
for (int i = 0; i < 10; i++) {
printf("%d, ", numArr[i]);
}
return 0;
}
4、 快速排序算法
void sort(int *a, int left, int right) {
if(left >= right) {
return ;
}
int i = left;
int j = right;
int key = a[left];
while (i < j) {
while (i < j && key >= a[j]) {
j--;
}
if (i < j) {
a[i] = a[j];
}
while (i < j && key < a[i]) {
i++;
}
if (i < j) {
a[j] = a[i];
}
}
a[i] = key;
sort(a, left, i-1);
sort(a, i+1, right);
}
5、 归并排序
void merge(int sourceArr[], int tempArr[], int startIndex, int midIndex, int endIndex) {
int i = startIndex;
int j = midIndex + 1;
int k = startIndex;
while (i != midIndex + 1 && j != endIndex + 1) {
if (sourceArr[i] >= sourceArr[j]) {
tempArr[k++] = sourceArr[j++];
} else {
tempArr[k++] = sourceArr[i++];
}
}
while (i != midIndex + 1) {
tempArr[k++] = sourceArr[i++];
}
while (j != endIndex + 1) {
tempArr[k++] = sourceArr[j++];
}
for (i = startIndex; i <= endIndex; i++) {
sourceArr[i] = tempArr[i];
}
}
void sort(int souceArr[], int tempArr[], int startIndex, int endIndex) {
int midIndex;
if (startIndex < endIndex) {
midIndex = (startIndex + endIndex) / 2;
sort(souceArr, tempArr, startIndex, midIndex);
sort(souceArr, tempArr, midIndex + 1, endIndex);
merge(souceArr, tempArr, startIndex, midIndex, endIndex);
}
}
int main(int argc, const char * argv[]) {
int numArr[10] = {86, 37, 56, 29, 92, 73, 15, 63, 30, 8};
int tempArr[10];
sort(numArr, tempArr, 0, 9);
for (int i = 0; i < 10; i++) {
printf("%d, ", numArr[i]);
}
return 0;
}
6、 二叉树的先序遍历为FBACDEGH,中序遍历为:ABDCEFGH,请写出这个二叉树的后序遍历结果。
ADECBHGF
先序+中序遍历还原二叉树:先序遍历是:ABDEGCFH 中序遍历是:DBGEACHF
首先从先序得到第一个为A,就是二叉树的根,回到中序,可以将其分为三部分:
左子树的中序序列DBGE,根A,右子树的中序序列CHF
接着将左子树的序列回到先序可以得到B为根,这样回到左子树的中序再次将左子树分割为三部分:
左子树的左子树D,左子树的根B,左子树的右子树GE
同样地,可以得到右子树的根为C
类似地将右子树分割为根C,右子树的右子树HF,注意其左子树为空
如果只有一个就是叶子不用再进行了,刚才的GE和HF再次这样运作,就可以将二叉树还原了
7、 实现一个字符串“how are you”的逆序输出(编程语言不限)。如给定字符串为“hello world”,输出结果应当为“world hello”,进阶:去掉首尾空格,每个单词间只保留一个空格。
void reverse(char *start, char *end) {
if (start == NULL || end == NULL) {
return;
}
//翻转字符
while (start < end) {
char tmp = *start;
*start = *end;
*end = tmp;
start++;
end--;
}
}
char *reverseStrings(char * s){
if (s == NULL) {
return '\0';
}
//去除多余空格
char *str = s;
//去除首部空格
while(*str != '\0') {
if (*str != ' ') {
s = str;
break;
}
str++;
}
str = s;
int i,j;
i = 0;
j = 0;
//去除中间或尾部空格
while(*(str+i) != '\0') {
if (*(str+j) == ' ') {
if (*(str+j+1) == ' ') {
j++;
continue;
} else if (*(str+j+1) == '\0' ) {
//去掉尾部空格
*(str+i) = '\0';
break;
}
} else if (*(str+j) == '\0' ) {
//去掉尾部空格
*(str+i) = '\0';
break;
}
if (*(str+i) != *(str+j)) {
*(str+i) = *(str+j);
}
i++;
j++;
}
char *start,*end;
start = s;
end = s;
while(*end != '\0') {
end++;
}
end--;
reverse(start,end);
//翻转单词
start = s;
end = s;
while (*start != '\0') {
if (*start == ' ') {
start++;
end++;
} else if (*end == ' ' || *end == '\0'){
end--;
reverse(start,end);
start = ++end;
} else {
end++;
}
}
return s;
}
int main(int argc, const char * argv[]) {
char *str = reverseStrings("have a brilliant future");
while (*str != '\0') {
printf("%c", *str++);
}
return 0;
}
8、字符串匹,输出子串第一次出现的下标,具体要求如下:
给定主串“ababcabc”,模式串“abc”,输出结果为:2
给定主串 “aaaa”,模式串“bb”,输出结果为:-1
当模式串为空串的时候,输出结果应为:0
请实现findStringIndex函数。
int findStringIndex(char * inputs, char * matchs){
if (inputs == NULL || matchs == NULL) {
return -1;
}
if (*matchs == '\0') {
return 0;
}
int i = 0,j = 0;
while (*(inputs + i) != '\0' && *(matchs + j) != '\0') {
if (*(inputs + i) == *(matchs + j)) {
i++;
j++;
} else {
i = i-j+1;
j = 0;
}
}
//模式串到串尾说明匹配成功,返回下标
if (*(matchs + j) == '\0') {
return i-j;
}
return -1;
}
int main(int argc, const char * argv[]) {
printf("index = %d", findStringIndex("ababcabc", "abc"));
return 0;
}
9、字符串匹配进阶,KMP算法:
void generateNextArr(char *s,int *next) {
//初始化
int k = -1,j = 0;
//next[0]初始化成-1
*next = -1;
while (j < strlen(s) - 1) {
if (k == -1 || *(s + j) == *(s + k)) {
j++;
k++;
//s[j]==s[next[k]]必然会失配
if (*(s + j) != *(s+k)) {
*(next + j) = k;
} else {
*(next + j) = *(next + k);
}
} else {
k = *(next + k);
}
}
}
int kmpMatch(char *inputs, char *matchs) {
if (inputs == NULL || matchs == NULL) {
return -1;
}
//模式串为空串时返回0
if (*matchs == '\0') {
return 0;
}
int inputLen = strlen(inputs);
int len = strlen(matchs);
int *next = (int *)malloc(len*sizeof(int));
//生成next数组:失配时模式串下标跳转的位置
generateNextArr(matchs, next);
int i = 0,j = 0;
while (i < inputLen && j < len) {
if (j == -1 || *(inputs + i) == *(matchs + j)) {
i++;
j++;
} else {
j = *(next + j);
}
}
if (*(matchs + j) == '\0') {
return i-j;
}
free(next);
return -1;
}
int main(int argc, const char * argv[]) {
printf("index = %d", kmpMatch("aabcbbabcb", "abc"));
return 0;
}
10、如何实现一个数组每个元素依次向右移动k位,后面的元素依次往前面补。比如: [1, 2, 3, 4, 5] 移动两位变成[4, 5, 1, 2, 3]。
思路:三次反转
后K位反转:12354
前部分反转:32154
整体全部反转:45123
int * reverse1(int *arr, int start, int end) {
while (start < end) {
arr[start] = arr[start] ^ arr[end];
arr[end] = arr[end] ^ arr[start];
arr[start] = arr[start] ^ arr[end];
start++;
end--;
}
return arr;
}
int * moveK(int *arr, int numSize, int k) {
reverse1(arr, numSize - k, numSize-1);
reverse1(arr, 0, numSize-k-1);
reverse1(arr, 0, numSize-1);
return arr;
}
int main(int argc, const char * argv[]) {
int arr[5] = {1,2,3,4,5};
int numSize = sizeof(arr) / sizeof(int);
moveK(arr, numSize, 2);
for (int i = 0; i < numSize; i++) {
printf("%d ",arr[i]);
}
return 0;
}
11、 给定一个字符串,输出本字符串中只出现一次并且最靠前的那个字符的位置?如“abaccddeeef”,字符是b,输出应该是2。
char findChar(char *s){
if (s == NULL) {
return ' ';
}
int hashTable[256];
memset(hashTable, 0, sizeof(hashTable));
char *p = s;
while(*p != '\0') {
hashTable[*p]++;
p++;
}
p = s;
while(*p != '\0') {
if (hashTable[*p] == 1) {
return *p;
}
p++;
}
return ' ';
}
int main(int argc, const char * argv[]) {
char *inputStr = "abaccddeeef";
char ch = findChar(inputStr);
printf("%c \n", ch);
return 0;
}
12、 如何实现链表翻转(链表逆序)?
思路:每次把第二个元素提到最前面来。
#include <stdio.h>
#include <stdlib.h>
typedef struct NODE {
struct NODE *next;
int num;
}node;
node *createLinkList(int length) {
if (length <= 0) {
return NULL;
}
node *head,*p,*q;
int number = 1;
head = (node *)malloc(sizeof(node));
head->num = 1;
head->next = head;
p = q = head;
while (++number <= length) {
p = (node *)malloc(sizeof(node));
p->num = number;
p->next = NULL;
q->next = p;
q = p;
}
return head;
}
void printLinkList(node *head) {
if (head == NULL) {
return;
}
node *p = head;
while (p) {
printf("%d ", p->num);
p = p -> next;
}
printf("\n");
}
node *reverseFunc1(node *head) {
if (head == NULL) {
return head;
}
node *p,*q;
p = head;
q = NULL;
while (p) {
node *pNext = p -> next;
p -> next = q;
q = p;
p = pNext;
}
return q;
}
int main(int argc, const char * argv[]) {
node *head = createLinkList(7);
if (head) {
printLinkList(head);
node *reHead = reverseFunc1(head);
printLinkList(reHead);
free(reHead);
}
free(head);
return 0;
}
13、删除链表中的重复元素,每个重复元素需要出现一次,如给定链表 1->2->2->3->4->5->5,输出结果应当为 1->2->3->4->5。请实现下面的deleteRepeatElements函数:
typedef struct NODE {
struct NODE *next;
int num;
} node;
node *deleteRepeatElements(node *head) {
if (head == NULL) {
return head;
}
struct ListNode* pNode = head;
while (pNode && pNode->next) {
if (pNode->val == pNode->next->val) {
struct ListNode *tempNode = pNode->next;
pNode->next = pNode->next->next;
free(tempNode);
} else {
pNode=pNode->next;
}
}
return head;
}
14、删除链表中重复的元素,只保留不重复的结点。如:1->1->2->3->4->4->5,输出结果:2->3->5,请实现下面的deleteRepeatElements函数。
typedef struct NODE {
struct NODE *next;
int num;
} node;
node *deleteRepeatElements(node *head) {
if (head == NULL) {
return head;
}
//头结点有可能会被删除,先创建一个头结点
node *pHead = (node *)malloc(sizeof(node));
pHead->next = head;
node *current = pHead;
while(current->next && current->next->next) {
if (current->next->val == current->next->next->val) {
node *tempNode = current->next;
while(tempNode && tempNode->next && tempNode->val == tempNode->next->val) {
tempNode = tempNode->next;
}
current->next = tempNode->next;
} else {
current = current->next;
}
}
return pHead->next;
}
15、 打印2-100之间的素数。
判断素数思路:通过分析我们可知5以上的自然数都可以用6x-1,6x,6x+1,6x+2,6x+3,6x+4,6x+5来代替,又因6x,6x+2=2(3x+1),6x+3=3(2x+1),6x+4=2*(3x+2)以上都不可能是素数,所以只需要判断6x-1,6x+1,6x+5(6x两侧的数)即可。
int main(int argc, const char * argv[]) {
for (int i = 2; i < 100; i++) {
int r = isPrime(i);
if (r == 1) {
printf("%ld ", i);
}
}
return 0;
}
int isPrime(int n)
{
if(n == 2 || n == 3) {
return 1;
}
if(n % 6 != 1 && n % 6 != 5) {
return 0;
}
for(int i = 5; (i * i) <= n; i += 6) {
if(n % i == 0 || n % (i + 2) == 0) {
return 0;
}
}
return 1;
}
16、计算100以内素数的个数
int countPrime(int n) {
int i,j,count = 0;
//开辟空间
int *prime = (int *)malloc(sizeof(int) * n);
//初始默认所有数为素数
memset(prime, 1, sizeof(int) * n);
for (i = 2; i < n; i++) {
if (prime[i]) {
count++;
for (j = i + i; j < n; j += i) {
//标记不是素数
prime[j] = 0;
}
}
}
return count;
}
17、 求两个整数的最大公约数。
int gcd(int a, int b) {
while (a != b) {
if (a > b) {
a = a - b;
} else {
b = b - a;
}
}
return a;
}
转自:https://www.jianshu.com/p/746495327da6
收起阅读 »iOS底层-方法的本质
通过clang -rewrite-objc main.m -o mian.cpp编译的对象调用方法底层
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
LGPerson *p = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("new"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("run"));
}
return 0;
}
可以看出在我们进行LGPerson初始化的时候,我们都知道会调用alloc,init.我这里为了简单只调用'new'.但是底层不是像我们利用[]调用的,而是调用了一个函数objc_msgSend这就是我们消息发送的方法,因为考虑的参数我们进行了前面的强转.如果有一定C功底就知道objc_msgSend就是发送消息,我们在断点调试ViewDidLoad的时候,发现能打印self,_cmd这就是我们的消息底层默认的两个参数id,SEL
一个是消息接受者
一个是消息编号
我们还可以在objc_msgSend末尾继续加参数,但是考虑到编译参数问题,我们需要关闭严格核查
我通过SEL能找到函数实现,底层是依赖一个IMP的函数指针
就会找我们具体的函数实现
我们模拟是不是也可不断发送消息,模拟四种消息发送:
LGStudent *s = [LGStudent new];
[s run];
// 方法调用底层编译
// 方法的本质: 消息 : 消息接受者 消息编号 ....参数 (消息体)
objc_msgSend(s, sel_registerName("run"));
// 类方法编译底层
[LGStudent walk];
objc_msgSend(objc_getClass("LGStudent"), sel_registerName("walk"));
// 向父类发消息(对象方法)
struct objc_super mySuper;
mySuper.receiver = s;
mySuper.super_class = class_getSuperclass([s class]);
objc_msgSendSuper(&mySuper, @selector(run));
//向父类发消息(类方法)
struct objc_super myClassSuper;
myClassSuper.receiver = [s class];
myClassSuper.super_class = class_getSuperclass(object_getClass([s class]));
objc_msgSendSuper(&myClassSuper, sel_registerName("walk"));
移动iOS架构起航
架构就如人体骨架,肌肉和血液还有其他就顺着骨架填充!
MVC架构思想
MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。MVC被独特的发展起来用于映射传统的输入、处理和输出功能在一个逻辑的图形化用户界面的结构中。
组成MVC的三个模式分别是组合模式、策咯模式、观察者模式,MVC在软件开发中发挥的威力,最终离不开这三个模式的默契配合
View层,单独实现了组合模式
Model层和View层,实现了观察者模式
View层和Controller层,实现了策咯模式
MVC要实现的目标是将软件用户界面和业务逻辑分离以使代码 可扩展性、可复用性、可维护性、灵活性加强.
ViewController过重
通过上面的图大家也看到了非常完美,但是用起来真有问题!
但是我们实际开发经常会变形:比如我们ViewController会非常之重,动不动几百行,几千行代码!那么是一些什么东西在里面?
繁重的网络层
复杂的UI层
难受的代理
啰嗦的业务逻辑
还有一些其他功能
控制器(controller)的作用就是这么简单, 用来将不同的View和不同的Model组织在一起,顺便替双方传递消息,仅此而已。
这里建议:
繁重的网络层 封装到我们业务逻辑管理者比如:present viewModel
复杂的UI层就应该是UI的事,直接剥离出VC
难受的代理就可以封装一个功能类比如我们常写的tableview collectionView的代理 我们就可以抽取出来封装为一个公共模块,一些特定的逻辑就可以利用适配器设计模式,根据相应的model消息转发
耦合性问题
经常我们在开发过程中会出现下面的线!这样的线对我们重用性,灵活性造成了压力
这里我推荐大家使用不直接依赖model 利用发送消息的方式传递
MVP架构思想
MVP 全称:Model-View-Presenter ;MVP 是从经典的模式MVC演变而来,它们的基本思想有相通的地方:Controller/Presenter负责逻辑的处理,Model提供数据,View负责显示。
我最喜欢MVP的面向协议编程的思想!
根据产品相应的需求,写出其次需求的接口,然后根据接口去找我们响应的发起者,和接受者!面向协议编程---面向接口编程---面向需求编程---需求驱动代码!
MVP能够解决:
代码思路清晰
耦合度降低显著
通讯还算比较简单
缺点:
我们需要写很多关于代理相关的代码
视图和Presenter的交互会过于频繁
如果Presenter过多地渲染了视图,往往会使得它与特定的视图的联系过于紧密。一旦视图需要变更,那么Presenter也需要变更了
MVVM架构思想
MVVM是Model-View-ViewModel的简写。它本质上就是MVC 的改进版。MVVM 就是将其中的View 的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。当然这些事 ViewModel 已经帮我们做了,它可以取出 Model 的数据同时帮忙处理 View 中由于需要展示内容而涉及的业务逻辑
如果要说MVVM的特色,我觉得最大莫过于:双向绑定
经常我们在设计我们的架构的时候,ViewModel层会设计响应的反向Block回调,方便我们的数据更新,只需要我们回调Block,那么在相应代码块绑定的视图中就能获取到最新的数据!
这个时候我们要向完美实现正向传递,经常借助另一个非常牛逼的思想:响应式
如果要想完美实现双向绑定,那么KVO我不太建议,推荐玩玩ReactiveCocoa这个框架---编程思想之集大成者!如果你们在MVVM架构设计中嵌入响应式,那就是双剑合璧.
组件路由设计
在众多架构中,在解耦性方面我觉得组件化开发无意做的真心不错,大家经常在各个控制器跳转,就会像蜘蛛网一样错综复杂。
站在架构的层面就是把项目规矩化!条理化
根据合适的边界把这个项目进行组件模块化出来,利用cocoaPods来管理!在整体组件分层下面的模型给大家进行参考学习!
架构之路,无论在知识的深度还有广度方面都有较高的要求!尤其重要的对问题的的解决思维,不止在普通的应用层的ipa调用;需要大家对思维更加宽广,从代码上升到项目,到产品,甚至到公司!有时候你会很感觉很累很难,但是不将就注定不一样的你!
摘自作者:Cooci_和谐学习_不急不躁
Charles抓取iPhone接口数据
抓取HTTP请求
安装Charles,自行百度安装
我安装的版本是4.2.6的
设置代理:Proxy->ProxySetting
手机设置,手机跟电脑接同一个局域网,配置HTTP代理
抓取HTTPS请求
抓取请求需要安装SSL证书,Help->SSL Proxying,安装证书,根据提示在手机上输入指定url安装CA证书。
手机安装完后,默认是不信任的,需要手动信任以下该CA证书,打开设置,通用->关于本机->证书信任设置,打开开关信任即可
证书配置完毕后,charles默认是没有抓取Https请求的,在需要抓取的Https url右击,选中Enable SSL Proxy即可。
Charles视图简单讲解
转自:https://www.jianshu.com/p/82096a460e56
iOS 利用UserDefaults快速实现常用搜索页记录工具
1、需求分析
- 存储内容为字符串
- 存储内容要去重
- 存储个数会有个上限
- 存储个数达到上限后要先前挤掉旧数据,保留新数据
- 调用动作一般为 存 / 读 / 清空全部
2、实现
.h文件
// RPCustomTool.h
// RollingPin
//
// Created by RollingPin on 2020/12/31.
// Copyright © 2020 RollingPin. All rights reserved.
//
#import
#import
@interface RPCustomTool : NSObject
/// 存
+ (void)saveHistoryString:(NSString *)saveStr;
/// 读
+ (NSArray *)readHistoryList;
/// 清空
+ (void)deleteHistoryList;
@end
.m文件
// RPCustomTool.h
// RollingPin
//
// Created by RollingPin on 2020/12/31.
// Copyright © 2020 RollingPin. All rights reserved.
//
#import "RPCustomTool.h"
@implementation RPCustomTool
#pragma mark - 存
+ (void)saveHistoryString:(NSString *)saveStr
{
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
NSArray *savedArray = [userDefaults arrayForKey:@"RPSearchHistoryMark"];
NSMutableArray *savedMuArray = [[NSMutableArray alloc]initWithArray:savedArray];
//去重
NSString *repetitiveStr = @"";
for (NSString * oneStr in savedArray) {
if ([oneStr isEqualToString:saveStr]) {
repetitiveStr = oneStr;
break;
}
}
if (repetitiveStr.length >0) {
[savedMuArray removeObject:repetitiveStr];
}
[savedMuArray addObject:saveStr];
//设置最大保存数
if(savedMuArray.count > 10)
{
[savedMuArray removeObjectAtIndex:0];
}
//最后再存储到NSUserDefaults中
[userDefaults setObject:savedMuArray forKey:@"RPSearchHistoryMark"];
[userDefaults synchronize];
}
#pragma mark - 读
+ (NSArray *)readHistoryList
{
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
//读取数组NSArray类型的数据
NSArray *savedArray = [userDefaults arrayForKey:@"RPSearchHistoryMark"];
NSLog(@"savedArray======%@",savedArray);
return [savedArray copy];
}
#pragma mark - 清空
+ (void)deleteHistoryList
{
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
[userDefaults setObject:[NSArray array] forKey:@"RPSearchHistoryMark"];
[userDefaults synchronize];
}
@end
转自:https://www.jianshu.com/p/006bd3fbc044
收起阅读 »UITableviewCell 使用Masonry撑开cell高度 遇见[LayoutConstraints] Unable to simultaneously satisfy constraints
1、问题描述
在布局UITableviewCell 内容时, 可用使用Masonry方便的自动计算高度撑开布局,但是当遇到cell高度不同,多个复杂的子view竖向排列时,容易产生高度计算冲突问题导致报如下一坨
2、解决办法
使用 Masonry 的 priorityHigh 属性来确定优先级
/**
* Sets the NSLayoutConstraint priority to MASLayoutPriorityHigh
*/
- (MASConstraint * (^)(void))priorityHigh;
具体使用要设置 <最后一个子view> 的 bottom 属性 priorityHigh()
[self.lastView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(self.topView.mas_bottom).offset(5);
make.left.equalTo(superView).offset(36);
make.right.equalTo(superView).offset(-16);
make.bottom.equalTo(self.contentView).offset(-16).priorityHigh();
}];
转自:https://www.jianshu.com/p/b334b69ab82e
收起阅读 »【iOS】Keychain 钥匙串
钥匙串,实际上是一个加密后的数据库,如下图所示。
即使吧App删除,钥匙串里面的数据也不会丢失。
数据都是以 Item 的形式来存储的,每个 Item 由一个加密后的 Data 数据,还有一系列用来描述该 Item 属性的 Attributes 组成。
由于是数据库,关键方法只有四种,增删改查,对应的是
SecItemAdd下面简单讲述一下使用方法
SecItemDelete
SecItemUpdate
SecItemCopyMatching
SecItemAdd
CFTypeRef result;
NSDictionary *query = @{
// 一个典型的新增方法的参数,包含三个部分
// 1.kSecClass key,它用来指定新增对象的类型
(NSString *)kSecClass: (NSString *)kSecClassGenericPassword,
// 2.若干项属性 key,例如 kSecAttrAccount,kSecAttrLabel 等,用来描述新增对象的属性
(NSString *)kSecAttrAccount: @"uniqueID",
// 3.kSecValueData key,用来设置新增对象保存的数据
(NSString *)kSecValueData: [@"token" dataUsingEncoding:NSUTF8StringEncoding],
// 可选
// 如果需要获取新增的 Item 对象的属性,需要如下属性,
(NSString *)kSecReturnData: (NSNumber *)kCFBooleanTrue,
(NSString *)kSecReturnAttributes: (NSNumber *)kCFBooleanTrue,
};
OSStatus status = SecItemAdd((CFDictionaryRef)query, &result);
if (result == errSecSuccess) {
// 新增成功
NSDictionary *itemInfo = (__bridge NSDictionary *)result;
NSLog(@"info: %@", itemInfo);
} else {
// 其他错误
}
result类型判断方式
SecItemDelete
NSDictionary *query = @{
// 一个典型的删除方法的参数,包含两个部分
// 1、kSecClass key,它用来指定删除对象的类型,必填。
(NSString *)kSecClass: (NSString *)kSecClassGenericPassword,
// 2、若干项属性 key,可选。
// 例如 kSecAttrAccount,kSecAttrLabel 等,用来设置删除对象的范围
// 默认情况下,符合条件的全部 Item 都会被删除
(NSString *)kSecAttrAccount: @"uniqueID",
};
OSStatus status = SecItemDelete((CFDictionaryRef)query);
if (result == errSecSuccess) {
// 删除成功
} else {
// 其他错误
}
SecItemUpdate
// 1、找出需要更新属性的 Item
// 参数格式与 SecItemCopyMatching 方法中的参数格式相同
NSDictionary *query = @{
(NSString *)kSecClass: (NSString *)kSecClassGenericPassword,
(NSString *)kSecAttrAccount: @"uniqueID",
};
// 2、需要更新的属性
// 若干项属性 key
NSDictionary *update = @{
(NSString *)kSecAttrAccount: @"another uniqueID",
(NSString *)kSecValueData: @"another value",
};
OSStatus status = SecItemUpdate((CFDictionaryRef)query, (CFDictionaryRef)update);
if (result == errSecSuccess) {
// 更新成功
} else {
// 其他错误
}
SecItemDelete
NSDictionary *query = @{
// 一个典型的删除方法的参数,包含两个部分
// 1、kSecClass key,它用来指定删除对象的类型,必填。
(NSString *)kSecClass: (NSString *)kSecClassGenericPassword,
// 2、若干项属性 key,可选。
// 例如 kSecAttrAccount,kSecAttrLabel 等,用来设置删除对象的范围
// 默认情况下,符合条件的全部 Item 都会被删除
(NSString *)kSecAttrAccount: @"uniqueID",
};
OSStatus status = SecItemDelete((CFDictionaryRef)query);
if (result == errSecSuccess) {
// 删除成功
} else {
// 其他错误
}
SecItemCopyMatching
CFTypeRef result;
NSDictionary *query = @{
// 一个典型的搜索方法的参数,包含三个部分
// 1、kSecClass key(必填),它用来指定搜索对象的类型
(NSString *)kSecClass: (NSString *)kSecClassGenericPassword,
// 2、若干项属性 key(可选),例如 kSecAttrAccount,kSecAttrLabel 等,用来描述搜索对象的属性
(NSString *)kSecAttrAccount: @"uniqueID",
// 3、搜索属性(可选)
// 例如 kSecMatchLimit(搜索一个还是多个,影响返回结果类型)
// kSecMatchCaseInsensitive 是否大小写敏感等
(NSString *)kSecMatchLimit: (NSString *)kSecMatchLimitAll,
(NSString *) kSecMatchCaseInsensitive: (NSNumber *) kCFBooleanTrue,
// (可选)如果需要获取新增的 Item 对象的属性,需要如下属性,
(NSString *)kSecReturnData: (NSNumber *)kCFBooleanTrue,
(NSString *)kSecReturnAttributes: (NSNumber *)kCFBooleanTrue,
};
OSStatus status = SecItemCopyMatching((CFDictionaryRef)query, &result);
if (result == errSecSuccess) {
// 新增成功
NSDictionary *itemInfo = (__bridge NSDictionary *)result;
NSLog(@"info: %@", itemInfo);
} else {
// 其他错误
}
result类型判断方式
iOS 网页和原生列表混合布局开发(文章+评论)
我们总会遇见特别不适合使用原生开发的页面,比如一个文章详情页,上面是文章下面是评论,就比如现在用的简书的手机版这样,那么这种需求应该怎么做呢?
最好的方法当然是整个页面都是用H5开发,哈哈哈;当然下面评论有时候会有很多交互导致得用原生控件开发,那这里就面临着严峻的问题了,上面是网页可以滑动,下面是评论最好是用列表做,具体怎么组合起来就值得我们说道说道了,当然方法有很多种,我这里讲解一种我觉得各方面都不错的。
ps:问题总结起来还是两个滑动视图上下滑动问题所以用我之前讲解的多个滑动视图冲突解决https://www.jianshu.com/p/cfe517ce437b 也可以解决不过这样使用H5那面配合的地方比较多。这个不多说,下面介绍我们今天要说的。
这个方案的整体思路:把web和table同时加在一个底层ScrollView上面,滑动底层ScrollView同时不断控制web和table的偏移量位置,使页面看起来是两个滑动视图连在一起的。
整体结构如图
一、视图介绍
黄色的是底层ScrollView,青色的一个加在底层ScrollView上的view(这里我们叫它contentView),然后正加载简书网页的是web,红色部分是table。web和table再加contentView上,这样我们控制整体位置的时候使用contentView就行;
二、视图之间的高度关系:
web和table的最大高度都是底层ScrollView的高度,这样做可以正好让其中一个充满整个底层ScrollView。
contentView的高度是web和table高度的和(毕竟就是为了放他们两)。
底层ScrollView的可滑动高度这里设定成web和table可滑动高度的总和,方便滑动处理。
ps:具体代码在后面。
三、滑动处理思路
滑动都靠底层ScrollView,禁用web和table的滑动,上面说了底层ScrollView的可滑动高度是web和table的总和所以进度条是正常的。
然后在滑动的同时不断调整contentView的位置,web和table的偏移量,使页面效果看起来符合预期。
四、滑动处理具体操作,整个滑动可以分成五阶段。ps:offsety 底层ScrollView的偏移量
1.offsety<=0,不用过多操作正常滑动
2.web内部可以滑动。控制contentView悬浮,使web在屏幕可视区域。同时修改web的偏移量。
3.web滑动到头。保持contentView的位置和web的偏移量,使table滑动到屏幕可视区域
4.table内部可以滑动。控制contentView悬浮,使table在屏幕可视区域。同时修改table的偏移量。
5.table滑动到头。保持contentView的位置和table的偏移量,使页面滑动到底部
五、具体代码
1.因为web和table都是随内容变高的,这里选择通过监听两者高度变化,同时刷新各个控件的高度,对应第二步骤
//添加监听
[self.webView addObserver:self forKeyPath:@"scrollView.contentSize" options:NSKeyValueObservingOptionNew context:nil];
[self.collectionView addObserver:self forKeyPath:@"contentSize" options:NSKeyValueObservingOptionNew context:nil];
//刷新各个控件高度
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
if (object == _webView) {
if ([keyPath isEqualToString:@"scrollView.contentSize"]) {
[self updateContainerScrollViewHeight];
}
}else if(object == _collectionView) {
if ([keyPath isEqualToString:@"contentSize"]) {
[self updateContainerScrollViewHeight];
}
}
}
- (void)updateContainerScrollViewHeight{
CGFloat webViewContentHeight = self.webView.scrollView.contentSize.height;
CGFloat collectionContentHeight = self.collectionView.contentSize.height;
if (webViewContentHeight == _lastWebViewContentHeight && collectionContentHeight == _lastCollectionContentHeight) {
return;
}
_lastWebViewContentHeight = webViewContentHeight;
_lastCollectionContentHeight = collectionContentHeight;
self.containerScrollView.contentSize = CGSizeMake(self.view.width, webViewContentHeight + collectionContentHeight);
CGFloat webViewHeight = (webViewContentHeight < _contentHeight) ?webViewContentHeight :_contentHeight;
CGFloat collectionHeight = collectionContentHeight < _contentHeight ?collectionContentHeight :_contentHeight;
self.webView.height = webViewHeight <= 0.1 ?0.1 :webViewHeight;
self.contentView.height = webViewHeight + collectionHeight;
self.collectionView.height = collectionHeight;
self.collectionView.top = self.webView.bottom;
[self scrollViewDidScroll:self.containerScrollView];
}
2.具体滑动处理代码:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
if (_containerScrollView != scrollView) {
return;
}
CGFloat offsetY = scrollView.contentOffset.y;
CGFloat webViewHeight = self.webView.height;
CGFloat collectionHeight = self.collectionView.height;
CGFloat webViewContentHeight = self.webView.scrollView.contentSize.height;
CGFloat collectionContentHeight = self.collectionView.contentSize.height;
if (offsetY <= 0) {
self.contentView.top = 0;
self.webView.scrollView.contentOffset = CGPointZero;
self.collectionView.contentOffset = CGPointZero;
}else if(offsetY < webViewContentHeight - webViewHeight){
self.contentView.top = offsetY;
self.webView.scrollView.contentOffset = CGPointMake(0, offsetY);
self.collectionView.contentOffset = CGPointZero;
}else if(offsetY < webViewContentHeight){
self.contentView.top = webViewContentHeight - webViewHeight;
self.webView.scrollView.contentOffset = CGPointMake(0, webViewContentHeight - webViewHeight);
self.collectionView.contentOffset = CGPointZero;
}else if(offsetY < webViewContentHeight + collectionContentHeight - collectionHeight){
self.contentView.top = offsetY - webViewHeight;
self.collectionView.contentOffset = CGPointMake(0, offsetY - webViewContentHeight);
self.webView.scrollView.contentOffset = CGPointMake(0, webViewContentHeight - webViewHeight);
}else if(offsetY <= webViewContentHeight + collectionContentHeight ){
self.contentView.top = self.containerScrollView.contentSize.height - self.contentView.height;
self.webView.scrollView.contentOffset = CGPointMake(0, webViewContentHeight - webViewHeight);
self.collectionView.contentOffset = CGPointMake(0, collectionContentHeight - collectionHeight);
}else {
//do nothing
NSLog(@"do nothing");
}
}
链接:https://www.jianshu.com/p/ca7f826fd39b
收起阅读 »iOS你需要知道的事--Crash分析
大家平时在开发过程中,经常会遇到Crash
,那也是在正常不过的事,但是作为一个优秀的iOS开发人员,必将这些用户不良体验降到最低。
线下Crash,我们直接可以调试,结合stack信息,不难定位!
线上Crash当然也有一些信息,毕竟苹果爸爸的产品还是做得非常不错的!
通过iPhone的Crash log也可以分析一些,但是这个是需要用户配合的,因为需要用户在手机 中 设置-> 诊断与用量->勾选 自动发送 ,然后在xcode中 Window->Organizer->Crashes 对应的app,就是当前app最新一版本的crash log ,并且是解析过的,可以根据crash 栈 等相关信息 ,尤其是程序代码级别的 有超链接,一键可以直接跳转到程序崩溃的相关代码,这样更容易定位bug出处.
为了能够第一时间发现程序问题,应用程序需要实现自己的崩溃日志收集服务,成熟的开源项目很多,如 KSCrash,plcrashreporter,CrashKit 等。追求方便省心,对于保密性要求不高的程序来说,也可以选择各种一条龙Crash统计产品,如 Crashlytics,Hockeyapp ,友盟,Bugly 等等
但是,所有的但是,这不够!因为我们不再是一个简单会用的iOS开发人员,必将走向底层,了解原理,掌握装逼内容和技巧是我们的必修课
首先我们来了解一下Crash的底层原理
iOS系统自带的 Apple’s Crash Reporter记录在设备中的Crash日志,Exception Type项通常会包含两个元素:Mach异常和 Unix信号。
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Subtype: KERN_INVALID_ADDRESS at 0x041a6f3
Mach异常是什么?它又是如何与Unix信号建立联系的?
Mach是一个XNU的微内核核心,Mach异常是指最底层的内核级异常,被定义在下 。每个thread,task,host都有一个异常端口数组,Mach的部分API暴露给了用户态,用户态的开发者可以直接通过Mach API设置thread,task,host的异常端口,来捕获Mach异常,抓取Crash事件。
所有Mach异常都在host层被ux_exception转换为相应的Unix信号,并通过threadsignal将信号投递到出错的线程。iOS中的 POSIX API就是通过Mach之上的 BSD层实现的。
因此,EXC_BAD_ACCESS (SIGSEGV)表示的意思是:Mach层的EXC_BAD_ACCESS异常,在host层被转换成SIGSEGV信号投递到出错的线程。
iOS的异常Crash
* KVO问题
* NSNotification线程问题
* 数组越界
* 野指针
* 后台任务超时
* 内存爆出
* 主线程卡顿超阀值
* 死锁
....
下面我就拿出最常见的两种Crash分析一下
Crash分析处理
上面我们也知道:既然最终以信号的方式投递到出错的线程,那么就可以通过注册相应函数来捕获信号.达到Hook的效果
+ (void)installUncaughtSignalExceptionHandler{
NSSetUncaughtExceptionHandler(&LGExceptionHandlers);
signal(SIGABRT, LGSignalHandler);
}
我们从上面的函数可以Hook到信息,下面我们开始进行包装处理.这里还是面向统一封装,因为等会我们还需要考虑Signal
void LGExceptionHandlers(NSException *exception) {
NSLog(@"%s",__func__);
NSArray *callStack = [LGUncaughtExceptionHandle lg_backtrace];
NSMutableDictionary *mDict = [NSMutableDictionary dictionaryWithDictionary:exception.userInfo];
[mDict setObject:callStack forKey:LGUncaughtExceptionHandlerAddressesKey];
[mDict setObject:exception.callStackSymbols forKey:LGUncaughtExceptionHandlerCallStackSymbolsKey];
[mDict setObject:@"LGException" forKey:LGUncaughtExceptionHandlerFileKey];
// exception - myException
[[[LGUncaughtExceptionHandle alloc] init] performSelectorOnMainThread:@selector(lg_handleException:) withObject:[NSException exceptionWithName:[exception name] reason:[exception reason] userInfo:mDict] waitUntilDone:YES];
}
下面针对封装好的myException进行处理,在这里要做两件事
1.存储,上传:方便开发人员检查修复
2.处理Crash奔溃,我们也不能眼睁睁看着BUG闪退在用户的手机上面,希望“起死回生,回光返照”
- (void)lg_handleException:(NSException *)exception{
// crash 处理
// 存
NSDictionary *userInfo = [exception userInfo];
[self saveCrash:exception file:[userInfo objectForKey:LGUncaughtExceptionHandlerFileKey]];
}
下面是一些封装的一些辅助函数
保存奔溃信息或者上传:针对封装数据本地存储,和相应上传服务器
- (void)saveCrash:(NSException *)exception file:(NSString *)file{
NSArray *stackArray = [[exception userInfo] objectForKey:LGUncaughtExceptionHandlerCallStackSymbolsKey];// 异常的堆栈信息
NSString *reason = [exception reason];// 出现异常的原因
NSString *name = [exception name];// 异常名称
// 或者直接用代码,输入这个崩溃信息,以便在console中进一步分析错误原因
// NSLog(@"crash: %@", exception);
NSString * _libPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:file];
if (![[NSFileManager defaultManager] fileExistsAtPath:_libPath]){
[[NSFileManager defaultManager] createDirectoryAtPath:_libPath withIntermediateDirectories:YES attributes:nil error:nil];
}
NSDate *dat = [NSDate dateWithTimeIntervalSinceNow:0];
NSTimeInterval a=[dat timeIntervalSince1970];
NSString *timeString = [NSString stringWithFormat:@"%f", a];
NSString * savePath = [_libPath stringByAppendingFormat:@"/error%@.log",timeString];
NSString *exceptionInfo = [NSString stringWithFormat:@"Exception reason:%@\nException name:%@\nException stack:%@",name, reason, stackArray];
BOOL sucess = [exceptionInfo writeToFile:savePath atomically:YES encoding:NSUTF8StringEncoding error:nil];
NSLog(@"保存崩溃日志 sucess:%d,%@",sucess,savePath);
}
获取函数堆栈信息,这里可以获取响应调用堆栈的符号信息,通过数组回传
+ (NSArray *)lg_backtrace{
void* callstack[128];
int frames = backtrace(callstack, 128);//用于获取当前线程的函数调用堆栈,返回实际获取的指针个数
char **strs = backtrace_symbols(callstack, frames);//从backtrace函数获取的信息转化为一个字符串数组
int i;
NSMutableArray *backtrace = [NSMutableArray arrayWithCapacity:frames];
for (i = LGUncaughtExceptionHandlerSkipAddressCount;
i < LGUncaughtExceptionHandlerSkipAddressCount+LGUncaughtExceptionHandlerReportAddressCount;
i++)
{
[backtrace addObject:[NSString stringWithUTF8String:strs[i]]];
}
free(strs);
return backtrace;
}
获取应用信息,这个函数提供给Siganl数据封装
NSString *getAppInfo(){
NSString *appInfo = [NSString stringWithFormat:@"App : %@ %@(%@)\nDevice : %@\nOS Version : %@ %@\n",
[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"],
[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"],
[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"],
[UIDevice currentDevice].model,
[UIDevice currentDevice].systemName,
[UIDevice currentDevice].systemVersion];
// [UIDevice currentDevice].uniqueIdentifier];
NSLog(@"Crash!!!! %@", appInfo);
return appInfo;
}
做完这些准备,你可以非常清晰的看到程序奔溃,哈哈哈!(好像以前奔溃还不清晰似的),这里说一下:我的意思你非常清晰的知道奔溃之前做了一些什么!
下面是检测我们奔溃之前的沙盒存储的信息:error.log
下面我们来一个骚操作:在监听的信息的时候来了一个Runloop,我们监听所有的mode,开启循环
(一个相对于我们应用程序自启的Runloop的平行空间).
SCLAlertView *alert = [[SCLAlertView alloc] initWithNewWindowWidth:300.0f];
[alert addButton:@"奔溃" actionBlock:^{
self.dismissed = YES;
}];
[alert showSuccess:exception.name subTitle:exception.reason closeButtonTitle:nil duration:0];
// 本次异常处理
CFRunLoopRef runloop = CFRunLoopGetCurrent();
CFArrayRef allMode = CFRunLoopCopyAllModes(runloop);
while (!self.dismissed) {
// machO
// 后台更新 - log
// kill
//
for (NSString *mode in (__bridge NSArray *)allMode) {
CFRunLoopRunInMode((CFStringRef)mode, 0.0001, false);
}
}
CFRelease(allMode);
在这个平行空间
我们开启一个弹框,这个弹框,跟着我们的应用程序保活,并且具备相应的响应能力,到目前为止:此时此刻还有谁!这不就是回光返照
?只要我们的条件成立,那么在相应的这个平行空间
继续做一些我们的工作,程序不死:what is dead may never die,but rises again harder and stronger
signal 函数拦截不到的解决方式
在debug模式下,如果你触发了崩溃,那么应用会直接崩溃到主函数,断点都没用,此时没有任何log信息显示出来,如果你想看log信息的话,你需要在lldb
中,拿SIGABRT
来说吧,敲入pro hand -p true -s false SIGABRT
命令,不然你啥也看不到。
然后断开断点,程序进入监听,下面剩下的操作就是包装异常,操作类似Exception
最后我们需要注意的针对我们的监听回收相应内存:
NSSetUncaughtExceptionHandler(NULL);
signal(SIGABRT, SIG_DFL);
signal(SIGILL, SIG_DFL);
signal(SIGSEGV, SIG_DFL);
signal(SIGFPE, SIG_DFL);
signal(SIGBUS, SIG_DFL);
signal(SIGPIPE, SIG_DFL);
if ([[exception name] isEqual:UncaughtExceptionHandlerSignalExceptionName])
{
kill(getpid(), [[[exception userInfo] objectForKey:UncaughtExceptionHandlerSignalKey] intValue]);
}
else
{
[exception raise];
}
到目前为止,我们响应的Crash处理已经入门,如果你还想继续探索也是有很多地方比如:
我们能否hook系统奔溃,异常的方法NSSetUncaughtExceptionHandler,已达到拒绝传递 UncaughtExceptionHandler的效果
我们在处理异常的时候,利用Runloop回光返照,有没有更加合适的方法
Runloop回光返照我们怎么继续保证应用程序稳定执行
摘自作者:Cooci_和谐学习_不急不躁
原贴链接:https://www.jianshu.com/p/56f96167a6e9
iOS-UIView常用的setNeedsDisplay和setNeedsLayout
- UIView的setNeedsDisplay和setNeedsLayout方法
首先两个方法都是异步执行的。而setNeedsDisplay会调用自动调用drawRect方法,这样可以拿到 UIGraphicsGetCurrentContext,就可以画画了。而setNeedsLayout会默认调用layoutSubViews,就可以 处理子视图中的一些数据。综上所诉,setNeedsDisplay方便绘图,而 layoutSubViews方便出来数据。
layoutSubviews在以下情况下会被调用:
1、init初始化不会触发layoutSubviews。
2、addSubview会触发layoutSubviews。
3、设置view的Frame会触发layoutSubviews,当然前提是frame的值设置前后发生了变化。
4、滚动一个UIScrollView会触发layoutSubviews。
5、旋转Screen会触发父UIView上的layoutSubviews事件。
6、改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件。
7、直接调用setLayoutSubviews。
drawRect在以下情况下会被调用:
1、如果在UIView初始化时没有设置rect大小,将直接导致drawRect不被自动调用。drawRect调用是在Controller->loadView, Controller->viewDidLoad 两方法之后掉用的.所以不用担心在控制器中,这些View的drawRect就开始画了.这样可以在控制器中设置一些值给View(如果这些View draw的时候需要用到某些变量值).
2、该方法在调用sizeToFit后被调用,所以可以先调用sizeToFit计算出size。然后系统自动调用drawRect:方法。
3、通过设置contentMode属性值为UIViewContentModeRedraw。那么将在每次设置或更改frame的时候自动调用drawRect:。
4、直接调用setNeedsDisplay,或者setNeedsDisplayInRect:触发drawRect:,但是有个前提条件是rect不能为0。
以上1,2推荐;而3,4不提倡
drawRect方法使用注意点:
1、若使用UIView绘图,只能在drawRect:方法中获取相应的contextRef并绘图。如果在其他方法中获取将获取到一个invalidate的ref并且不能用于画图。drawRect:方法不能手动显示调用,必须通过调用setNeedsDisplay 或者 setNeedsDisplayInRect,让系统自动调该方法。
2、若使用CAlayer绘图,只能在drawInContext: 中(类似于drawRect)绘制,或者在delegate中的相应方法绘制。同样也是调用setNeedDisplay等间接调用以上方法
3、若要实时画图,不能使用gestureRecognizer,只能使用touchbegan等方法来掉用setNeedsDisplay实时刷新屏幕
链接:https://www.jianshu.com/p/33a28bb14749 收起阅读 »
iOS Crash分析中的Signal
下面是一些信号说明
1.SIGHUP
本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联。
登录Linux时,系统会分配给登录用户一个终端(Session)。在这个终端运行的所有程序,包括前台进程组和后台进程组,一般都属于这个 Session。当用户退出Linux登录时,前台进程组和后台有对终端输出的进程将会收到SIGHUP信号。这个信号的默认操作为终止进程,因此前台进 程组和后台有终端输出的进程就会中止。不过可以捕获这个信号,比如wget能捕获SIGHUP信号,并忽略它,这样就算退出了Linux登录, wget也 能继续下载。
此外,对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。
2.SIGINT
程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。
3.SIGQUIT
和SIGINT类似, 但由QUIT字符(通常是Ctrl-)来控制. 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。
4.SIGILL
执行了非法指令. 通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出时也有可能产生这个信号。
5.SIGTRAP
由断点指令或其它trap指令产生. 由debugger使用。
6.SIGABRT
调用abort函数生成的信号。
7.SIGBUS
非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数, 但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。
8.SIGFPE
在发生致命的算术运算错误时发出. 不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误。
9.SIGKILL
用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号。
10.SIGUSR1
留给用户使用
11.SIGSEGV
试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据.
12.SIGUSR2
留给用户使用
13.SIGPIPE
管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止。
14.SIGALRM
时钟定时信号, 计算的是实际的时间或时钟时间. alarm函数使用该信号.
15.SIGTERM
程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号。如果进程终止不了,我们才会尝试SIGKILL。
16.SIGCHLD
子进程结束时, 父进程会收到这个信号。
如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这时的子进程称为僵尸进程。这种情 况我们应该避免(父进程或者忽略SIGCHILD信号,或者捕捉它,或者wait它派生的子进程,或者父进程先终止,这时子进程的终止自动由init进程 来接管)。
17.SIGCONT
让一个停止(stopped)的进程继续执行. 本信号不能被阻塞. 可以用一个handler来让程序在由stopped状态变为继续执行时完成特定的工作. 例如, 重新显示提示符
18.SIGSTOP
停止(stopped)进程的执行. 注意它和terminate以及interrupt的区别:该进程还未结束, 只是暂停执行. 本信号不能被阻塞, 处理或忽略.
19.SIGTSTP
停止进程的运行, 但该信号可以被处理和忽略. 用户键入SUSP字符时(通常是Ctrl-Z)发出这个信号
20.SIGTTIN
当后台作业要从用户终端读数据时, 该作业中的所有进程会收到SIGTTIN信号. 缺省时这些进程会停止执行.
21.SIGTTOU
类似于SIGTTIN, 但在写终端(或修改终端模式)时收到.
22.SIGURG
有”紧急”数据或out-of-band数据到达socket时产生.
23.SIGXCPU
超过CPU时间资源限制. 这个限制可以由getrlimit/setrlimit来读取/改变。
24.SIGXFSZ
当进程企图扩大文件以至于超过文件大小资源限制。
25.SIGVTALRM
虚拟时钟信号. 类似于SIGALRM, 但是计算的是该进程占用的CPU时间.
26.SIGPROF
类似于SIGALRM/SIGVTALRM, 但包括该进程用的CPU时间以及系统调用的时间.
27.SIGWINCH
窗口大小改变时发出.
28.SIGIO
文件描述符准备就绪, 可以开始进行输入/输出操作.
SIGPWR
Power failure
SIGSYS
非法的系统调用。
关键点注意
在以上列出的信号中,程序不可捕获、阻塞或忽略的信号有:
SIGKILL,SIGSTOP
不能恢复至默认动作的信号有:
SIGILL,SIGTRAP
默认会导致进程流产的信号有:
SIGABRT,SIGBUS,SIGFPE,SIGILL,SIGIOT,SIGQUIT,SIGSEGV,SIGTRAP,SIGXCPU,SIGXFSZ
默认会导致进程退出的信号有:
SIGALRM,SIGHUP,SIGINT,SIGKILL,SIGPIPE,SIGPOLL,SIGPROF,SIGSYS,SIGTERM,SIGUSR1,SIGUSR2,SIGVTALRM
默认会导致进程停止的信号有:
SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU
默认进程忽略的信号有:
SIGCHLD,SIGPWR,SIGURG,SIGWINCH
此外,SIGIO在SVR4是退出,在4.3BSD中是忽略;SIGCONT在进程挂起时是继续,否则是忽略,不能被阻塞。
摘自作者:Cooci_和谐学习_不急不躁
原贴链接:https://www.jianshu.com/p/3a9dc6bd5e58
iOS——SDWebImage加载WebP图片
1.确定第三方库
首先直接去SDWebImage的仓库,里面直接就有关于WebP的仓库地址
也就是SDWebImageWebPCoder,直接pod 'SDWebImageWebPCoder'就行。(如果项目里没有SDWebImage,需要pod 'SDWebImage')
这里要注意!!!是pod 'SDWebImageWebPCoder'
我搜索SDWebImage加载WebP,权重高的答案都是pod 'SDWebImage/WebP',但是这个仓库我在SDWebImage的repositories里搜索不到,也就是说没有这个仓库,结果如图。
猜测可能之前的旧仓库是这个名字,那些文章也一直没更新,但是权重又高,不免误人子弟了一番。
2.导入SDWebImageWebPCoder
大概率会在pod install时报错,因为libwebp这个仓库的地址连接不上。
1、在终端输入pod repo 查看 cocoapods 在本机的PATH,每个人的路径都可能不一样
2、复制trunk的path,command + shift + G 输入上一步的地址,依次点击Specs-->1-->9-->2-->libwebp。(这里要注意有可能你的路径是cocoapods的path)
3、选择报错的版本打开,将source下git地址更改为
https://github.com/webmproject/libwebp.git
4、pod install(如果还报一样的错,那么是第2步出了问题,去另一个路径改source-git的地址即可)
3.使用SDWebImageWebPCoder
SDImageWebPCoder *webPCoder = [SDImageWebPCoder sharedCoder];
[[SDImageCodersManager sharedManager] addCoder:webPCoder];
NSData *webpData;
UIImage *wimage = [[SDImageWebPCoder sharedCoder] decodedImageWithData:webpData options:nil];
NSData *webpData;
[UIImage sd_imageWithWebPData:webpData];
经测试以上两种写法都能成功加载webp图片
转自:https://www.jianshu.com/p/74fab9c7de77
收起阅读 »iOS dispatch_semaphore信号量的使用(for循环请求网络时,使用信号量导致死锁)
有的时候我们会遇到这样的需求:
循环请求网络,但是在循环的过程中,必须上一个网络回调完成后才能请求下一个网络即进行下一个循环,也就是所谓的多个异步网络做同步请求,首先想到的就是用信号量拦截,但是发现AFNetWorking配合信号量使用时,网络不回调了,是什么原因引起的网络无法回调。下面我们模拟下正常使用过程并分析,如下:
-(void)semaphoreTest{
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
for (int i = 0; i<10; i++) {
[self semaphoreTestBlock:^(NSString *TNT) {
NSLog(@"任务完成 %d",i);
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"信号量限制 %d",i);
}
}
//这里用延迟模拟异步网络请求
-(void)semaphoreTestBlock:(void(^)(NSString * TNT))block{
/*
queue 的类型无论是串行队列还是并行队列并不影响最终结果
如果 queue = dispatch_get_main_queue() 将会堵塞组线程,造成死锁
*/
dispatch_queue_t queue = dispatch_queue_create("myqueue",DISPATCH_QUEUE_SERIAL);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), queue, ^{
block(@"完成");
});
}
这段代码的输出结果为:
2019-10-11 14:40:23.961328+0800 LJC[9013:1358198] 任务完成 0
2019-10-11 14:40:23.961751+0800 LJC[9013:1356826] 信号量限制 0
2019-10-11 14:40:25.061312+0800 LJC[9013:1358198] 任务完成 1
2019-10-11 14:40:25.061673+0800 LJC[9013:1356826] 信号量限制 1
2019-10-11 14:40:26.062082+0800 LJC[9013:1356931] 任务完成 2
2019-10-11 14:40:26.062381+0800 LJC[9013:1356826] 信号量限制 2
2019-10-11 14:40:27.062883+0800 LJC[9013:1356931] 任务完成 3
2019-10-11 14:40:27.063275+0800 LJC[9013:1356826] 信号量限制 3
2019-10-11 14:40:28.160535+0800 LJC[9013:1356931] 任务完成 4
2019-10-11 14:40:28.160988+0800 LJC[9013:1356826] 信号量限制 4
2019-10-11 14:40:29.161327+0800 LJC[9013:1356931] 任务完成 5
2019-10-11 14:40:29.161512+0800 LJC[9013:1356826] 信号量限制 5
2019-10-11 14:40:30.161756+0800 LJC[9013:1356931] 任务完成 6
2019-10-11 14:40:30.161989+0800 LJC[9013:1356826] 信号量限制 6
2019-10-11 14:40:31.261507+0800 LJC[9013:1356931] 任务完成 7
2019-10-11 14:40:31.261912+0800 LJC[9013:1356826] 信号量限制 7
2019-10-11 14:40:32.361503+0800 LJC[9013:1356931] 任务完成 8
2019-10-11 14:40:32.361870+0800 LJC[9013:1356826] 信号量限制 8
2019-10-11 14:40:33.461544+0800 LJC[9013:1358198] 任务完成 9
2019-10-11 14:40:33.461953+0800 LJC[9013:1356826] 信号量限制 9
如果我们把
dispatch_queue_t queue = dispatch_queue_create("myqueue",DISPATCH_QUEUE_SERIAL);
替换成
dispatch_queue_t queue = dispatch_get_main_queue()
发现输出结果为空
为什么呢?
首先我们要知道
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
他怎么才能实现锁的功能,他的锁其实是针对线程的,我们当前任务是在主线程执行的,我们就需要在主线程上锁。
完成任务我们去将信号量+1,即执行
dispatch_semaphore_signal(semaphore)
这个时候发现你的回调也是在主线程触发的,但是此时主线程上锁,已经卡住了,是不能让你在主线程做任务的,这就形成了相互等待,卡死了,所以我们需要将回调任务放在非主线程中(以目前这个例子来说,就是非主线程,其实我们最终调整的目的是让执行任务和回调任务不在同一线程即可)。
那我们如果将任务(for循环)在子线程中执行,回调在主线程中是否可以呢?下面我们修改代码
-(void)semaphoreTest{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
for (int i = 0; i<10; i++) {
[self semaphoreTestBlock:^(NSString *TNT) {
NSLog(@"任务完成 %d",i);
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"信号量限制 %d",i);
}
});
}
-(void)semaphoreTestBlock:(void(^)(NSString * TNT))block{
// dispatch_queue_t queue = dispatch_queue_create("myqueue",DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t queue = dispatch_get_main_queue();
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), queue, ^{
block(@"完成");
});
}
输出结果:
2019-10-11 14:51:00.224109+0800 LJC[9063:1362953] 任务完成 0
2019-10-11 14:51:00.224486+0800 LJC[9063:1363099] 信号量限制 0
2019-10-11 14:51:01.325117+0800 LJC[9063:1362953] 任务完成 1
2019-10-11 14:51:01.325493+0800 LJC[9063:1363099] 信号量限制 1
2019-10-11 14:51:02.425129+0800 LJC[9063:1362953] 任务完成 2
2019-10-11 14:51:02.425491+0800 LJC[9063:1363099] 信号量限制 2
2019-10-11 14:51:03.524266+0800 LJC[9063:1362953] 任务完成 3
2019-10-11 14:51:03.524715+0800 LJC[9063:1363099] 信号量限制 3
2019-10-11 14:51:04.625254+0800 LJC[9063:1362953] 任务完成 4
2019-10-11 14:51:04.625659+0800 LJC[9063:1363099] 信号量限制 4
2019-10-11 14:51:05.725228+0800 LJC[9063:1362953] 任务完成 5
2019-10-11 14:51:05.725573+0800 LJC[9063:1363099] 信号量限制 5
2019-10-11 14:51:06.726094+0800 LJC[9063:1362953] 任务完成 6
2019-10-11 14:51:06.726442+0800 LJC[9063:1363099] 信号量限制 6
2019-10-11 14:51:07.825270+0800 LJC[9063:1362953] 任务完成 7
2019-10-11 14:51:07.825613+0800 LJC[9063:1363099] 信号量限制 7
2019-10-11 14:51:08.925323+0800 LJC[9063:1362953] 任务完成 8
2019-10-11 14:51:08.925674+0800 LJC[9063:1363099] 信号量限制 8
2019-10-11 14:51:10.025359+0800 LJC[9063:1362953] 任务完成 9
2019-10-11 14:51:10.025722+0800 LJC[9063:1363099] 信号量限制 9
这就验证了我们的想法, 执行任务和任务回调是不能在一个线程中的
整理
在使用信号量的时候,需要注意 dispatch_semaphore_wait 需要和 任务 放在同一线程,在任务执行异步回调的时候,需要将回调放在与执行任务不同的线程中,因为如果在同一线程中 dispatch_semaphore_wait 操作会造成相互等待导致死锁问题,我们在使用 AFNetWorking 的时候,他默认的回调是在 主线程中,所以我们在配合 AFNetWorking 使用信号量的时候可以指定 AFNetWorking 的回调线程,或者我们在执行任务的时候,将任务放在其他线程
注释:
写这篇文章是因为我在用信号量配合AFNetWorking做网路任务的时候发现一只卡死,在网上找的都说指定AFNetWorking 的 completionQueue ,然后我更改了代码,request是我们网络对AFNetWorking的封装对象实例,按理来说是没问题的,但是不知道为什么还是会造成死锁。目前原因没找到。所以我将for循环再放了子线程中
request.sessionManager.completionQueue = dispatch_get_global_queue(0, 0);
如发现理解错误,望指出 ^_^ THANKS
转自:https://www.jianshu.com/p/91e9e38e3f51
收起阅读 »iOS 登录接口封装实践
登录。。。基本所有APP都少不了,开始写APP,可能首先就是从登录开始
我也一样,我手上有一个封装了所有账户体系相关接口的SDK,运行良好但也遇到一些烦心事,就拿登录来说说吧。
首先有如下相关封装,很常见,也无需太多解释:
import Foundation
public typealias Response = (_ json: String?, _ error: Error?) -> Void
// 账户体系管理器
public class AccountMgr: NSObject {
private override init() {}
public static let shared = AccountMgr()
}
public extension AccountMgr {
/// 登录
/// - Parameters:
/// - accountType: 账户类型 see `AccountType`
/// - password: 密码
/// - res: 请求结果
func login(by accountType: AccountType, password: String, res: Response?) {
var params = [String: Any]()
switch accountType {
case let .email(email):
params["type"] = "email"
params["email"] = email
case let .mobile(mobile, mobileArea):
params["type"] = "mobile"
params["mobile"] = mobile
params["mobileArea"] = mobileArea
}
params["password"] = password
//网络请求,并回调
//request(type: .post, api: .login, params: params, res: res)
}
}
/// 账号类型
public enum AccountType {
/// 手机号
/// - mobile: 手机号
/// - mobileArea: 国家区号(中国 86)
case mobile(_ phoneNumber: String, mobileArea: String = "86")
/// 邮箱
case email(_ email: String)
}
使用也很方便:
// 分开使用
AccountMgr.shared.login(by: .email(""), password: "", res: nil)
AccountMgr.shared.login(by: .mobile("", mobileArea: ""), password: "", res: nil)
// 合并使用
var loginType: AccountType
if inputEmail {
loginType = .email("test@weixian.com")
} else {
loginType = .mobile("18000000000", mobileArea: "86")
}
AccountMgr.shared.login(by: loginType, password: "xxxxx", res: nil)
无论是邮箱,手机号登录分开逻辑登录,还是统一的登录管理器登录都能胜任,并且只有两种登录,分开写也不会多很多代码。
有一天,这个SDK需要在OC项目中使用
感觉没爱了,懒得想太多,直接废弃了Swift 枚举的便利性,写成了两个方法:
public class AccountMgr: NSObject {
private override init() {}
@objc(shareInstance)
public static let shared = AccountMgr()
}
@objc func loginBy(email: String, password: String, res: Response?)
@objc func loginBy(mobile: String, mobilArea: String, password: String, res: Response?)
之所以写成loginBy(email:)而不是login(by email:),主要是为了swift 转 OC 后使用的时候能直接看懂,也不需要去查看定义,看如下截图就能明白了:
第一个方法不看定义,应该没办法了解参数应该填什么了。
就这样,我的SDK又运行了一段时间,看起来也没什么大问题,无非是手机登录和邮箱登录一定要分开调用罢了
又有一天,这个登录方法要增加用户账号登录
依样画葫芦,我又增加了一个接口~~~,只是这样,那故事就结束了。
可惜,我还有第三方绑定接口,即微信登录后绑定手机,邮箱,或账号、、、、这里又三个接口,还有查询账号信息又三个,还有。。。又三个。。。,还有。。。又三个。。。
这个时候我又开始怀念第一版的接口了,其实这很容易解决,只要一个整型枚举,然后把多出来的参数设置为可选,虽然使用的时候会有点奇怪,但是很好的解决了问题。并且最终我也是这么做的,可我还是想在Swift中能够更好的使用Swfit特性,写出更简洁的代码。。所以我写了两套接口。。。。,一套OC使用,一套Swfit使用,因为我总觉得在不久的将来,我就不需要支持OC了:
首先增加了一个OC的类型枚举:
@objc public enum AccountType_OC: Int {
case mobile
case email
case userId
}
然后增加了一个只有OC可用的方法:
@available(swift 10.0)
@objc func loginBy(accountType: AccountType_OC, account: String, password: String, mobileArea: String?, res: Response?) {
let type = getSwiftAccountType(accountType: accountType, account: account, mobileArea: mobileArea)
login(by: type, password: password, res: res)
}
private func getSwiftAccountType(accountType: AccountType_OC, account: String, mobileArea: String?) -> AccountType {
var type: AccountType
switch accountType {
case .mobile:
guard let mobileArea = mobileArea else { fatalError("need mobile area") }
type = .mobile(account, mobileArea: mobileArea)
case .email:
type = .email(account)
case .userId:
type = .userId(account)
}
return type
}
OC中没办法给参数赋默认值,即类似mobileArea: String = "86" 这种,完全没有用。。。
私有类型转换的方法的封装,使得所有其他方法可以快速转换,关于@available(swift 10.0) 意思就是说只有Swift 版本10.0只后才可以使用。。即变相达到了,在Swift 代码中不会出现这个方法,只有下面方法可以使用:
func login(by accountType: AccountType, password: String, res: Response?)
基本就是这样了,看起来很麻烦,也确实挺麻烦,其实完全可以只保留OC使用的方法,这完全归于我的代码洁癖,以及我自己在使用Swift和对于日后去掉OC支持时我可以快乐的删代码的白日幻想。
当然,如果你只是在自己的混编APP内部封装一些接口,那一套接口应该是比较好的,如果你的是SDK,同时你也不是很怕麻烦,像我这样写也许会有一些意外的收获。
链接:https://www.jianshu.com/p/247c1e923c5c
iOS自定义键盘-简单版
为什么说是简单版,因为这里只说一个数字键盘。
一,怎么自定义键盘
随便一个view都可以作为键盘,主要代码是为你的输入框指定inputView,这个inputView就是键盘,键盘具体什么样子都可以。
kfZNumberKeyBoard * mkb = [kfZNumberKeyBoard moneyKeyBoardBuyer];
UITextField * field = [[UITextField alloc]init];
field.backgroundColor = [UIColor cyanColor];
field.inputView = mkb;
[self.view addSubview:field];
field.frame = CGRectMake(20, NavBottom + 50, DEF_SCREEN_WIDTH - 40, 40);
二,自定义键盘怎么实现各种输入
这里千万不要自己拼接字符串太容易出问题了,用系统自带的方法。我们发现不管UITextField还是UITextView都遵循UITextInput协议,这个协议又遵循UIKeyInput协议,我们用的就是UIKeyInput协议中的方法。
- (void)insertText:(NSString *)text;//插入文字,不用处理光标位置
- (void)deleteBackward;//删除,不用处理光标位置
用这两个方法是不是事情就特别简单了,其实说到这里已经可以了,怎么做都说完了。不过我还是推销一下我写的数字键盘吧。最后面我会贴出代码用的可以拷贝改一下。
三,数字键盘
先看效果图:
a.UI布局上,删除和确定是单独的按键,其他部分我用了collectionView,想着之后做的乱序加密效果好做,打乱数据源刷新一下就行(当然现在没有,不是懒,过渡开发是病)
b.获取当前输入框,这里为了不在外面传,直接在内部监听了输入框开始输入和结束输入。
c.加了几个输入限制:
1.有小数点不能在输入小数点
2.内容为空输入小数点时,前面自动补0
3.最大小数位数限制(测试不多可能有bug哦)
4.移除焦点时小数点前面没东西自动补0
5.输入框有内容确定可以点击,输入框没内容确定不能点击。
下面是代码了:
@interface kfZNumberKeyBoard : UIView
/** 确认按键 */
@property (nonatomic, strong) UIButton * returnButton;
/** 有没有小数点 */
@property (nonatomic, assign) BOOL hiddenPoint;
/** 小数位数,为0不限制,不需要小数时请使用hiddenPoint隐藏点 默认是2 */
@property (nonatomic, assign) NSUInteger decimalCount;
/** 整体高度 */
@property (nonatomic, assign, readonly) CGFloat KFZNumberKeyBoardHeight;
+(instancetype)moneyKeyBoardBuyer;
+(instancetype)moneyKeyBoardSeller;
-(instancetype)initWitHiddenPoint:(BOOL)hiddenPoint;
@end
#import "kfZNumberKeyBoard.h"
#import "KFZKeyBoardCell.h"
@interface kfZNumberKeyBoard ()
@property(nonatomic, weak) UIView * textInputView;
/** 删除按键 */
@property (nonatomic, strong) UIButton * deleteButton;
@property (nonatomic, strong) UICollectionView *collectionView;
@property (nonatomic, strong) NSArray *dataSource;
/** 间隔 */
@property (nonatomic, assign) CGFloat KFZNumberKeyBoardSpace;
/** 数字按键高度 */
@property (nonatomic, assign) CGFloat KFZNumberKeyBoardItemHeight;
@end
@implementation kfZNumberKeyBoard
+(instancetype)moneyKeyBoardBuyer{
kfZNumberKeyBoard * keyBoard = [[kfZNumberKeyBoard alloc]initWitHiddenPoint:NO];
return keyBoard;
}
+(instancetype)moneyKeyBoardSeller{
kfZNumberKeyBoard * keyBoard = [[kfZNumberKeyBoard alloc]initWitHiddenPoint:NO];
keyBoard.returnButton.backgroundColor = [UIColor maintonal_sellerMain];
return keyBoard;
}
-(instancetype)initWitHiddenPoint:(BOOL)hiddenPoint{
self = [super init];
if (self) {
_hiddenPoint = hiddenPoint;
_KFZNumberKeyBoardItemHeight = 50.f;
_KFZNumberKeyBoardSpace = 0.5;
_KFZNumberKeyBoardHeight = _KFZNumberKeyBoardItemHeight * 4 + _KFZNumberKeyBoardSpace * 5 + HOMEINDICATOR_HEIGHT;
_decimalCount = 2;
self.frame = CGRectMake(0, 0, DEF_SCREEN_WIDTH, _KFZNumberKeyBoardHeight);
_deleteButton = [[UIButton alloc]init];
_deleteButton.backgroundColor = [UIColor color_FAFAFA];
[_deleteButton setImage:[UIImage imageNamed:@"keyboard_icon_backspace"] forState:UIControlStateNormal];
[_deleteButton addTarget:self action:@selector(deleteEvent) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:_deleteButton];
[_deleteButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.mas_equalTo(_KFZNumberKeyBoardSpace);
make.right.mas_equalTo(0.f);
make.width.equalTo(self).multipliedBy(0.25);
}];
_returnButton = [[UIButton alloc]init];
[_returnButton setTitle:@"确定" forState:UIControlStateNormal];
[_returnButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
_returnButton.titleLabel.font = [UIFont custemFontOfSize:20 weight:UIFontWeightRegular];
_returnButton.backgroundColor = [UIColor mainTonal_main];
[_returnButton addTarget:self action:@selector(returnEvent) forControlEvents:UIControlEventTouchUpInside];
[self addSubview:_returnButton];
[_returnButton mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.equalTo(_deleteButton.mas_bottom);
make.right.equalTo(_deleteButton);
make.bottom.mas_equalTo(-HOMEINDICATOR_HEIGHT);
make.height.equalTo(_deleteButton);
make.width.equalTo(_deleteButton).offset(_KFZNumberKeyBoardSpace);
}];
//101对应小数点 102对应收起键盘 修改的话其他的判断逻辑也要修改
_dataSource = @[@(1), @(2), @(3), @(4), @(5), @(6), @(7), @(8), @(9), @(101), @(0), @(102)];
UICollectionViewFlowLayout * layout = [[UICollectionViewFlowLayout alloc]init];
layout.itemSize = CGSizeMake((DEF_SCREEN_WIDTH * 3.f/4.f - _KFZNumberKeyBoardSpace*3)/3.f, (_KFZNumberKeyBoardHeight - HOMEINDICATOR_HEIGHT - _KFZNumberKeyBoardSpace*5)/4.f);
layout.sectionInset = UIEdgeInsetsMake(_KFZNumberKeyBoardSpace, 0, _KFZNumberKeyBoardSpace, _KFZNumberKeyBoardSpace);
layout.minimumLineSpacing = _KFZNumberKeyBoardSpace;
layout.minimumInteritemSpacing = _KFZNumberKeyBoardSpace;
_collectionView = [[UICollectionView alloc]initWithFrame:CGRectZero collectionViewLayout:layout];
_collectionView.dataSource = self;
_collectionView.delegate = self;
[_collectionView registerClass:[KFZKeyBoardCell class] forCellWithReuseIdentifier:NSStringFromClass([KFZKeyBoardCell class])];
_collectionView.backgroundColor = [UIColor clearColor];
_collectionView.scrollEnabled = NO;
[self addSubview:_collectionView];
[_collectionView mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.left.mas_equalTo(0.f);
make.bottom.mas_equalTo(-HOMEINDICATOR_HEIGHT);
make.right.equalTo(_deleteButton.mas_left);
}];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textInputViewDidBeginEditing:) name:UITextFieldTextDidBeginEditingNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textInputViewDidEndEditing:) name:UITextFieldTextDidEndEditingNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textInputViewDidBeginEditing:) name:UITextViewTextDidBeginEditingNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textInputViewDidEndEditing:) name:UITextViewTextDidEndEditingNotification object:nil];
}
return self;
}
-(void)dealloc{
[[NSNotificationCenter defaultCenter] removeObserver:self name:UITextFieldTextDidBeginEditingNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UITextFieldTextDidEndEditingNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UITextViewTextDidBeginEditingNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UITextViewTextDidEndEditingNotification object:nil];
}
#pragma mark - response
-(void)textInputWithNumber:(NSNumber *)number{
NSString *strValue = [self inputViewString];
if ([number isEqualToNumber:@(101)]) {
if ([strValue containsString:@"."]){
return;
}else{
if ([strValue length] <= 0)
[self.textInputView insertText:@"0."];
else
[self.textInputView insertText:@"."];
}
}else{
if ([strValue containsString:@"."] && _decimalCount > 0) {
NSInteger pointLocation = [strValue rangeOfString:@"."].location;
NSInteger curDecimalCount = strValue.length - pointLocation - 1;
if (curDecimalCount >= _decimalCount) {
NSInteger cursorLocation = [self inputViewSelectRangeLocation];
if (cursorLocation <= pointLocation) {
[_textInputView insertText:number.stringValue];
}
}else{
[_textInputView insertText:number.stringValue];
}
}else{
[_textInputView insertText:number.stringValue];
}
}
[self freshReturnButtonEnabled];
}
-(void)deleteEvent{
[_textInputView deleteBackward];
[self freshReturnButtonEnabled];
}
-(void)returnEvent{
[_textInputView resignFirstResponder];
}
-(void)textInputViewDidBeginEditing:(NSNotification*)notification{
_textInputView = notification.object;
[self freshReturnButtonEnabled];
}
-(void)textInputViewDidEndEditing:(NSNotification*)notification{
NSString *strValue = [self inputViewString];
if ([strValue startsWithString:@"."]) {
strValue = [NSString stringWithFormat:@"0%@", strValue];
[self setInputViewString:strValue];
}
_textInputView = nil;
}
-(NSString *)inputViewString{
NSString *strValue = @"";
if ([self.textInputView isKindOfClass:[UITextView class]]){
strValue = ((UITextView *)self.textInputView).text;
}else if ([self.textInputView isKindOfClass:[UITextField class]]){
strValue = ((UITextField *)self.textInputView).text;
}
return strValue;
}
-(void)setInputViewString:(NSString *)string{
if ([self.textInputView isKindOfClass:[UITextView class]]){
((UITextView *)self.textInputView).text = string;
}else if ([self.textInputView isKindOfClass:[UITextField class]]){
((UITextField *)self.textInputView).text = string;
}
}
-(NSInteger)inputViewSelectRangeLocation{
NSInteger location = 0;
if ([self.textInputView isKindOfClass:[UITextView class]]){
UITextView * textView = (UITextView *)self.textInputView;
location = textView.selectedRange.location;
}else if ([self.textInputView isKindOfClass:[UITextField class]]){
UITextField *textField = (UITextField *)self.textInputView;
UITextPosition* beginning = textField.beginningOfDocument;
UITextRange* selectedRange = textField.selectedTextRange;
UITextPosition* selectionStart = selectedRange.start;
location = [textField offsetFromPosition:beginning toPosition:selectionStart];
}
return location;
}
-(void)freshReturnButtonEnabled{
NSString *strValue = [self inputViewString];
if (strValue.length == 0) {
_returnButton.enabled = NO;
_returnButton.alpha = 0.6;
}else{
_returnButton.enabled = YES;
_returnButton.alpha = 1.f;
}
}
#pragma mark -- Delegate
#pragma mark - UICollectionViewDataSource
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
return self.dataSource.count;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
KFZKeyBoardCell * cell = [collectionView dequeueReusableCellWithReuseIdentifier:NSStringFromClass([KFZKeyBoardCell class]) forIndexPath:indexPath];
NSNumber * number = self.dataSource[indexPath.row];
if ([number isEqualToNumber:@(101)] && _hiddenPoint) {
cell.textLabel.text = @"";
}else{
cell.textNumber = number;
}
return cell;
}
#pragma mark - UICollectionViewDelegate
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath{
NSNumber * number = self.dataSource[indexPath.row];
if ([number isEqualToNumber:@(102)]) {
[_textInputView resignFirstResponder];
return;
}
if ([number isEqualToNumber:@(101)] && _hiddenPoint) {
return;
}
[self textInputWithNumber:number];
}
#pragma mark - init
-(void)setHiddenPoint:(BOOL)hiddenPoint{
_hiddenPoint = hiddenPoint;
[_collectionView reloadData];
}
@end
这个是里面cell的:
@interface KFZKeyBoardCell : UICollectionViewCell
/** 文字 */
@property (nonatomic, strong) UILabel * textLabel;
/** 图片 */
@property (nonatomic, strong) UIImageView * imageIcon;
/** 设置值 */
@property (nonatomic, strong) NSNumber * textNumber;
@end
#import "KFZKeyBoardCell.h"
@implementation KFZKeyBoardCell
- (instancetype)initWithFrame:(CGRect)frame{
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor color_FAFAFA];
[self.contentView addSubview:self.textLabel];
[self.textLabel mas_makeConstraints:^(MASConstraintMaker *make) {
make.top.bottom.left.right.mas_equalTo(0.f);
}];
self.imageIcon.hidden = YES;
[self.contentView addSubview:self.imageIcon];
[self.imageIcon mas_makeConstraints:^(MASConstraintMaker *make) {
make.center.mas_equalTo(CGPointZero);
make.size.mas_equalTo(CGSizeMake(24.f, 22.f));
}];
}
return self;
}
-(void)prepareForReuse{
self.textLabel.hidden = NO;
self.imageIcon.hidden = YES;
}
- (void)setTextNumber:(NSNumber *)textNumber{
_textNumber = textNumber;
if ([textNumber isEqualToNumber:@(101)]) {
self.textLabel.text = @"·";
}
else if ([textNumber isEqualToNumber:@(102)]){
self.textLabel.hidden = YES;
self.imageIcon.hidden = NO;
self.imageIcon.image = [UIImage imageNamed:@"keyboard_icon_smallkb"];
}
else{
self.textLabel.text = textNumber.stringValue;
}
}
- (UILabel *)textLabel{
if (!_textLabel) {
_textLabel = [[UILabel alloc]init];
_textLabel.font = [UIFont KFZSpecial_DINAlternateBoldWithFontSize:24.f];
_textLabel.textAlignment = NSTextAlignmentCenter;
_textLabel.userInteractionEnabled = NO;
_textLabel.backgroundColor = UIColor.clearColor;
}
return _textLabel;
}
-(UIImageView *)imageIcon{
if (!_imageIcon) {
_imageIcon = [[UIImageView alloc]init];
}
return _imageIcon;
}
@end
转自:https://www.jianshu.com/p/226f67166770
收起阅读 »iOS 设备信息获取
1.获取电池电量(一般用百分数表示,大家自行处理就好)
-(CGFloat)getBatteryQuantity
{
return [[UIDevice currentDevice] batteryLevel];
}
2.获取电池状态(UIDeviceBatteryState为枚举类型)
-(UIDeviceBatteryState)getBatteryStauts
{
return [UIDevice currentDevice].batteryState;
}
3.获取总内存大小
-(long long)getTotalMemorySize
{
return [NSProcessInfo processInfo].physicalMemory;
}
4.获取当前可用内存
-(long long)getAvailableMemorySize
{
vm_statistics_data_t vmStats;
mach_msg_type_number_t infoCount = HOST_VM_INFO_COUNT;
kern_return_t kernReturn = host_statistics(mach_host_self(), HOST_VM_INFO, (host_info_t)&vmStats, &infoCount);
if (kernReturn != KERN_SUCCESS)
{
return NSNotFound;
}
return ((vm_page_size * vmStats.free_count + vm_page_size * vmStats.inactive_count));
}
5.获取已使用内存
- (double)getUsedMemory
{
task_basic_info_data_t taskInfo;
mach_msg_type_number_t infoCount = TASK_BASIC_INFO_COUNT;
kern_return_t kernReturn = task_info(mach_task_self(),
TASK_BASIC_INFO,
(task_info_t)&taskInfo,
&infoCount);
if (kernReturn != KERN_SUCCESS
) {
return NSNotFound;
}
return taskInfo.resident_size;
}
6.获取总磁盘容量
include
-(long long)getTotalDiskSize
{
struct statfs buf;
unsigned long long freeSpace = -1;
if (statfs("/var", &buf) >= 0)
{
freeSpace = (unsigned long long)(buf.f_bsize * buf.f_blocks);
}
return freeSpace;
}
7.获取可用磁盘容量
-(long long)getAvailableDiskSize
{
struct statfs buf;
unsigned long long freeSpace = -1;
if (statfs("/var", &buf) >= 0)
{
freeSpace = (unsigned long long)(buf.f_bsize * buf.f_bavail);
}
return freeSpace;
}
8.容量转换
-(NSString *)fileSizeToString:(unsigned long long)fileSize
{
NSInteger KB = 1024;
NSInteger MB = KB*KB;
NSInteger GB = MB*KB;
if (fileSize < 10) {
return @"0 B";
}else if (fileSize < KB) {
return @"< 1 KB";
}else if (fileSize < MB) {
return [NSString stringWithFormat:@"%.1f KB",((CGFloat)fileSize)/KB];
}else if (fileSize < GB) {
return [NSString stringWithFormat:@"%.1f MB",((CGFloat)fileSize)/MB];
}else {
return [NSString stringWithFormat:@"%.1f GB",((CGFloat)fileSize)/GB];
}
}
9.型号
#import
+ (NSString *)getCurrentDeviceModel:(UIViewController *)controller
{
int mib[2];
size_t len;
char *machine;
mib[0] = CTL_HW;
mib[1] = HW_MACHINE;
sysctl(mib, 2, NULL, &len, NULL, 0);
machine = malloc(len);
sysctl(mib, 2, machine, &len, NULL, 0);
NSString *platform = [NSString stringWithCString:machine encoding:NSASCIIStringEncoding];
free(machine);
if ([platform isEqualToString:@"iPhone3,1"]) return @"iPhone 4 (A1332)";
if ([platform isEqualToString:@"iPhone3,2"]) return @"iPhone 4 (A1332)";
if ([platform isEqualToString:@"iPhone3,3"]) return @"iPhone 4 (A1349)";
if ([platform isEqualToString:@"iPhone4,1"]) return @"iPhone 4s (A1387/A1431)";
if ([platform isEqualToString:@"iPhone5,1"]) return @"iPhone 5 (A1428)";
if ([platform isEqualToString:@"iPhone5,2"]) return @"iPhone 5 (A1429/A1442)";
if ([platform isEqualToString:@"iPhone5,3"]) return @"iPhone 5c (A1456/A1532)";
if ([platform isEqualToString:@"iPhone5,4"]) return @"iPhone 5c (A1507/A1516/A1526/A1529)";
if ([platform isEqualToString:@"iPhone6,1"]) return @"iPhone 5s (A1453/A1533)";
if ([platform isEqualToString:@"iPhone6,2"]) return @"iPhone 5s (A1457/A1518/A1528/A1530)";
if ([platform isEqualToString:@"iPhone7,1"]) return @"iPhone 6 Plus (A1522/A1524)";
if ([platform isEqualToString:@"iPhone7,2"]) return @"iPhone 6 (A1549/A1586)";
if ([platform isEqualToString:@"iPhone8,1"]) return @"iPhone 6s";
if ([platform isEqualToString:@"iPhone8,2"]) return @"iPhone 6s Plus";
if ([platform isEqualToString:@"iPhone8,4"]) return @"iPhone SE";
if ([platform isEqualToString:@"iPhone9,1"]) return @"国行、日版、港行iPhone 7";
if ([platform isEqualToString:@"iPhone9,2"]) return @"港行、国行iPhone 7 Plus";
if ([platform isEqualToString:@"iPhone9,3"]) return @"美版、台版iPhone 7";
if ([platform isEqualToString:@"iPhone9,4"]) return @"美版、台版iPhone 7 Plus";
if ([platform isEqualToString:@"iPhone10,1"]) return @"国行(A1863)、日行(A1906)iPhone 8";
if ([platform isEqualToString:@"iPhone10,4"]) return @"美版(Global/A1905)iPhone 8";
if ([platform isEqualToString:@"iPhone10,2"]) return @"国行(A1864)、日行(A1898)iPhone 8 Plus";
if ([platform isEqualToString:@"iPhone10,5"]) return @"美版(Global/A1897)iPhone 8 Plus";
if ([platform isEqualToString:@"iPhone10,3"]) return @"国行(A1865)、日行(A1902)iPhone X";
if ([platform isEqualToString:@"iPhone10,6"]) return @"美版(Global/A1901)iPhone X";
if ([platform isEqualToString:@"iPod1,1"]) return @"iPod Touch 1G (A1213)";
if ([platform isEqualToString:@"iPod2,1"]) return @"iPod Touch 2G (A1288)";
if ([platform isEqualToString:@"iPod3,1"]) return @"iPod Touch 3G (A1318)";
if ([platform isEqualToString:@"iPod4,1"]) return @"iPod Touch 4G (A1367)";
if ([platform isEqualToString:@"iPod5,1"]) return @"iPod Touch 5G (A1421/A1509)";
if ([platform isEqualToString:@"iPad1,1"]) return @"iPad 1G (A1219/A1337)";
if ([platform isEqualToString:@"iPad2,1"]) return @"iPad 2 (A1395)";
if ([platform isEqualToString:@"iPad2,2"]) return @"iPad 2 (A1396)";
if ([platform isEqualToString:@"iPad2,3"]) return @"iPad 2 (A1397)";
if ([platform isEqualToString:@"iPad2,4"]) return @"iPad 2 (A1395+New Chip)";
if ([platform isEqualToString:@"iPad2,5"]) return @"iPad Mini 1G (A1432)";
if ([platform isEqualToString:@"iPad2,6"]) return @"iPad Mini 1G (A1454)";
if ([platform isEqualToString:@"iPad2,7"]) return @"iPad Mini 1G (A1455)";
if ([platform isEqualToString:@"iPad3,1"]) return @"iPad 3 (A1416)";
if ([platform isEqualToString:@"iPad3,2"]) return @"iPad 3 (A1403)";
if ([platform isEqualToString:@"iPad3,3"]) return @"iPad 3 (A1430)";
if ([platform isEqualToString:@"iPad3,4"]) return @"iPad 4 (A1458)";
if ([platform isEqualToString:@"iPad3,5"]) return @"iPad 4 (A1459)";
if ([platform isEqualToString:@"iPad3,6"]) return @"iPad 4 (A1460)";
if ([platform isEqualToString:@"iPad4,1"]) return @"iPad Air (A1474)";
if ([platform isEqualToString:@"iPad4,2"]) return @"iPad Air (A1475)";
if ([platform isEqualToString:@"iPad4,3"]) return @"iPad Air (A1476)";
if ([platform isEqualToString:@"iPad4,4"]) return @"iPad Mini 2G (A1489)";
if ([platform isEqualToString:@"iPad4,5"]) return @"iPad Mini 2G (A1490)";
if ([platform isEqualToString:@"iPad4,6"]) return @"iPad Mini 2G (A1491)";
if ([platform isEqualToString:@"iPad4,7"]) return @"iPad Mini 3";
if ([platform isEqualToString:@"iPad4,8"]) return @"iPad Mini 3";
if ([platform isEqualToString:@"iPad4,9"]) return @"iPad Mini 3";
if ([platform isEqualToString:@"iPad5,1"]) return @"iPad Mini 4 (WiFi)";
if ([platform isEqualToString:@"iPad5,2"]) return @"iPad Mini 4 (LTE)";
if ([platform isEqualToString:@"iPad5,3"]) return @"iPad Air 2";
if ([platform isEqualToString:@"iPad5,4"]) return @"iPad Air 2";
if ([platform isEqualToString:@"iPad6,3"]) return @"iPad Pro 9.7";
if ([platform isEqualToString:@"iPad6,4"]) return @"iPad Pro 9.7";
if ([platform isEqualToString:@"iPad6,7"]) return @"iPad Pro 12.9";
if ([platform isEqualToString:@"iPad6,8"]) return @"iPad Pro 12.9";
if ([platform isEqualToString:@"iPad6,11"]) return @"iPad 5 (WiFi)";
if ([platform isEqualToString:@"iPad6,12"]) return @"iPad 5 (Cellular)";
if ([platform isEqualToString:@"iPad7,1"]) return @"iPad Pro 12.9 inch 2nd gen (WiFi)";
if ([platform isEqualToString:@"iPad7,2"]) return @"iPad Pro 12.9 inch 2nd gen (Cellular)";
if ([platform isEqualToString:@"iPad7,3"]) return @"iPad Pro 10.5 inch (WiFi)";
if ([platform isEqualToString:@"iPad7,4"]) return @"iPad Pro 10.5 inch (Cellular)";
if ([platform isEqualToString:@"AppleTV2,1"]) return @"Apple TV 2";
if ([platform isEqualToString:@"AppleTV3,1"]) return @"Apple TV 3";
if ([platform isEqualToString:@"AppleTV3,2"]) return @"Apple TV 3";
if ([platform isEqualToString:@"AppleTV5,3"]) return @"Apple TV 4";
if ([platform isEqualToString:@"i386"]) return @"iPhone Simulator";
if ([platform isEqualToString:@"x86_64"]) return @"iPhone Simulator";
return platform;
}
10.IP地址
#import 和#import
- (NSString *)deviceIPAdress {
NSString *address = @"an error occurred when obtaining ip address";
struct ifaddrs *interfaces = NULL;
struct ifaddrs *temp_addr = NULL;
int success = 0;
success = getifaddrs(&interfaces);
if (success == 0) { // 0 表示获取成功
temp_addr = interfaces;
while (temp_addr != NULL) {
if( temp_addr->ifa_addr->sa_family == AF_INET) {
// Check if interface is en0 which is the wifi connection on the iPhone
if ([[NSString stringWithUTF8String:temp_addr->ifa_name] isEqualToString:@"en0"]) {
// Get NSString from C String
address = [NSString stringWithUTF8String:inet_ntoa(((struct sockaddr_in *)temp_addr->ifa_addr)->sin_addr)];
}
}
temp_addr = temp_addr->ifa_next;
}
}
freeifaddrs(interfaces);
return address;
}
11.当前手机连接的WIFI名称(SSID)
需要#import
- (NSString *)getWifiName
{
NSString *wifiName = nil;
CFArrayRef wifiInterfaces = CNCopySupportedInterfaces();
if (!wifiInterfaces) {
return nil;
}
NSArray *interfaces = (__bridge NSArray *)wifiInterfaces;
for (NSString *interfaceName in interfaces) {
CFDictionaryRef dictRef = CNCopyCurrentNetworkInfo((__bridge CFStringRef)(interfaceName));
if (dictRef) {
NSDictionary *networkInfo = (__bridge NSDictionary *)dictRef;
wifiName = [networkInfo objectForKey:(__bridge NSString *)kCNNetworkInfoKeySSID];
CFRelease(dictRef);
}
}
CFRelease(wifiInterfaces);
return wifiName;
}
12.当前手机系統版本
[[[UIDevice currentDevice] systemVersion] floatValue] ;
摘自作者:Cooci_和谐学习_不急不躁
原贴链接:https://www.jianshu.com/p/b25cdf09ece2
WKWebView的特性及原理
WKWebView是在Apple的WWDC 2014随iOS 8和OS X 10.10出来的,是为了解决UIWebView加载速度慢、占用内存大的问题。
使用UIWebView加载网页的时候,我们会发现内存会无限增长,还有内存泄漏的问题存在。
WebKit中更新的WKWebView控件的新特性与使用方法,它很好的解决了UIWebView存在的内存、加载速度等诸多问题。
一、WKWebView新特性
在性能、稳定性、功能方面有很大提升(最直观的体现就是加载网页是占用的内存);
允许JavaScript的Nitro库加载并使用(UIWebView中限制);
支持了更多的HTML5特性;
高达60fps的滚动刷新率以及内置手势;
将UIWebViewDelegate与UIWebView重构成了14类与3个协议查看苹果官方文档;
二、WebKit框架概览
如上图所示,WebKit框架中最核心的类应该属于WKWebView了,这个类专门用来渲染网页视图,其他类和协议都将基于它和服务于它。
WKWebView:网页的渲染与展示,通过WKWebViewConfiguration可以进行自定义配置
WKWebViewConfiguration:这个类专门用来配置WKWebView。
WKPreference:这个类用来进行相关webView设置。
WKProcessPool:这个类用来配置进程池,与网页视图的资源共享有关。
WKUserContentController:这个类主要用来做native与JavaScript的交互管理。
WKUserScript:用于进行JavaScript注入。
WKScriptMessageHandler:这个类专门用来处理JavaScript调用native的方法。
WKNavigationDelegate:网页跳转间的导航管理协议,这个协议可以监听网页的活动
WKNavigationAction:网页某个活动的示例化对象。
WKUIDelegate:用于交互处理JavaScript中的一些弹出框。
WKBackForwardList:堆栈管理的网页列表。
WKBackForwardListItem:每个网页节点对象。
三、WKWebView的属性
/// webView的自定义配置
@property (nonatomic,readonly, copy) WKWebViewConfiguration *configuration;
/// 导航代理
@property (nullable, nonatomic, weak)id navigationDelegate;
/// UI代理
@property (nullable, nonatomic, weak)id UIDelegate;
/// 访问过网页历史列表
@property (nonatomic,readonly, strong) WKBackForwardList *backForwardList;
/// 自定义初始化
- (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration NS_DESIGNATED_INITIALIZER;- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER;
/// url加载webView视图
- (nullable WKNavigation *)loadRequest:(NSURLRequest *)request;
/// 文件加载webView视图
- (nullable WKNavigation *)loadFileURL:(NSURL *)URL allowingReadAccessToURL:(NSURL *)readAccessURL API_AVAILABLE(macosx(10.11), ios(9.0));
/// HTMLString字符串加载webView视图
- (nullable WKNavigation *)loadHTMLString:(NSString *)stringbaseURL:(nullable NSURL *)baseURL;
/// NSData数据加载webView视图
- (nullable WKNavigation *)loadData:(NSData *)data MIMEType:(NSString *)MIMEType characterEncodingName:(NSString *)characterEncodingName baseURL:(NSURL *)baseURL API_AVAILABLE(macosx(10.11), ios(9.0));
/// 返回上一个网页节点
- (nullable WKNavigation *)goToBackForwardListItem:(WKBackForwardListItem *)item;
/// 网页的标题
@property (nullable, nonatomic,readonly, copy) NSString *title;
/// 网页的URL地址
@property (nullable, nonatomic,readonly, copy) NSURL *URL;
/// 网页是否正在加载
@property (nonatomic,readonly, getter=isLoading) BOOL loading;
/// 加载的进度 范围为[0, 1]
@property (nonatomic,readonly)double estimatedProgress;
/// 网页链接是否安全
@property (nonatomic,readonly) BOOL hasOnlySecureContent;
/// 证书服务
@property (nonatomic,readonly, nullable) SecTrustRef serverTrust API_AVAILABLE(macosx(10.12), ios(10.0));
/// 是否可以返回
@property (nonatomic,readonly) BOOL canGoBack;
/// 是否可以前进
@property (nonatomic,readonly) BOOL canGoForward;
/// 返回到上一个网页
- (nullable WKNavigation *)goBack;
/// 前进到下一个网页
- (nullable WKNavigation *)goForward;
/// 重新加载
- (nullable WKNavigation *)reload;
/// 忽略缓存 重新加载
- (nullable WKNavigation *)reloadFromOrigin;
/// 停止加载
- (void)stopLoading;
/// 执行JavaScript
- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void(^ _Nullable)(_Nullableid, NSError * _Nullable error))completionHandler;
/// 是否允许左右滑动,返回-前进操作 默认是NO
@property (nonatomic) BOOL allowsBackForwardNavigationGestures;
/// 自定义代理字符串
@property (nullable, nonatomic, copy) NSString *customUserAgent API_AVAILABLE(macosx(10.11), ios(9.0));
/// 在iOS上默认为NO,标识不允许链接预览
@property (nonatomic) BOOL allowsLinkPreview API_AVAILABLE(macosx(10.11), ios(9.0));
/// 滚动视图
@property (nonatomic,readonly, strong) UIScrollView *scrollView;
/// 是否支持放大手势,默认为NO
@property (nonatomic) BOOL allowsMagnification;
/// 放大因子,默认为1
@property (nonatomic) CGFloat magnification;
/// 据设置的缩放因子来缩放页面,并居中显示结果在指定的点
- (void)setMagnification:(CGFloat)magnification centeredAtPoint:(CGPoint)point;/// 证书列表@property (nonatomic,readonly, copy) NSArray *certificateChain API_DEPRECATED_WITH_REPLACEMENT("serverTrust", macosx(10.11,10.12), ios(9.0,10.0));
四、WKWebView的使用
简单使用,直接加载url地址
WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.bounds];
[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://developer.apple.com/reference/webkit"]]];
[self.view addSubview:webView];
自定义配置
再WKWebView里面注册供JS调用的方法,是通过WKUserContentController类下面的方法:
- (void)addScriptMessageHandler:(id )scriptMessageHandler name:(NSString *)name;
// 创建配置
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
// 创建UserContentController(提供JavaScript向webView发送消息的方法)
WKUserContentController* userContent = [[WKUserContentController alloc] init];
// 添加消息处理,注意:self指代的对象需要遵守WKScriptMessageHandler协议,结束时需要移除
[userContent addScriptMessageHandler:self name:@"NativeMethod"];
// 将UserConttentController设置到配置文件
config.userContentController = userContent;
// 高端的自定义配置创建WKWebView
WKWebView *webView = [[WKWebView alloc] initWithFrame:[UIScreen mainScreen].bounds configuration:config];
// 设置访问的
URLNSURL *url = [NSURL URLWithString:@"https://developer.apple.com/reference/webkit"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[webView loadRequest:request];
[self.view addSubview:webView];
// 实现WKScriptMessageHandler协议方法
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
// 判断是否是调用原生的
if([@"NativeMethod" isEqualToString:message.name]) {
// 判断message的内容,然后做相应的操作
if([@"close" isEqualToString:message.body]) {
}
}
}
注意:上面将当前ViewController设置为MessageHandler之后需要在当前ViewController销毁前将其移除,否则会造成内存泄漏。
[self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"NativeMethod"];
五、WKNavigationDelegate代理方法
如果实现了代理方法,一定要在decidePolicyForNavigationAction和decidePolicyForNavigationResponse方法中的回调设置允许跳转。
typedef NS_ENUM(NSInteger, WKNavigationActionPolicy) {
WKNavigationActionPolicyCancel, // 取消跳转
WKNavigationActionPolicyAllow, // 允许跳转
} API_AVAILABLE(macosx(10.10), ios(8.0));
1.在发送请求之前,决定是否跳转
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void(^)(WKNavigationActionPolicy))decisionHandler {
NSLog(@"1-------在发送请求之前,决定是否跳转 -->%@",navigationAction.request);
decisionHandler(WKNavigationActionPolicyAllow);
}
2. 页面开始加载时调用
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation {
NSLog(@"2-------页面开始加载时调用");
}
3.在收到响应后,决定是否跳转
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void(^)(WKNavigationResponsePolicy))decisionHandler {
/// 在收到服务器的响应头,根据response相关信息,决定是否跳转。decisionHandler必须调用,来决定是否跳转,参数WKNavigationActionPolicyCancel取消跳转,WKNavigationActionPolicyAllow允许跳转 NSLog(@"3-------在收到响应后,决定是否跳转");
decisionHandler(WKNavigationResponsePolicyAllow);
4. 当内容开始返回时调用
- (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation {
NSLog(@"4-------当内容开始返回时调用");
}
5 页面加载完成之后调用
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
NSLog(@"5-------页面加载完成之后调用");
}
6 页面加载失败时调用
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation {
NSLog(@"6-------页面加载失败时调用");
}
7.接收到服务器跳转请求之后调用
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation {
NSLog(@"-------接收到服务器跳转请求之后调用");
}
8.数据加载发生错误时调用
- (void)webView:(WKWebView *)webView didFailNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error {
NSLog(@"----数据加载发生错误时调用");
}
9.需要响应身份验证时调用 同样在block中需要传入用户身份凭证
- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void(^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler {
//用户身份信息 NSLog(@"----需要响应身份验证时调用 同样在block中需要传入用户身份凭证");
NSURLCredential *newCred = [NSURLCredential credentialWithUser:@"" password:@"" persistence:NSURLCredentialPersistenceNone];
// 为 challenge 的发送方提供 credential [[challenge sender] useCredential:newCred forAuthenticationChallenge:challenge];
completionHandler(NSURLSessionAuthChallengeUseCredential,newCred);
}
10.进程被终止时调用
- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView {
NSLog(@"----------进程被终止时调用");
}
六、WKUIDelegate代理方法
/**
* web界面中有弹出警告框时调用
*
* @param webView 实现该代理的webview
* @param message 警告框中的内容
* @param completionHandler 警告框消失调用
*/
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(void(^)())completionHandler {
NSLog(@"-------web界面中有弹出警告框时调用");
}
* 创建新的webView时调用的方法
- (nullable WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures {
NSLog(@"-----创建新的webView时调用的方法");
return webView;
}
// 关闭webView时调用的方法
- (void)webViewDidClose:(WKWebView *)webView {
NSLog(@"----关闭webView时调用的方法");
}
// 下面这些方法是交互JavaScript的方法
// JavaScript调用confirm方法后回调的方法 confirm是js中的确定框,需要在block中把用户选择的情况传递进去
-(void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void(^)(BOOL))completionHandler {
NSLog(@"%@",message);
completionHandler(YES);
}
// JavaScript调用prompt方法后回调的方法 prompt是js中的输入框 需要在block中把用户输入的信息传入
-(void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void(^)(NSString * _Nullable))completionHandler{
NSLog(@"%@",prompt);
completionHandler(@"123");
}
// 默认预览元素调用
- (BOOL)webView:(WKWebView *)webView shouldPreviewElement:(WKPreviewElementInfo *)elementInfo {
NSLog(@"-----默认预览元素调用");
return YES;
}
// 返回一个视图控制器将导致视图控制器被显示为一个预览。返回nil将WebKit的默认预览的行为。
- (nullable UIViewController *)webView:(WKWebView *)webView previewingViewControllerForElement:(WKPreviewElementInfo *)elementInfo defaultActions:(NSArray> *)previewActions {
NSLog(@"----返回一个视图控制器将导致视图控制器被显示为一个预览。返回nil将WebKit的默认预览的行为。");
return self;
}
// 允许应用程序向它创建的视图控制器弹出
- (void)webView:(WKWebView *)webView commitPreviewingViewController:(UIViewController *)previewingViewController {
NSLog(@"----允许应用程序向它创建的视图控制器弹出");
}
// 显示一个文件上传面板。completionhandler完成处理程序调用后打开面板已被撤销。通过选择的网址,如果用户选择确定,否则为零。如果不实现此方法,Web视图将表现为如果用户选择了取消按钮。
- (void)webView:(WKWebView *)webView runOpenPanelWithParameters:(WKOpenPanelParameters *)parameters initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void(^)(NSArray * _Nullable URLs))completionHandler {
NSLog(@"----显示一个文件上传面板");
}
摘自作者:Cooci_和谐学习_不急不躁
原贴链接:https://www.jianshu.com/p/1fd78ec144bb