KVC

原文链接Key-value coding is a mechanism enabled by the NSKeyValueCoding informal protocol that objects adopt to provide indirect access to their properties. When an object is key-value coding compliant, its properties are addressable via string parameters through a concise, uniform messaging interface. This indirect access mechanism supplements the direct access afforded by instance variables and their associated accessor methods.以上是Apple对KVC的定义,翻译过来就是:键值编码是由NSKeyValueCoding非正式协议启用的机制,对象采用该机制提供对其属性的间接访问。 当对象符合键值编码时,其属性可通过字符串参数通过简洁,统一的消息传递接口寻址。 这种间接访问机制补充了实例变量及其相关访问器方法提供的直接访问。所有直接或者间接继承NSObject的对象都遵守 NSKeyValueCoding 协议,并默认提供实现,找到 NSKeyValueCoding.h 我们可以看到里面为这些类添加了一些extension@interface NSObject(NSKeyValueCoding)@interface NSArray<ObjectType>(NSKeyValueCoding)@interface NSDictionary<KeyType, ObjectType>(NSKeyValueCoding)@interface NSMutableDictionary<KeyType, ObjectType>(NSKeyValueCoding)@interface NSOrderedSet<ObjectType>(NSKeyValueCoding)@interface NSSet<ObjectType>(NSKeyValueCoding)下面是一些主要的方法:- (nullable id)valueForKey:(NSString *)key;- (void)setValue:(nullable id)value forKey:(NSString *)key;- (nullable id)valueForKeyPath:(NSString *)keyPath; - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;- (nullable id)valueForUndefinedKey:(NSString *)key;- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;实践是检验真理的唯一标准,下面创建一个ComandLine工程实践一下Xcode -> New -> MacOS -> CommandLine创建Person类关于KVC要分为两部分,集合类型与非集合类型,下面先来看非集合类型Person.h#import <Foundation/Foundation.h>NS_ASSUME_NONNULL_BEGIN@interface Person : NSObject@property (nonatomic ,copy) NSString *name;@property (nonatomic ,assign) NSUInteger age;@property (nonatomic ,copy) NSArray<Person *> *friends;@endNS_ASSUME_NONNULL_ENDPerson.m#import “Person.h”@implementation Person- (instancetype)init { self = [super init]; if (self) { _name = @""; _age = 0; _friends = @[]; } return self;}// 重写以便打印对象的属性- (NSString *)description { return [NSString stringWithFormat:@"- name: %@, age: %ld, friends: %@",self.name, self.age, self.friends];}@end打开main.m,创建两个Person的实例Alice和Bob#import <Foundation/Foundation.h>#import “Person.h"int main(int argc, const char * argv[]) { @autoreleasepool { Person *Alice = Person.new; Alice.name = @“Alice”; Alice.age = 18; Alice.friends = @[]; Person Bob = Person.new; [Bob setValue:@“Bob” forKey:@“name”]; [Bob setValue:@(28) forKey:@“age”]; [Bob setValue:@[Alice] forKey:@“friends”]; NSLog(@”%@",@[Alice,Bob]); NSLog(@"%@",[Bob valueForKeyPath:@“friends”]); } return 0;}其中,ALice使用正常的set访问器进行赋值,Bob使用KVC方式赋值,Command + R,能够看到如下输出( “- name: Alice, age: 18, friends: (\n)”, “- name: Bob, age: 28, friends: (\n "- name: Alice, age: 18, friends: (\n)"\n)”)( “- name: Alice, age: 18, friends: (\n)")输出正如我们想的那样,通过KVC,正确的对Bob的name,age,friends进行了赋值和取值操作那么若果想直接获取Bob名字的长度呢id nameLength = [Bob valueForKeyPath:@“name.length”];有没有发现这种方式很便捷呢,后面还有更有趣的用法。接下来想一个问题,如果我想使用KVC进行赋值但是相应的类并没有对应的属性怎么办[Bob setValue:@“Bob” forKey:@“familyName”];运行就会崩溃掉,而在生产环境的情况下我们可不想让App Crash,可以看下报错信息 Terminating app due to uncaught exception ‘NSUnknownKeyException’, reason: ‘[<Person 0x1030063b0> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key familyName.‘可以发现导致崩溃的原因是Person类没有实现这个方法 setValue:forUndefinedKey:接下来在Person.m里面添加这个方法的实现- (void)setValue:(id)value forUndefinedKey:(NSString )key { }可以看到运行之后不会崩溃了上面简单介绍了一下非集合类型,下面先来看集合类型首先new一个ArrayNSArray family = @[Alice,Bob];NSLog(@”%@",[family valueForKey:@“count”]);运行后发现又又又crash了, Terminating app due to uncaught exception ‘NSUnknownKeyException’, reason: ‘[<Person 0x100703db0> valueForUndefinedKey:]: this class is not key value coding-compliant for the key count.‘按照正常的理解,我们无非是想回去Array的count属性,为什么会崩溃呢,原来对集合类型进行KVC操作时是分别对集合内对象进行操作,NSLog(@"%@",[family valueForKey:@“name”]);现在的输出是一个Array( Alice, Bob)使用KVC对集合类型内部对象赋值也是一样的NSArray *family = @[Alice,Bob];[family setValue:@“NoName” forKey:@“name”];NSLog(@"%@",family);当然,你也可以自己重写上述的方法来使用,这里就不多赘述。下面来还有一些更有趣的KVC集合操作符可以看到对于集合操作,在keypath中间我们可以加入一个集合操作符,能够实现更为便捷的骚操作集合操作符有一下几种@avg // 算数平均值@count // 计数@max // 最大值@min // 最小值@sum // 求和@unionOfObjects // 创建并返回一个数组,该数组包含与右键路径指定的属性对应的集合的所有对象。@distinctUnionOfObjects // 同unionOfObjects,但会对数组去重@unionOfArrays // 创建并返回一个数组,该数组包含与右键路径指定的属性对应的所有集合的组合的对象。@distinctUnionOfArrays // 同unionOfArrays,但会对数组去重NSLog(@"%@",[family valueForKeyPath:@"@max.age"]);NSLog(@"%@",[family valueForKeyPath:@"@min.age"]);NSLog(@"%@",[family valueForKeyPath:@"@sum.age"]);NSLog(@"%@",[family valueForKeyPath:@"@avg.age"]);这里可以看到获取到了Alice和Bob年龄的最大值,最小值,和,平均值还有一种特殊场景,就是我想获取这个family的所有friends怎么做呢NSLog(@"%@",[family valueForKeyPath:@"@unionOfObjects.friends"]);( ( ), ( “- name: Alice, age: 18, friends: (\n)” ))可以看到打印似乎有些不对劲,我们想要的是所有的friends,但数组中却是Alice的friends,Bob的friends两个对象。这时候我们可以使用unionOfArrays,这个操作符会帮我们将数组元素【铺平】NSLog(@"%@",[family valueForKeyPath:@"@unionOfArrays.friends"]);一句代码即展开获取了你所需要的Array,否则你只能写一些胶水代码NSArray *arr = [Alice.friends arrayByAddingObjectsFromArray:Bob.friends];不够优雅基本的KVC就介绍这么多,如果有更生层次的自定义实现需求的话还是推荐多看看Apple的文档,不过记得使用KVC的话当对象更新了property不要忘记顺便也要改下keypath ...

March 31, 2019 · 2 min · jiezi

KVO

原文链接Key-Value Observing 键值观察 ,是一种设计模式观察者模式的实现官方定义键值观察提供了一种机制,允许对象通知其他对象的特定属性的更改。它对应用程序中模型和控制器层之间的通信特别有用。(在OS X中,控制器层绑定技术严重依赖于键值观察。)控制器对象通常观察模型对象的属性,视图对象通过控制器观察模型对象的属性。然而,另外,模型对象可以观察其他模型对象(通常用于确定从属值何时改变)或甚至自身(再次确定从属值何时改变)。先看下使用KVO的姿势Xcode -> New -> MacOS -> CommandLine 新建工程,创建Person类Person.h#import <Foundation/Foundation.h>NS_ASSUME_NONNULL_BEGIN@interface Person : NSObject@property (nonatomic ,copy) NSString *name;@property (nonatomic ,assign) NSUInteger age;@property (nonatomic ,copy) NSArray<Person *> *friends;@endNS_ASSUME_NONNULL_ENDPerson.m#import “Person.h”@implementation Person- (instancetype)init { self = [super init]; if (self) { _name = @""; _age = 0; _friends = @[]; } return self;}- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context { // 当收到通知的时候打印观察的对象,旧值和新值 NSLog(@"\nReceving ObserveValueChanged \nObject: %@ OldValue: %@, NewValue: %@",object, change[NSKeyValueChangeOldKey], change[NSKeyValueChangeNewKey]); change[@“new”]);}// 重写以便打印对象的属性- (NSString *)description { return [NSString stringWithFormat:@"- name: %@, age: %ld, friends: %@",self.name, self.age, self.friends];}@end打开main.m,创建Alice和Bob,设置Bob观察Alice的age属性Person *Alice = Person.new;Alice.name = @“Alice”;Alice.age = 18;Person *Bob = Person.new;Bob.name = @“Bob”;Bob.age = 28;[Alice addObserver:Bob forKeyPath:@“age” options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:PersonChangeContext];Alice.age = 100;[Alice removeObserver:Bob forKeyPath:@“age”];Alice.age = 200;可以看到控制台输出了一次Alice的age属性的前后变化.其中,NSKeyValueObservingOptions 有以下几个选项,可以使用 | 符号组合使用NSKeyValueObservingOptionNew // 收到通知时change字典将包含新值NSKeyValueObservingOptionOld // 收到通知时change字典将包含旧值NSKeyValueObservingOptionInitial // 在addObserver时会发送通知,change字典将包含初始值NSKeyValueObservingOptionPrior // 在所观察keyPath改变之前将收到通知change字典的key值在这里FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeKindKey;FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNewKey;FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeOldKey;FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeIndexesKey;FOUNDATION_EXPORT NSKeyValueChangeKey const NSKeyValueChangeNotificationIsPriorKey KVO的使用场景有很多,比如Person拥有一个account属性,person需要获取account的变化该如何处理呢,轮训或许是个办法,但是无疑效率低下,而且很不安全,更合理的办法就是使用KVO观察account,在account发生变化时更新。原理现在我们已经知其然了,但是还不知其所以然。先说结论系统通过runtime生成继承与被观察者的新类(NSKVONotifying_Person),对原对象进行isa-swizzing(isa混写)根据KVC(键值编码)对对象的keypath进行hock在将要对属性进行赋值操作时发送通知如何证明上述结论呢,上代码!#import <Foundation/Foundation.h>#import <objc/runtime.h>#import “Person.h"static void *PersonChangeContext = &PersonChangeContext;int main(int argc, const char * argv[]) { @autoreleasepool { Person *Alice = Person.new; Alice.name = @“Alice”; Alice.age = 18; Person *Bob = Person.new; Bob.name = @“Bob”; Bob.age = 28; Class cls0 = object_getClass(Alice); [Alice addObserver:Bob forKeyPath:@“age” options:NSKeyValueObservingOptionOld|NSKeyValueObservingOptionNew context:PersonChangeContext]; Alice.age = 100; Class cls1 = object_getClass(Alice); NSLog(@”%@ %@",cls0,cls1); } return 0;}首先引入runtime,这样我们可以打印isa所指向的真实的类,在向Alice添加观察者之前,先获取Class在向Alice添加观察者之后,获取ClassPerson NSKVONotifying_Person可以看到输出确实如此这就是KVO的核心思路了,关于KVC请看这里. ...

March 31, 2019 · 1 min · jiezi

NSProxy&NSTimer

原文链接开发过程中我们必不可少的需要接触定时器,在iOS中,常用的定时器有以下几种:GCD TimerCADisplayLinkNSTimer这里我们主要来看下 NSTimer 的一个问题#import “ViewController.h”@interface ViewController ()@property (nonatomic, strong) NSTimer *t;@end@implementation ViewController- (void)viewDidLoad { [super viewDidLoad];}- (void)startTImer { _t = [NSTimer timerWithTimeInterval:1.0f target:self selector:@selector(someBussiness) userInfo:nil repeats:true]; [[NSRunLoop currentRunLoop] addTimer:_t forMode:NSRunLoopCommonModes];}- (void)someBussiness { NSLog(@“timer triggered”);}- (void)dealloc { NSLog(@“Controller dealloc”); if (self.t) { [self.t invalidate]; }}- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { if (self.presentingViewController) { [self dismissViewControllerAnimated:true completion:nil]; }else { ViewController *vc = ViewController.new; vc.view.backgroundColor = UIColor.grayColor; [self presentViewController:vc animated:true completion:nil]; [vc startTImer]; }}@end这里在我们点击页面之后,会present出来一个新页面,并开始使用 NSTimer 计时,并在 dealloc 中打印信息。再次点击present出来的viewController把当前的Controller销毁掉。再次点击我们会发现,计时器并没有停止,而且预期的dealloc中的信息也并没有打印,这是为什么呢?这里我们可以使用Xcode的 Debug Memory Graph ,就在下方控制台上面的按键里面,可以看到如图所示我们可以看到这里 Runloop 引用了 timer ,而 timer 又引用了当前的Controller,最终导致Controller无法释放我们通常会想,那把 NSTimer 的 property 用weak来修饰,或者把timer的target使用 weak 修饰不就好了吗。那我们来修改一下代码@property (nonatomic, weak) NSTimer *t;- (void)startTImer { __weak typeof(self) ws = self; _t = [NSTimer timerWithTimeInterval:1.0f target:ws selector:@selector(someBussiness) userInfo:nil repeats:true]; [[NSRunLoop currentRunLoop] addTimer:_t forMode:NSRunLoopCommonModes];}这里我们修改timer的property为weak,把target也修饰为weak,再次运行。哈 , 还是没有释放,timer 仍在打印。这里其实是因为Runloop会对加入的Timer自动强引用 , 而timer会对target进行强引用,即使修饰为weak也没用,那么,有咩有什么办法来释放他呢?- (void)startTImer { __weak typeof(self) ws = self; _t = [NSTimer timerWithTimeInterval:1.0f repeats:true block:^(NSTimer * _Nonnull timer) { [ws someBussiness]; }]; [[NSRunLoop currentRunLoop] addTimer:_t forMode:NSRunLoopCommonModes];}???? ???? ???? 改为Block调用的方式就可以了,那么有没有别的方式也可以解决这个问题呢?(当然有了要不这篇我tm是在写啥)NSProxyAn abstract superclass defining an API for objects that act as stand-ins for other objects or for objects that don’t exist yet.一个抽象超类,用于定义对象的API,这些对象充当其他对象或尚不存在的对象的替身。官方文档使用NSProxy我们可以把任意的对象隐藏在后面,由这个抽象类在前面为我们真实的对象代理,当然,我们需要实现两个方法- (void)forwardInvocation:(NSInvocation *)invocation;- - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel;第一个是方法决议,我们可以在这里改变方法的指针,更换方法,第二个是方法签名,用来提供相应的函数返回类型和参数,接下来我们新建 TimerProxy 类 继承 NSProxyTimerProxy.h#import <Foundation/Foundation.h>NS_ASSUME_NONNULL_BEGIN@interface TimerProxy : NSProxy+ (instancetype)proxyWithObject:(id)obj;@endNS_ASSUME_NONNULL_ENDTimerProxy.m#import “TimerProxy.h”@interface TimerProxy ()@property (nonatomic, weak) id object;@end@implementation TimerProxy- (instancetype)withProxy:(id)obj { _object = obj; return self;}+ (instancetype)proxyWithObject:(id)obj { return [[self alloc] withProxy:obj];}- (void)forwardInvocation:(NSInvocation *)invocation { SEL selector = invocation.selector; if ([_object respondsToSelector:selector]) { [invocation invokeWithTarget:_object]; }}- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel { return [_object methodSignatureForSelector:sel];}@end再更新一下viewController的实现#import “ViewController.h”#import “TimerProxy.h”@interface ViewController ()@property (nonatomic, strong) NSTimer *t;@end@implementation ViewController- (void)viewDidLoad { [super viewDidLoad];}- (void)startTImer { _t = [NSTimer timerWithTimeInterval:1.0f target:[TimerProxy proxyWithObject:self] selector:@selector(someBussiness) userInfo:nil repeats:true]; [[NSRunLoop currentRunLoop] addTimer:_t forMode:NSRunLoopCommonModes];}- (void)someBussiness { NSLog(@“timer triggered”);}- (void)dealloc { NSLog(@“Controller dealloc”); if (self.t) { [self.t invalidate]; }}- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { if (self.presentingViewController) { [self dismissViewControllerAnimated:true completion:nil]; }else { ViewController *vc = ViewController.new; vc.view.backgroundColor = UIColor.grayColor; [self presentViewController:vc animated:true completion:nil]; [vc startTImer]; }}@end应该可以看到正常的dealloc的输出,并且timer也停止了,NSProxy是一个非常有用的抽象类,当然还有其他用途,比如能够模拟多继承,待后续补充。 ...

March 31, 2019 · 2 min · jiezi

Runtime小结

原文链接首先,为什么说ObjC是动态语言我们看下苹果官方文档对runtime的定义The Objective-C runtime is a runtime library that provides support for the dynamic properties of the Objective-C language, and as such is linked to by all Objective-C apps. Objective-C runtime library support functions are implemented in the shared library found at /usr/lib/libobjc.A.dylib.译文如下Objective-C运行时是一个运行时库,它提供对Objective-C语言的动态属性的支持,因此被所有Objective-C应用程序链接。 Objective-C运行时库支持函数在/usr/lib/libobjc.A.dylib中的共享库中实现。在Objective-C中,消息直到运行时才绑定到方法实现。编译器将把方法调用转化为消息发送例如如下代码[receiver message]将会被转化为这种调用方式objc_msgSend(receiver, selector)在消息需要绑定参数的时候会转化如下objc_msgSend(receiver, selector, arg1, arg2, …)那么抓花为发送消息之后都做了什么呢?[receiver message]通过receiver的 isa 指针 查找它的 Class查找 Class 下的 methodLists如果 methodLists 没有相应的方法则递归查找 superClass 的 methodLists如果在 methodLists 里面找到了 对应的 message 则 获取实现指针 imp 并执行发送方法返回值这里我们发现还缺少了一种情况,那就是递归在父类的methodlist里面也没有找到对应的实现,这个时候就会报错 unrecognized selector send to instance X消息转发Runtime 为这种可能提供了最后的机会,就是触发消息转发流程resolveClassMethod/resolveInstanceMethod ,向对象发送其无法识别的消息后会触发,在这里可以动态添加方法的实现forwardingTargetForSelector ,快速转发,可以把对应的消息发送给其他对象methodSignatureForSelector ,对方法进行签名(为完整的消息转发做准备)forwardInvocation ,进行完整的消息转发(包括修改实际方法,对象等)doesNotRecognizeSelector , 最后如果还未执行方法,就会抛出错误Show Me The Code:动态添加方法:#import “AViewController.h”#import <objc/runtime.h>@interface AViewController ()@end@implementation AViewController- (void)viewDidLoad { [super viewDidLoad]; [self performSelector:@selector(speak)];}+ (BOOL)resolveInstanceMethod:(SEL)sel { if (sel == @selector(speak)) { class_addMethod([self class], sel, (IMP)fakeSpeak, “v@:”); // 关于最后一个参数可以看https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html return true; } return [super resolveInstanceMethod:sel];}void fakeSpeak(id target, SEL _cmd){ NSLog(@“method added”);}@end快速转发#import “AViewController.h”#import <objc/runtime.h>@interface AViewController ()@end@implementation AViewController- (void)viewDidLoad { [super viewDidLoad]; [self performSelector:@selector(speak)];}- (id)forwardingTargetForSelector:(SEL)aSelector { if (aSelector == @selector(speak)) { return [XXXX new]; } return nil;}@end完整转发#import “AViewController.h”#import <objc/runtime.h>@interface AViewController ()@end@implementation AViewController- (void)viewDidLoad { [super viewDidLoad]; [self performSelector:@selector(speak)];}- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { if (aSelector == @selector(speak)) { return [NSMethodSignature signatureWithObjCTypes:“v@:”]; } return [super methodSignatureForSelector:aSelector];}- (void)forwardInvocation:(NSInvocation *)anInvocation { [anInvocation setSelector:@selector(otherMethod)]; [anInvocation invokeWithTarget:self];}- (void)otherMethod{ NSLog(@"%s",func);}@end对消息转发的流程有了一些基本概念以后我们就可以稍微深入看看方法交换这个理念了。方法交换有的时候我们可能会面对一些需求,比如在每个页面中统一都做的一些处理,像访问埋点等逻辑,如果一个一个去改写的话十分麻烦,用继承的方式去做慢慢会产生各种耦合的情况,这里,我们可以使用方法交换的方式去统一添加处理。比如我们需要在每一个 ViewController viewDidLoad 的方法中输出一个log先创建一个 category#import “UIViewController+Log.h”#import <objc/runtime.h>@implementation UIViewController (Log)static void AGExchangeMethod(Class cls, SEL originSelector, SEL newSelector) { Method originMethod = class_getInstanceMethod(cls, originSelector); Method newMethod = class_getInstanceMethod(cls, newSelector); // method_exchangeImplementations(newMethod, originMethod); BOOL addMethod = class_addMethod(cls, originSelector, method_getImplementation(newMethod), method_getTypeEncoding(newMethod)); if (addMethod) { class_replaceMethod(cls, newSelector, method_getImplementation(originMethod), method_getTypeEncoding(originMethod)); }else { method_exchangeImplementations(newMethod, originMethod); }}+ (void)load { static dispatch_once_t once; dispatch_once(&once, ^{ AGExchangeMethod([self class], @selector(viewDidLoad), @selector(Logging)); });}- (void)Logging{ NSLog(@"%s",func); [self Logging];}@end编译运行,你可以看到控制台会输出 Logging 这里有几个地方需要特别留意下如果是交换的系统方法,在新的方法内部一定要再调用这个方法一次,因为这个时候方法的imp指针已经交换,调用该方法就是调用系统方法,为什么要这么做呢,因为这些系统的方法是黑盒的,有很多我们不清楚的操作,如果不调用可能会给程序带来问题执行交换方法的最佳时机是在类方法load中, 该方法会在类被加载的时候执行一定要使用GCD或其他办法保证交换的线程安全,仅执行一次,防止出现错误。关联对象比如我们想要为 UIViewController 添加一个flag属性记录状态,但是无法更改 UIViewController,那么我们可以在 category 中添加属性#import <UIKit/UIKit.h>NS_ASSUME_NONNULL_BEGIN@interface UIViewController (Log)@property (nonatomic ,copy) NSString *flag;@endNS_ASSUME_NONNULL_END然后在其他的 viewController 中使用- (void)viewDidLoad { [super viewDidLoad]; self.flag = @“active”;}运行后可以看到崩溃 unrecognized selector sent to instance ,这是因为在 category 中 property修饰符并不会自动为我们生成成员变量,而我们知道,属性其实是 ivar + getter & setter ,所以我们可以使用 runtime 来手动关联:在 category 的 .m 文件中增加以下代码- (void)setFlag:(NSString *)flag { objc_setAssociatedObject(self, @selector(flag), flag, OBJC_ASSOCIATION_COPY);}- (NSString *)flag { return objc_getAssociatedObject(self, _cmd);}然后就可以在其他 viewController 中随意使用了,由于 objc_setAssociatedObject 也是在ARC管理之下的所以我们也不必手动释放。写在最后虽然 Runtime 有诸多魔幻的使用方法,但是不建议过多的使用(除非掌握的很熟练),除非是开发框架,否则多个互相交换的方法和动态的属性在调试的时候会很无奈的。。。 ...

March 31, 2019 · 2 min · jiezi

MVVM

原文链接目前客户端最流行的架构应该就是MVVM,然而在看了一些文章之后发现大部分是理论而并没有仔细讲解具体的架构方法和实践,这篇博客说说我在实际工作中的使用。引言提到MVVM我们不得不先来认识一下MVC:MVC模式(Model–view–controller)是软件工程中的一种软件架构模式,把软件系统分为三个基本部分:模型(Model)、视图(View)和控制器(Controller)。MVC模式最早由Trygve Reenskaug在1978年提出[1],是施乐帕罗奥多研究中心(Xerox PARC)在20世纪80年代为程序语言Smalltalk发明的一种软件架构。MVC模式的目的是实现一种动态的程序设计,使后续对程序的修改和扩展简化,并且使程序某一部分的重复利用成为可能。除此之外,此模式通过对复杂度的简化,使程序结构更加直观。软件系统通过对自身基本部分分离的同时也赋予了各个基本部分应有的功能。专业人员可以通过自身的专长分组:控制器(Controller)- 负责转发请求,对请求进行处理。视图(View) - 界面设计人员进行图形界面设计。模型(Model) - 程序员编写程序应有的功能(实现算法等等)、数据库专家进行数据管理和数据库设计(可以实现具体的功能)。MVVMMVVM是Model-View-ViewModel的简写,最早是由微软公司提出并运用,是MVP(Model-View-Presenter)模式与WPF结合的应用方式时发展演变过来的一种新型架构架构。MVVM有助于将图形用户界面的开发与业务逻辑或后端逻辑(数据模型)的开发分离开来,这是通过置标语言或GUI代码实现的。MVVM的视图模型是一个值转换器,这意味着视图模型负责从模型中暴露(转换)数据对象,以便轻松管理和呈现对象。在这方面,视图模型比视图做得更多,并且处理大部分视图的显示逻辑。视图模型可以实现中介者模式,组织对视图所支持的用例集的后端逻辑的访问。模型 模型是指代表真实状态内容的领域模型(面向对象),或指代表内容的数据访问层(以数据为中心)。视图 就像在MVC和MVP模式中一样,视图是用户在屏幕上看到的结构、布局和外观(UI)。视图模型 视图模型是暴露公共属性和命令的视图的抽象。MVVM没有MVC模式的控制器,也没有MVP模式的presenter,有的是一个绑定器。在视图模型中,绑定器在视图和数据绑定器之间进行通信。绑定器 声明性数据和命令绑定隐含在MVVM模式中。在Microsoft解决方案堆中,绑定器是一种名为XAML的标记语言。绑定器使开发人员免于被迫编写样板式逻辑来同步视图模型和视图。在微软的堆之外实现时,声明性数据绑定技术的出现是实现该模式的一个关键因素。MVVM的优点解决controller过于臃肿在MVC中很容易就会把一些业务逻辑,网络请求,数据IO都放在controller中 注意,这里不是说MVC的控制器一定很臃肿,而是「容易变得臃肿」 在我们新建一个工程的时候,苹果会自动帮我们生成一个ViewController,而在动手开始写代码的时候,往往控制不住就直接将逻辑写在Controller中。MVVM架构会要求我们把任何与非View的逻辑玻璃出来,Controller中除了绑定viewModel之外的代码只允许出现对View的操作。因为Controller对我们来说也只是一个View。逻辑分离就像上面说的,业务逻辑都会抽离出来放在viewModel中 ,这样可以在任何地方重用这一堆业务 除此之外,我们的代码将会更加易于测试,避免出现在MVC中可能出现的那种超长的方法、严重依赖全局状态导致难以测试的问题。View重用 (可拥有view单独的viewModel)在MVVM中,View只需要与ViewMode交互,不会收到其他的影响,所以不但vm、m 可以重用,view一样可以重复使用,修改的时候也更加方便。缺点BUG与传递由于在MVVM里面View和ViewModel是松耦合的,在测试出问题的时候就要排查各个地方的问题,有可能是vm中的也有可能是view中的。由于vm会传递数据,一个bug会很容易的传递到其他地方,引发更大的问题。并且其中一个地方出现问题的话,这个BUG就极有可能随着传递到其他的逻辑中,从而导致更严重的问题发生。需要维护Model,viewModel,和view的开销,controller相当于被抽象成View。额外的viewModel使用也并不是无代价的,有可能由于各种原因导致管理起来稍微复杂。而额外的,如果因为强引用或其他原因导致的循环引用等内存不能正确释放的情况下,有可能会内存疯涨,所以需要确保你的使用方式是无副作用的。使用方法上面说了这些只是一个大致的介绍,我们还是来看看应该怎样使用吧。你可以使用delegate的方式或者block的方式对view和viewModel进行桥接,在这里我们选择使用delegate,我认为这样看着比较直观,在代码中也更加明确。首先创建一个工程,选择singleViewApplication,我们就以最常见的的登陆功能作为示例新建用于由viewModel调用,view进行响应的protocol首先要起个名字,就叫LoginViewModelDelegateProtocol 吧@protocol LoginViewModelDelegateProtocol <NSObject>@end好,让我们想一想view会发送一些什么数据给VM ,VM都需要什么数据。对于简单登陆的VM来说,我们需要通知view的数据和方法登陆成功错误提示按钮状态改变(是否可以点击)那么我们可以在protocol中添加方法了@protocol LoginViewModelDelegateProtocol <NSObject>- (void)loginSuccess;- (void)showTips:(NSString *)tip;- (void)buttonEnable:(BOOL )enable;@end新建用于由view调用,viewModel进行响应的protocol同理,我们只要确认vm和v需要交换的数据就好了。用户名输入框的字符串密码输入框的字符串点击登陆事件@protocol LoginViewModelInterfaceProtocol <NSObject>- (void)inputUserName:(NSString *)uname;- (void)inputPwd:(NSString *)pwd;- (void)didTapLoginBUtton;@end实现代理方法下面我们新建一个viewModel叫做LoginViewModelLoginViewModel.h#import <Foundation/Foundation.h>#import “LoginViewModelDelegateProtocol.h”#import “LoginViewModelInterfaceProtocol.h”@interface LoginViewModel : NSObject<LoginViewModelInterfaceProtocol>@property (nonatomic ,weak) id<LoginViewModelDelegateProtocol> delegate;@endLoginViewModel.m#import “LoginViewModelDelegateProtocol.h”@interface LoginViewModel ()@property (assign, nonatomic) BOOL unameValid;@property (assign, nonatomic) BOOL pwdValid;@end@implementation LoginViewModel- (void)inputUserName:(NSString *)uname { self.unameValid = uname.length>0; [self judgeAllValid];}- (void)inputPwd:(NSString *)pwd { self.pwdValid = pwd.length>0; [self judgeAllValid];}- (void)didTapLoginBUtton { // 一些请求,这里忽略网络请求,直接模拟结果 [self.delegate loginSuccess];}- (void)judgeAllValid { BOOL v = [self isAllValid]; [self.delegate buttonEnable:v];}- (BOOL)isAllValid { return self.unameValid && self.pwdValid;}@end然后在controller初始化,并且实现全部的方法就可以了。viewController.m#import “viewController.h”…@interface viewController () <LoginViewModelDelegateProtocol>@property (nonatomic ,strong) LoginViewModel *vm;@end@implementation TDFSetPhoneNumController- (void)viewDidLoad { [super viewDidLoad]; self.vm.delegate = self;}#pragma mark - VMDelegate- (void)loginSuccess { [self.navigationController pushViewController:[SuccessVC new]] animated:true];}- (void)showTips:(NSString *)tip { [self showAlert:tip];}- (void)buttonEnable:(BOOL )enable { self.loginbutton.enable = enable;}#pragma mark - Getter- (LoginViewModel *)vm { if (!_vm) { _vm = [LoginViewModel new]; } return _vm;}@end大功告成这里全部的代码是我手写的,后面省略了一些UIKit相关的布局,想必以你的聪明才智应该已经能很轻松的将剩余的补全了吧。注意事项这里有几点我认为应该注意的:vm与v之间不论通过什么传递值和响应,都要保持数据的单向流动。代理用weak,内存回收时会自动置为nil,全部model与viewModel中不应包含任何UIKit框架下的类总结总而言之,我认为MVVM在我们的代码整体分工和应用架构的过程中应用还是十分优雅和安全的,不过话说回来什么架构也罢,还是要看我们怎么去用它,不是吗 ...

March 31, 2019 · 1 min · jiezi

RunLoop

原文链接什么是RunLoop?A RunLoop object processes input for sources such as mouse and keyboard events from the window system, Port objects, and NSConnection objects. A RunLoop object also processes Timer events.Your application neither creates or explicitly manages RunLoop objects. Each Thread object—including the application’s main thread—has an RunLoop object automatically created for it as needed. If you need to access the current thread’s run loop, you do so with the class method current.Note that from the perspective of RunLoop, Timer objects are not “input”—they are a special type, and one of the things that means is that they do not cause the run loop to return when they fire.官方文档翻译如下RunLoop对象处理来自窗口系统,Port对象和NSConnection对象的源(如鼠标和键盘事件)的输入。 RunLoop对象还处理Timer事件。 您的应用程序既不创建也不显式管理RunLoop对象。每个Thread对象(包括应用程序的主线程)都会根据需要自动为其创建RunLoop对象。如果需要访问当前线程的运行循环,可以使用类方法current。 请注意,从RunLoop的角度来看,Timer对象不是“输入” - 它们是一种特殊类型,其中一个意思是它们不会导致运行循环在它们触发时返回。我们都知道我们的应用是一个进程,在应用程序周期中我们大多都会开辟不同的线程来处理一些耗时的事情,在这里面Runloop扮演的是什么角色呢?Runloop 可以翻译为 事件循环 我们可以把其理解为一个 while 循环do { // something}while()Runloop是和线程一一对应的,其中,主线程的Runloop会在应用启动的时候进行启动和绑定,其他线程的Runloop要通过手动调用 [NSRunLoop currentRunLoop] 才会生成看一下Foundation中的NSRunLoop.h, 里面有这几个方法需要注意一下:@property (class, readonly, strong) NSRunLoop *currentRunLoop;@property (class, readonly, strong) NSRunLoop *mainRunLoop;@property (nullable, readonly, copy) NSRunLoopMode currentMode;- (void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode;- (void)addPort:(NSPort *)aPort forMode:(NSRunLoopMode)mode;其中 currentRunLoop 表示获取当前的Runloop ,mainRunLoop代表获取主事件循环,这里可以看出,非主线程的Runloop必须在子线程内获取,而mainRunLoop可以在任意线程获取。NSRunLoopMode 在官方文档中提到的有一下子五个:NSDefaultRunLoopModeNSConnectionReplyModeNSModalPanelRunLoopModeNSEventTrackingRunLoopModeNSRunLoopCommonModes其中公开暴露出来的只有 NSDefaultRunLoopMode 和 NSRunLoopCommonModes ,在这里面 NSRunLoopCommonModes 实际上包含了 NSDefaultRunLoopMode 和 NSEventTrackingRunLoopMode,在需要追踪包括scrollview滚动等事件的时候最好使用 NSRunLoopCommonModesRunloop 的实际应用首先,我们可以在viewcontroller中的touchbegin方法打断点,看一下该方法的调用栈- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent )event { }在断点出发时我们在控制台查看(lldb) thread backtrace thread #1, queue = ‘com.apple.main-thread’, stop reason = breakpoint 2.1 * frame #0: 0x000000010a1589a0 Library_test-[ViewController touchesBegan:withEvent:](self=0x00007f7fda41adc0, _cmd="touchesBegan:withEvent:", touches=1 element, event=0x0000600002649440) at ViewController.m:35 frame #1: 0x0000000110e088e8 UIKitCoreforwardTouchMethod + 353 frame #2: 0x0000000110e08776 UIKitCore-[UIResponder touchesBegan:withEvent:] + 49 frame #3: 0x0000000110e17dff UIKitCore-[UIWindow _sendTouchesForEvent:] + 2052 frame #4: 0x0000000110e197a0 UIKitCore-[UIWindow sendEvent:] + 4080 frame #5: 0x0000000110df7394 UIKitCore-[UIApplication sendEvent:] + 352 frame #6: 0x0000000110ecc5a9 UIKitCore__dispatchPreprocessedEventFromEventQueue + 3054 frame #7: 0x0000000110ecf1cb UIKitCore__handleEventQueueInternal + 5948 frame #8: 0x000000010cb87721 CoreFoundation__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17 frame #9: 0x000000010cb86f93 CoreFoundation__CFRunLoopDoSources0 + 243 frame #10: 0x000000010cb8163f CoreFoundation__CFRunLoopRun + 1263 frame #11: 0x000000010cb80e11 CoreFoundationCFRunLoopRunSpecific + 625 frame #12: 0x00000001143891dd GraphicsServicesGSEventRunModal + 62 frame #13: 0x0000000110ddb81d UIKitCoreUIApplicationMain + 140 frame #14: 0x000000010a1590d0 Library_testmain(argc=1, argv=0x00007ffee5aa7000) at main.m:14 frame #15: 0x000000010e331575 libdyld.dylibstart + 1(lldb) 可以看到 CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION 的方法调用其实这就是一个点击的事件触发了,通过runloop传递的例子,实际上所有的事件都会通过这个方法调用,最后触发到我们编写的代码, 理解了这个我们接下来看看具体的应用。NSTimer在controller中添加一个timer#import “ViewController.h”@interface ViewController ()@property (nonatomic, strong) NSTimer *t;@end@implementation ViewController- (void)viewDidLoad { [super viewDidLoad]; _t = [NSTimer timerWithTimeInterval:1.0f repeats:true block:^(NSTimer * _Nonnull timer) { NSLog(@“Timer Triggered”); }]; [[NSRunLoop currentRunLoop] addTimer:_t forMode:NSRunLoopCommonModes];}@end处理耗时逻辑在后台进程处理逻辑,完成后通过modes避免在滑动视图的时候返回- (void)viewDidLoad { [super viewDidLoad]; [self performSelectorInBackground:@selector(someBussiness) withObject:nil];}- (void)someBussiness { [self performSelector:@selector(bussinessFinished) withObject:nil afterDelay:0.0f inModes:@[NSDefaultRunLoopMode]];}- (void)bussinessFinished { }自定义后台线程有时我们想把一些逻辑全部放到自定义的线程中去处理,下面给出一个解决方案。#import “ViewController.h”@interface ViewController ()@property (nonatomic, strong) NSThread *thread;@property (nonatomic, strong) NSPort *port;@end@implementation ViewController- (void)viewDidLoad { [super viewDidLoad]; [self.thread start];}- (void)selfThread { @autoreleasepool { [[NSRunLoop currentRunLoop] run]; }}- (NSThread *)thread { if (!_thread) { _thread = [[NSThread alloc] initWithTarget:self selector:@selector(selfThread) object:nil]; } return _thread;}- (NSPort *)port { if (!_port) { _port = [NSPort port]; } return _port;}- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { [self performSelector:@selector(someBussiness) onThread:self.thread withObject:nil waitUntilDone:false];}- (void)someBussiness { }@end运行之后貌似点击屏幕并没有反应?这是因为我们没有为Runloop加入source(输入源),source可以是timer,port。其中,NSPort是一个描述通信通道的抽象类,我们可以先使用port作为输入源来让Runloop持续运行,改造这个方法- (void)selfThread { @autoreleasepool { [[NSRunLoop currentRunLoop] addPort:self.port forMode:NSRunLoopCommonModes]; [[NSRunLoop currentRunLoop] run]; }}现在我们就可以把需要执行的业务逻辑放入自定义的线程中执行了还有其他许多实际的应用,比如优化空闲状体的利用、集合视图的优化等等,这里就不多叙述了 ...

March 31, 2019 · 2 min · jiezi

iOS-International

原文链接随着项目越来越成熟,逐渐拓展到海外市场,我们就需要适配多种国际化和地区、需要对自己的产品进行国际化,让更多的用户可以使用我们的APP,这就需要对我们的产品进行国际化了。在这里就介绍一下自己在国际化项目里面踩过的一些坑。Project配置打开你的工程 , 在左侧栏选中project在打开的面板中选中project下的蓝色图标找到Localizations选项 , 添加你需要国家化的语言在主工程下command + N 新建Localizable.string文件选中你创建的string文件 , 打开右侧面板点击Localization组下的Localize按钮把你刚才配置在工程内的选项添加进去就好。调用方式在以前调用的地方我们需要替换为本地化调用方式比如以前我们是这么调用的:NSString tips = @“wait”;现在要换成NSString tips = NSLocalizedString(@“wait”, nil);(ps 第二个是为了方便翻译人员理解上下文语境使用的 。)然后在stirng文件内添加对应的字符串:“wait” = “别着急”;你可以在不同的文件添加对应不同语言的翻译 。让我们看下这个宏的定义 :#define NSLocalizedString(key, comment) \ [NSBundle.mainBundle localizedStringForKey:(key) value:@"" table:nil]#define NSLocalizedStringFromTable(key, tbl, comment) \ [NSBundle.mainBundle localizedStringForKey:(key) value:@"" table:(tbl)]#define NSLocalizedStringFromTableInBundle(key, tbl, bundle, comment) \ [bundle localizedStringForKey:(key) value:@"" table:(tbl)]#define NSLocalizedStringWithDefaultValue(key, tbl, bundle, val, comment) \ [bundle localizedStringForKey:(key) value:(val) table:(tbl)]可以看到实际上就是从mainBundle中取出了指定的String文件 , 然后根据我们在代码中定义的 ‘key’ 值取出value有的时候我们要指定我们的string文件名字而不使用上面的那个默认名字 。NSString tips = NSLocalizedStringFromTableInBundle(@“wait”, @“TDFOSLocalizable”, [NSBundle bundleForClass:[self class]], @“some tips to tell user wait”);这里面多出来两个参数,第一个是我们要指定的string文件名字,第二个就是要从哪个bundle中取,这个bundle的问题我们下面就会讲到。使用脚本替换工程内部调用方式那么在我们工程很大的情况下我们要把全部的字符串都替换为前缀为NSLocalizedString的形式人工手动替换肯定是不行的,又慢又不安全,没准复制粘贴的时候还把原来的key改错了,这里贴一段python的实例代码,可以稍加改动运行替换工程中的string前缀 。#!/usr/bin/env python3# -- coding: utf-8 --import osimport reimport sysimport datetimefolderPath = ‘/Users/felix/Documents/2dfire/TDFOpenShop/TDFOpenShop/Classes’stringToReplace = ‘NSLocalizedString’stringReplaceTo = ‘TDFOSLocalizedString’def scan_files(rootdir): files = [] for parent, dirnames, filenames in os.walk(rootdir): for filename in filenames: if len(filename.split(’.’,1))>1: if filename.split(’.’,1)[1] == ’m’: files.append(os.path.join(parent, filename)) return filesdef replaceStringIn(file): code = open(file,‘r+’) text = code.read() code.seek(0,0) code.write(text.replace(stringToReplace,stringReplaceTo)) code.close()def main(): files = scan_files(folderPath) for filePath in files: replaceStringIn(filePath)if name == ‘main’: main()这里面我是把以前的NS开头的宏替换为自定义的宏 , 大同小异 , 可以参照这个修改下即可 。编译运行测试当我们全部修改好以后肯定是要测试下显示对不对的、有两种方法可以快速切换App语言打开模拟器或真机 general -> setting -> lang ,不过这样比较繁琐。点击导航栏上的模拟器图标左边的schema选项,选择editSchema -> Run -> Options ->Application Language 直接改为你需要验证的语言运行就可以啦 。如何在模块内单独配置国际化文件很多时候我们的工程体量大到一定程度的时候都会模块化掉,几个人或者一个人负责一个模块,而有的模块是要作为SDK提供外部interface使用的 。当你把SDK提供出去的时候,我们期望的效果是在其他人(其他工程)内部运行的、我们的SDK也能够国际化显示 。但是在我们整个工程的string文件包含几千上万条的时候显然不需要全部移动到模块内 。所以在这里面我们要做的就是把需要的使用到的字符串和翻译好的value从主工程迁移到模块内部 。解决方案:使用cocoapods集成的模块化内部进行国际化我们先把主工程的国际化文件的文件夹复制到我们的模块路径下面,在模块内部有一个配置文件 x.podspec 里面添加这样一段配置s.resources = ‘xxx/.{xib,jpg,png,xcassets}’,‘xxx/.lproj’主要注意后面 , 这里就把我们刚复制来的国际化问价加入了模块的资源中 ,这时 ,如果我们不想用跟主工程一样的名字 ,Localizable.strings ,可以把这个名字也给改掉 , 比如 abc.strings调用这里为了有别于主工程的国际化 , 我们把strings文件改名为 abc.strings 然后在模块内新建一个头文件#ifndef TDFOSLocalizeMacro_h#define TDFOSLocalizeMacro_h#define TDFOSLocalizedString(key, comment) \NSLocalizedStringFromTableInBundle(key, @“abc”, [NSBundle bundleForClass:[self class]], comment)#endif / TDFOSLocalizeMacro_h */这里可以使用上面的脚本再次替换字符串前缀,然后运行下看看是否替换成功。这里注意要重新pod install下,否则cocoapods不会帮我们把资源引入包内 。ps: 别忘了删掉主工程中移除来的字符串 !!!总结基本用法暂时介绍到这里 , 关于模块化和国际化的东西要多了解一下NSbundle这个类 。 ...

March 31, 2019 · 1 min · jiezi

CocoaPods使用小结

原文链接CocoaPods是OS X和iOS下的一个第三类库管理工具,通过CocoaPods工具我们可以为项目添加被称为“Pods”的依赖库(这些类库必须是CocoaPods本身所支持的),并且可以轻松管理其版本。使用CocoaPods有以下几点好处:在引入第三方库时它可以自动为我们完成各种各样的配置,包括配置编译阶段、连接器选项、甚至是ARC环境下的一些配置等。使用CocoaPods可以很方便地管理的第三方SDK,大部分稳定好用的SDK都支持cocoapods导入。在项目模块化的过程中方便我们模块间解耦。Installsudo gem install cocoapods查看版本pod –version在开发中安装使用cocoapods要注意版本,因为一般开发过程中要大家一起使用同一个工程,一般为了指定版本我们会在工程下创建Gemfile来指定使用cocoapods的版本。指定使用Cocoapods的版本除了指定Gemfile以外 , 我们还可以安装指定版本的podssudo gem install cocoapods -v 1.3.1再查看一下pod版本我们就会发现已经安装了1.3.1卸载掉不需要的版本当我们本地同时存在多个版本的pod的时候可以把多余的卸载掉sudo gem uninstall cocoapods会提示我们选择卸载的版本Select gem to uninstall: 1. cocoapods-1.2.1 2. cocoapods-1.3.1 3. All versions>我们选择想要卸载的版本的序号就好了 。如何查看某个SDK的详细信息cocoapods支持我们去查找想要使用的仓库 , 比如我们想查找ReactoveObjC这个库pod spec cat ReactiveObjC我们可以看到该仓库的配置信息。{ “name”: “ReactiveObjC”, “version”: “3.1.0”, “summary”: “The 2.x ReactiveCocoa Objective-C API: Streams of values over time”, “description”: “ReactiveObjC (formally ReactiveCocoa or RAC) is an Objective-C\nframework inspired by Functional Reactive Programming.\nIt provides APIs for composing and transforming streams of values.”, “homepage”: “https://reactivecocoa.io”, “screenshots”: “https://reactivecocoa.io/img/logo.png", “license”: { “type”: “MIT”, “file”: “LICENSE.md” }, “documentation_url”: “https://github.com/ReactiveCocoa/ReactiveObjC/tree/master/Documentation#readme", “authors”: “ReactiveCocoa”, “social_media_url”: “https://twitter.com/ReactiveCocoa", “platforms”: { “ios”: “8.0”, “osx”: “10.9”, “watchos”: “2.0”, “tvos”: “9.0” }, “source”: { “git”: “https://github.com/ReactiveCocoa/ReactiveObjC.git", “tag”: “3.1.0” }, “source_files”: [ “ReactiveObjC/.{h,m,d}”, “ReactiveObjC/extobjc/.{h,m}” ], “private_header_files”: [ “/*Private.h”, “/EXTRuntimeExtensions.h”, “**/RACEmpty.h” ], “ios”: { “exclude_files”: “ReactiveObjC//{AppKit,NSControl,NSText,NSTable}” }, “osx”: { “exclude_files”: “ReactiveObjC//{UIActionSheet,UIAlertView,UIBarButtonItem,UIButton,UICollectionReusableView,UIControl,UIDatePicker,UIGestureRecognizer,UIImagePicker,UIRefreshControl,UISegmentedControl,UISlider,UIStepper,UISwitch,UITableViewCell,UITableViewHeaderFooterView,UIText,MK}” }, “tvos”: { “exclude_files”: “ReactiveObjC//{AppKit,NSControl,NSText,NSTable,UIActionSheet,UIAlertView,UIDatePicker,UIImagePicker,UIRefreshControl,UISlider,UIStepper,UISwitch,MK}” }, “watchos”: { “exclude_files”: “ReactiveObjC//{UIActionSheet,UIAlertView,UIBarButtonItem,UIButton,UICollectionReusableView,UIControl,UIDatePicker,UIGestureRecognizer,UIImagePicker,UIRefreshControl,UISegmentedControl,UISlider,UIStepper,UISwitch,UITableViewCell,UITableViewHeaderFooterView,UIText,MK,AppKit,NSControl,NSText,NSTable,NSURLConnection}“开始使用首先 , 新建一个singlgViewApp ,然后在命令行进入该project目录pod init我们可以看到cocoapods为我们生成了一个Podfileplatform表示这个工程安装的设备,后面是系统最低版本现在我们可以在里面添加一下刚才搜索的仓库# Uncomment the next line to define a global platform for your projectplatform :ios, ‘9.0’target ‘ocTest’ do # Uncomment the next line if you’re using Swift or would like to use dynamic frameworks # use_frameworks! pod ‘ReactiveObjC’ # Pods for ocTestend这里我们添加了一个仓库,接下来再命令行执行pod update来安装所需要的仓库,安装完毕后我们可以看到当前路径下有两个工程文件Test.xcodeprojTest.xcworkspace我们在使用了cocoapods管理第三方库的时候,每次install或者update的时候就会生成*.xcworkspace这个文件我们需要使用这个工程进行开发调试。现在打开工程,定位到viewController.m中就可以import并使用ReactiveObjC啦 。如何使用私有源公司内部有自己搭建的gitlab服务时,有的公司搭建的gitlab服务为了安全并没有提供外网接口,我们只能在内网访问,这时候就要在podfile中添加私有源的地址source ‘http://xx.xxxx.com/ios/cocoapods-spec.git'source ‘https://github.com/CocoaPods/Specs.git' # 官方库上面那个就是我们要添加的私有源地址,下面的是官方源地址,如果都不写的话那么默认就会使用官方源 。如何创建并发布仓库到私有repo首先执行命令,后面替换为你要发布SDK的名字pod lib create TDFCommonUtilpod会给出一些选项让我们选择To get you started we need to ask a few questions, this should only take a minute.If this is your first time we recommend running through with the guide: - https://guides.cocoapods.org/making/using-pod-lib-create.html ( hold cmd and click links to open in a browser. )What platform do you want to use?? [ iOS / macOS ] >iosWhat language do you want to use?? [ Swift / ObjC ] >swiftWould you like to include a demo application with your library? [ Yes / No ] >yesWhich testing frameworks will you use? [ Quick / None ] > NoneWould you like to do view based testing? [ Yes / No ] > NO现在我们需要添加一些信息cd进入刚才创建的目录,执行命令 ls 可以看到控制台有以下输出。Example README.md TDFCommonUtil.podspecLICENSE TDFCommonUtil _Pods.xcodeproj解释一下这里都是啥Example 这里面就是示例工程README.md 这里面是我们对改pod功能和使用等的介绍 , 使用markdown语法TDFCommonUtil.podspec 这里是改模块的全部基本信息LICENSE 这里放的是改开源项目遵守的协议TDFCommonUtil 这里放着全部的类文件和资源文件_Pods.xcodeproj 这是示例工程的快捷方式我们首先要为我们的pod创建一个实际的git仓库 。打开gitlab服务创建一个空的git仓库 然后先把pod目录提交到我们的仓库 blalblabla打开TDFCommonUtil.podspec文件Pod::Spec.new do |s| s.name = ‘TDFCommonUtil’ s.version = ‘0.1.0’ s.summary = ‘A short description of TDFCommonUtil.’ s.description = <<-DESCTODO: Add long description of the pod here. DESC s.homepage = ‘https://github.com/xxxxx@yeah.net/TDFCommonUtil' s.license = { :type => ‘MIT’, :file => ‘LICENSE’ } s.author = { ‘xxxxx’ => ‘xxxxx@2dfire.com’ } s.source = { :git => ‘https://github.com/xxxxx@yeah.net/TDFCommonUtil.git', :tag => s.version.to_s } s.ios.deployment_target = ‘8.0’ s.source_files = ‘TDFCommonUtil/Classes//’ # s.resource_bundles = { # ‘TDFCommonUtil’ => [‘TDFCommonUtil/Assets/.png’] # } # s.public_header_files = ‘Pod/Classes//*.h’ # s.frameworks = ‘UIKit’, ‘MapKit’ # s.dependency ‘AFNetworking’, ‘> 2.3’end这里的配置项就是生成pod需要配置的大部分选项了 ,简单介绍一下name pod名称version 版本号 ,特别注意下在更新时需要对git仓库打tagsummary 简介description 描述homepage 主页license 使用协议author 作者deployment_target 需要系统版本source_files 源文件路径resource_bundles 资源文件路径 注意这里会把资源文件打包成bundle , 调用的时候要注意下public_header_files 公开的头文件 (有些头文件我们不想要外部看见可以在这里去掉)frameworks 需要依赖的framework库dependency 需要依赖的其他pod现在把这些填好吧 。如何添加依赖有的时候我们开发的pod仓库需要依赖其他仓库,比如我们需要依赖ReactiveObjC这里就可以在TDFCommonUtil.podspec下面添加这一行s.dependency ‘ReactiveObjC’ps ,这里可以指向特定的版本也可以用 ‘> 2.3’ 的形式表示依赖此仓库至少大于2.3版本但是不会超过3.0 。使用lint命令检测我们的仓库是否还有问题一切就绪后你可以在里面创建你的文件,添加代码了,别忘了再有文件的增加或删除后在运行一遍 ‘pod install’。先随便创建几个文件,然后我们使用cocoapods检测我们的库pod lib lint –sources='git@git.xxx.com:ios/cocoapods-spec.git’ –use-libraries –allow-warnings –verbose –no-clean这里的sources填写你所使用的私有gitlab服务,然后我们就可以静静的看着命令行了。最后如果出现Test passed validation.证明你的库是可运行的,如果没有出现passed就注意下输出中的error信息,搜索一下error看是什么导致的 。向私有源推送lint通过后我们就可以把自己的仓库信息推送到私有源了,注意不是「仓库」是「仓库信息」,也就是x.podspec 。cocoapod可以自动帮我们完成这件事情pod repo push xxx-cocoapods-spec TDFOpenShopSDK.podspec –sources=git@git.xxx.com:ios/cocoapods-spec.git –allow-warnings –use-libraries –verbose总结Cocoapods极大方便了我们管理外部SDK和内部模块化解耦,用好这个管理工具无疑会为我们的工作效率带来巨大提升。 ...

March 31, 2019 · 3 min · jiezi

NSCache-最佳的缓存方式

原文链接NSCache 基本上就是一个会自动移除对象来释放内存的 NSMutableDictionary。无需响应内存警告或者使用计时器来清除缓存。唯一的不同之处是键对象不会像 NSMutableDictionary 中那样被复制,这实际上是它的一个优点(键不需要实现 NSCopying 协议)。先列一下使用NSCache的好处NSCache是一个类似NSDictionary一个可变的集合。提供了可设置缓存的数目与内存大小限制的方式。保证了处理的数据的线程安全性。缓存使用的key不需要是实现NSCopying的类。当内存警告时内部自动清理部分缓存数据。NSCache的属性与方法@property (assign) id<NSCacheDelegate>delegate;cache对象的代理 , 用来即将清理cache的时候得到通知- (void)cache:(NSCache *)cache willEvictObject:(id)obj;代理方法 , 这里面不要对cache进行改动 , 如果对象obj需要被持久化存储的话可以在这里进行操作这里面有几种情况会导致该方法执行:手动移除(removeObjectForKey)缓存超过设定的上线App不活跃系统内存爆炸@property BOOL evictsObjectsWithDiscardedContent;该属性默认为True , 表示在内存销毁时丢弃该对象 。@property NSUInteger totalCostLimit;总成本数 , 用来设置最大缓存数量开始使用NSCache//// TDFSetPhoneNumController.m// TDFLoginModule//// Created by doubanjiang on 2017/6/5.// Copyright © 2017年 doubanjiang. All rights reserved.//#import “viewController.h”@interface viewController () <NSCacheDelegate>@property (nonatomic ,strong) NSCache *cache;@end@implementation viewController- (void)viewDidLoad { [super viewDidLoad]; [self beginCache];}- (void)beginCache { for (int i = 0; i<10; i++) { NSString *obj = [NSString stringWithFormat:@“object–%d”,i]; [self.cache setObject:obj forKey:@(i) cost:1]; NSLog(@"%@ cached",obj); }}#pragma mark - NSCacheDelegate- (void)cache:(NSCache *)cache willEvictObject:(id)obj { //evict : 驱逐 NSLog(@"%@", [NSString stringWithFormat:@"%@ will be evict",obj]);}#pragma mark - Getter- (NSCache *)cache { if (!_cache) { _cache = [NSCache new]; _cache.totalCostLimit = 5; _cache.delegate = self; } return _cache;}@end我们会看到以下输出2018-07-31 09:30:56.485719+0800 Test_Example[52839:214698] object–0 cached2018-07-31 09:30:56.485904+0800 Test_Example[52839:214698] object–1 cached2018-07-31 09:30:56.486024+0800 Test_Example[52839:214698] object–2 cached2018-07-31 09:30:56.486113+0800 Test_Example[52839:214698] object–3 cached2018-07-31 09:30:56.486254+0800 Test_Example[52839:214698] object–4 cached2018-07-31 09:30:56.486382+0800 Test_Example[52839:214698] object–0 will be evict2018-07-31 09:30:56.486480+0800 Test_Example[52839:214698] object–5 cached2018-07-31 09:30:56.486598+0800 Test_Example[52839:214698] object–1 will be evict2018-07-31 09:30:56.486681+0800 Test_Example[52839:214698] object–6 cached2018-07-31 09:30:56.486795+0800 Test_Example[52839:214698] object–2 will be evict2018-07-31 09:30:56.486888+0800 Test_Example[52839:214698] object–7 cached2018-07-31 09:30:56.486995+0800 Test_Example[52839:214698] object–3 will be evict2018-07-31 09:30:56.487190+0800 Test_Example[52839:214698] object–8 cached2018-07-31 09:30:56.487446+0800 Test_Example[52839:214698] object–4 will be evict2018-07-31 09:30:56.487604+0800 Test_Example[52839:214698] object–9 cached总结经过实验我们发现NSCache使用上性价比还是比较高的,可以在项目里放心去用,可以提升app的性能。 ...

March 31, 2019 · 1 min · jiezi

宁波办理假支票

宁波办理假支票/存单Q70105760支票是以银行为付款人的即期汇票,可以看作汇票的特例。支票出票人签发的支票金额,不得超出其在付款人处的存款金额。如果存款低于支票金额,银行将拒付给持票人。这种支票称为空头支票,出票人要负法律上的责任。办理假支票/存单Q701 05760开立支票存款账户和领用支票,必须有可靠的资信,并存入一定的资金。支票可分为现金支票、转账支票。支票一经背书即可流通转让,具有通货作用,成为替代货币发挥流通手段和支付手段职能的信用流通工具。运用支票进行货币结算,可以减少现金的流通量,节约货币流通费用。已经签发的普通支票和现金支票。如因遗失,被盗等原因而丧失,应立即向银行申请挂失。办理假支票/存单Q70 105 760(1)出票人将已经签发内容齐备的可以直接支取现金的支票遗失或被盗等,应当出具公函或有关证明,填写两联挂失申请书(可以用进账单代替),加盖预留银行的签名式样和印鉴,办理假支票/存单Q701 057 60向开户银行申请挂失止付。银行查明该支票确未支付,经收取一定的挂失手续费后受理挂失,在挂失人账户中用红笔注明支票号码及挂失的日期。(2)收款人将收受的可以直接支取现金的支票遗失或被盗等,也应当出具公函或有关证明,填写两联挂失止付申请书,经付款人签章证明后,到收款人开户银行申请挂失止付。其他有关手续同上。办理假支票/存单Q70 105 760同时,依据《票据法》第15条第3款规定:“失票人应当在通知挂失止付后3月内,也可以在票据丧失后,依法向人民法院申请公示催告,或者向人民法院提起诉讼。“即可以背书转让的票据的待票人在票据被盗、遗失或灭失时,须以书面形式向票据支付地(即付款地)的基层人民法院提出公示催告申请。在失票人向人民法院提交的申请书上,应写明票据类别、票面金额、出票人、付款人、背书人等票据主要内容,并说明票据丧失的情形,同时提出有关证据,以证明自己确属丧失的票据的持票人,有权提出申请。办理假支票/存单Q70105760失票人在向付款人挂失止付之前,或失票人在申请公示催告以前,票据已经由付款人善意付款的,失票人不得再提出公示催告的申请,付款银行也不再承担付款的责任。由此给支票权利人造成的损失,应当由失票人自行负责。

March 27, 2019 · 1 min · jiezi

iOS 多线程(NNSThread,GCD,NSOperation)

线程基本概念线程是进程内假想的持有 cpu 使用权的执行单位,一个进程下可以创建多个线程并行执行;使用多线程的程序称为多线程运行,从程序开始执行是运行的程序成为主线程,除此之外之后生成的线程为次线程或子线程。线程安全和注意点多个线程操作某个实例时,没有得到错误的结果或实例时,那么该类就称为线程安全。结果不能保证时,则称为非线程安全。一般情况下,常数对象是线程安全的,变量对象不是线程安全的。要想使用多线程不出错且高效执行,并行编程的知识必不可少,线程间的任务分配和信息交换、共享资源的互斥、与 GUI 的交互以及动画显示等,使用时都要格外小心。iOS 多线程对比NSThread简介NSThread 是苹果官方提供的面向对象操作线程技术,简单方便,可以直接操作对象,不过需要自己控制线程的生命周期,在平时较少使用。初始化创建 NSThread 的方法有如下几种:/使用target对象的selector作为线程的任务执行体,该selector方法最多可以接收一个参数,该参数即为argument/- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));/使用block作为线程的任务执行体/- (instancetype)initWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));/类方法,返回值为void使用一个block作为线程的执行体,并直接启动线程上面的实例方法返回NSThread对象需要手动调用start方法来启动线程执行任务/+ (void)detachNewThreadWithBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));/类方法,返回值为void使用target对象的selector作为线程的任务执行体,该selector方法最多接收一个参数,该参数即为argument同样的,该方法创建完县城后会自动启动线程不需要手动触发/+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(nullable id)argument;线程的相关用法和线程状态的控制方法// 获得主线程+ (NSThread *)mainThread; // 判断是否为主线程(对象方法)- (BOOL)isMainThread;// 判断是否为主线程(类方法)+ (BOOL)isMainThread; // 获得当前线程NSThread *current = [NSThread currentThread];// 线程的名字——setter方法- (void)setName:(NSString *)n; // 线程的名字——getter方法- (NSString )name; // 线程进入就绪状态 -> 运行状态。当线程任务执行完毕,自动进入死亡状态- (void)start;// 线程进入阻塞状态+ (void)sleepUntilDate:(NSDate )date;+ (void)sleepForTimeInterval:(NSTimeInterval)ti;// 线程进入死亡状态+ (void)exit;线程之间的通信// 在主线程上执行操作- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray<NSString > )array; // equivalent to the first method with kCFRunLoopCommonModes// 在指定线程上执行操作- (void)performSelector:(SEL)aSelector onThread:(NSThread )thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray )array NS_AVAILABLE(10_5, 2_0);- (void)performSelector:(SEL)aSelector onThread:(NSThread )thr withObject:(id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);// 在当前线程上执行操作,调用 NSObject 的 performSelector:相关方法- (id)performSelector:(SEL)aSelector;- (id)performSelector:(SEL)aSelector withObject:(id)object;- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;GCD概念Grand Central Dispatch(GCD) 是 Apple 开发的一个多核编程的较新的解决方法。它主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。它是一个在线程池模式的基础上执行的并发任务。在 Mac OS X 10.6 雪豹中首次推出,也可在 iOS 4 及以上版本使用。优点GCD 可用于多核的并行运算GCD 会自动利用更多的 CPU 内核(比如双核、四核)GCD 会自动管理线程的生命周期(创建线程、调度任务、销毁线程)程序员只需要告诉 GCD 想要执行什么任务,不需要编写任何线程管理代码GCD 的使用1、创建队列(串行队列或并发队列)2、将任务追加到队列中,系统根据任务类型执行任务(同步或者异步)队列的创建方法/获取方法// 串行队列的创建方法dispatch_queue_t queue = dispatch_queue_create(“net.bujige.testQueue”, DISPATCH_QUEUE_SERIAL);// 并发队列的创建方法dispatch_queue_t queue = dispatch_queue_create(“net.bujige.testQueue”, DISPATCH_QUEUE_CONCURRENT);// 主队列的获取方法dispatch_queue_t queue = dispatch_get_main_queue();// 全局并发队列的获取方法dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);任务的创建方法// 同步执行任务创建方法dispatch_sync(queue, ^{ // 这里放同步执行任务代码});// 异步执行任务创建方法dispatch_async(queue, ^{ // 这里放异步执行任务代码});GCD的基本使用我们可以看到,GCD 有两种创建任务的方法:同步或异步;三种队列:并发队列、串行队列和主队列,一共有六种的组合方式,我们逐个进行分析:#pragma mark ————————GCD 基本使用(六种不同的组合)#pragma mark ———————–异步执行主队列:在主线程中串行执行任务- (void)asyncMain { NSLog(@“mainThread—%@”,[NSThread currentThread]); // 打印当前线程 NSLog(@“asyncSerial—begin”); dispatch_queue_t queue = dispatch_get_main_queue(); dispatch_async(queue, ^{ for(int i=0;i<2;i++){// 任务1 [NSThread sleepForTimeInterval:2];//模拟耗时操作 NSLog(@“1——-%@”,[NSThread currentThread]); } }); dispatch_async(queue, ^{ // 任务2 for(int i=0;i<2;i++){ [NSThread sleepForTimeInterval:2]; NSLog(@“2——-%@”,[NSThread currentThread]); } }); dispatch_async(queue, ^{ // 任务3 for(int i=0;i<2;i++){ [NSThread sleepForTimeInterval:2]; NSLog(@“3——-%@”,[NSThread currentThread]); } }); NSLog(@“main—end”);}#pragma mark ———————–同步执行主队列//在主线程中使用 同步执行主队列,程序会出现死锁- (void)syncMain { NSLog(@“mainThread—%@”,[NSThread currentThread]); // 打印当前线程 NSLog(@“syncSerial—begin”); dispatch_queue_t queue = dispatch_get_main_queue(); dispatch_sync(queue, ^{ for(int i=0;i<2;i++){// 任务1 [NSThread sleepForTimeInterval:2];//模拟耗时操作 NSLog(@“1——-%@”,[NSThread currentThread]); } }); dispatch_sync(queue, ^{ // 任务2 for(int i=0;i<2;i++){ [NSThread sleepForTimeInterval:2]; NSLog(@“2——-%@”,[NSThread currentThread]); } }); dispatch_sync(queue, ^{ // 任务3 for(int i=0;i<2;i++){ [NSThread sleepForTimeInterval:2]; NSLog(@“3——-%@”,[NSThread currentThread]); } }); NSLog(@“main—end”);}#pragma mark ———————–异步执行串行对类:开启新线程,在当前线程下串行执行任务,任务不做等待- (void)asyncSerial { NSLog(@“serialThread—%@”,[NSThread currentThread]); // 打印当前线程 NSLog(@“asyncSerial—begin”); dispatch_queue_t queue = dispatch_queue_create(“serial.queue.test”, DISPATCH_QUEUE_SERIAL); dispatch_async(queue, ^{ // 任务1 for(int i=0;i<2;i++){ [NSThread sleepForTimeInterval:2];//模拟耗时操作 NSLog(@“1——-%@”,[NSThread currentThread]); } }); dispatch_async(queue, ^{ // 任务2 for(int i=0;i<2;i++){ [NSThread sleepForTimeInterval:2]; NSLog(@“2——-%@”,[NSThread currentThread]); } }); dispatch_async(queue, ^{ // 任务3 for(int i=0;i<2;i++){ [NSThread sleepForTimeInterval:2]; NSLog(@“3——-%@”,[NSThread currentThread]); } }); NSLog(@“asyncSerial—end”);}#pragma mark ———————–同步执行串行对类:不开启新线程,在当前线程下串行执行任务- (void)syncSerial { NSLog(@“serialThread—%@”,[NSThread currentThread]); // 打印当前线程 NSLog(@“syncSerial—begin”); dispatch_queue_t queue = dispatch_queue_create(“serial.queue.test”, DISPATCH_QUEUE_SERIAL); dispatch_sync(queue, ^{ // 任务1 for(int i=0;i<2;i++){ [NSThread sleepForTimeInterval:2];//模拟耗时操作 NSLog(@“1——-%@”,[NSThread currentThread]); } }); dispatch_sync(queue, ^{ // 任务2 for(int i=0;i<2;i++){ [NSThread sleepForTimeInterval:2]; NSLog(@“2——-%@”,[NSThread currentThread]); } }); dispatch_sync(queue, ^{ // 任务3 for(int i=0;i<2;i++){ [NSThread sleepForTimeInterval:2]; NSLog(@“3——-%@”,[NSThread currentThread]); } }); NSLog(@“syncSerial—end”);}#pragma mark ————————-异步执行并发队列:开启多个线程,任务交替(同时)执行- (void)asyncConcurrent { NSLog(@“currentThread—%@”,[NSThread currentThread]); // 打印当前线程 NSLog(@“asyncConcurrent—begin”); dispatch_queue_t queue = dispatch_queue_create(“concurrent.queue.test”, DISPATCH_QUEUE_CONCURRENT); dispatch_async(queue, ^{ // 任务1 for(int i=0;i<2;i++){ [NSThread sleepForTimeInterval:2];//模拟耗时操作 NSLog(@“1——-%@”,[NSThread currentThread]); } }); dispatch_async(queue, ^{ // 任务2 for(int i=0;i<2;i++){ [NSThread sleepForTimeInterval:2]; NSLog(@“2——-%@”,[NSThread currentThread]); } }); dispatch_async(queue, ^{ // 任务3 for(int i=0;i<2;i++){ [NSThread sleepForTimeInterval:2]; NSLog(@“3——-%@”,[NSThread currentThread]); } }); NSLog(@“asyncConcurrent—end”);}#pragma mark ————————同步执行并发队列:不开启新线程,执行完一个任务在执行下一个任务,因为只有一个线程- (void)syncConcurrent { NSLog(@“currentThread—%@”,[NSThread currentThread]); // 打印当前线程 NSLog(@“syncConcurrent—begin”); dispatch_queue_t queue = dispatch_queue_create(“concurrent.queue.test”, DISPATCH_QUEUE_CONCURRENT); dispatch_sync(queue, ^{// 任务1 for(int i=0;i<2;i++){ [NSThread sleepForTimeInterval:2];//模拟耗时操作 NSLog(@“1——-%@”,[NSThread currentThread]); } }); dispatch_sync(queue, ^{ // 任务2 for(int i=0;i<2;i++){ [NSThread sleepForTimeInterval:2]; NSLog(@“2——-%@”,[NSThread currentThread]); } }); dispatch_sync(queue, ^{ // 任务3 for(int i=0;i<2;i++){ [NSThread sleepForTimeInterval:2]; NSLog(@“3——-%@”,[NSThread currentThread]); } }); NSLog(@“syncConcurrent—end”);}线程间的通信/ * 线程间通信 /- (void)communication { // 获取全局并发队列 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); // 获取主队列 dispatch_queue_t mainQueue = dispatch_get_main_queue(); dispatch_async(queue, ^{ // 异步追加任务 for (int i = 0; i < 2; ++i) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@“1—%@”,[NSThread currentThread]); // 打印当前线程 } // 回到主线程 dispatch_async(mainQueue, ^{ // 追加在主线程中执行的任务 [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@“2—%@”,[NSThread currentThread]); // 打印当前线程 }); });}GCD 栅栏方法:dispatch_barrier_async我们有时需要异步执行两组操作,而且第一组操作执行完之后,才能开始执行第二组操作。这样我们就需要一个相当于栅栏一样的一个方法将两组异步执行的操作组给分割起来,当然这里的操作组里可以包含一个或多个任务。这就需要用到dispatch_barrier_async方法在两个操作组间形成栅栏。- (void)barrier { dispatch_queue_t queue = dispatch_queue_create(“concurrent.queue”, DISPATCH_QUEUE_CONCURRENT); dispatch_async(queue, ^{ // 追加任务1 for (int i = 0; i < 2; ++i) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@“1—%@”,[NSThread currentThread]); // 打印当前线程 } }); dispatch_async(queue, ^{ // 追加任务2 for (int i = 0; i < 2; ++i) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@“2—%@”,[NSThread currentThread]); // 打印当前线程 } }); dispatch_barrier_async(queue, ^{ [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@“barrier—%@”,[NSThread currentThread]); // 打印当前线程 }); dispatch_async(queue, ^{ // 追加任务3 for (int i = 0; i < 2; ++i) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@“3—%@”,[NSThread currentThread]); // 打印当前线程 } }); dispatch_async(queue, ^{ // 追加任务4 for (int i = 0; i < 2; ++i) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@“4—%@”,[NSThread currentThread]); // 打印当前线程 } });}GCD 延时执行方法:dispatch_after/ * 延时执行方法 dispatch_after /- (void)after { NSLog(@“currentThread—%@”,[NSThread currentThread]); // 打印当前线程 NSLog(@“asyncMain—begin”); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ // 2.0秒后异步追加任务代码到主队列,并开始执行 NSLog(@“after—%@”,[NSThread currentThread]); // 打印当前线程 });}GCD 一次性代码(只执行一次):dispatch_once/ * 一次性代码(只执行一次)dispatch_once /- (void)once { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // 只执行1次的代码(这里面默认是线程安全的) });}GCD 快速迭代方法:dispatch_apply通常我们会用 for 循环遍历,但是 GCD 给我们提供了快速迭代的函数dispatch_apply。dispatch_apply按照指定的次数将指定的任务追加到指定的队列中,并等待全部队列执行结束。dispatch_apply 可以 在多个线程中同时(异步)遍历多个数字。无论是在串行队列,还是异步队列中,dispatch_apply 都会等待全部任务执行完毕,这点就像是同步操作,也像是队列组中的 dispatch_group_wait方法。GCD 队列组:dispatch_groupdispatch_group_notify监听 group 中任务的完成状态,当所有的任务都执行完成后,追加任务到 group 中,并执行任务。/ * 队列组 dispatch_group_notify /- (void)groupNotify { NSLog(@“currentThread—%@”,[NSThread currentThread]); // 打印当前线程 NSLog(@“group—begin”); dispatch_group_t group = dispatch_group_create(); dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // 追加任务1 for (int i = 0; i < 2; ++i) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@“1—%@”,[NSThread currentThread]); // 打印当前线程 } }); dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // 追加任务2 for (int i = 0; i < 2; ++i) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@“2—%@”,[NSThread currentThread]); // 打印当前线程 } }); dispatch_group_notify(group, dispatch_get_main_queue(), ^{ // 等前面的异步任务1、任务2都执行完毕后,回到主线程执行下边任务 for (int i = 0; i < 2; ++i) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@“3—%@”,[NSThread currentThread]); // 打印当前线程 } NSLog(@“group—end”); });}dispatch_group_wait暂停当前线程(阻塞当前线程),等待指定的 group 中的任务执行完成后,才会往下继续执行。队列组 dispatch_group_wait-(void)groupWait { NSLog(@“currentThread—%@”,[NSThread currentThread]); // 打印当前线程 NSLog(@“group—begin”); dispatch_group_t group = dispatch_group_create(); dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // 追加任务1 for (int i = 0; i < 2; ++i) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@“1—%@”,[NSThread currentThread]); // 打印当前线程 } }); dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // 追加任务2 for (int i = 0; i < 2; ++i) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@“2—%@”,[NSThread currentThread]); // 打印当前线程 } }); // 等待上面的任务全部完成后,会往下继续执行(会阻塞当前线程) dispatch_group_wait(group, DISPATCH_TIME_FOREVER); NSLog(@“group—end”);}dispatch_group_enter、dispatch_group_leavedispatch_group_enter 标志着一个任务追加到 group,执行一次,相当于 group 中未执行完毕任务数+1dispatch_group_leave 标志着一个任务离开了 group,执行一次,相当于 group 中未执行完毕任务数-1。当 group 中未执行完毕任务数为0的时候,才会使dispatch_group_wait解除阻塞,以及执行追加到dispatch_group_notify中的任务。- (void)groupEnterAndLeave{ NSLog(@“currentThread—%@”,[NSThread currentThread]); // 打印当前线程 NSLog(@“group—begin”); dispatch_group_t group = dispatch_group_create(); dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_group_enter(group); dispatch_async(queue, ^{ // 追加任务1 for (int i = 0; i < 2; ++i) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@“1—%@”,[NSThread currentThread]); // 打印当前线程 } dispatch_group_leave(group); }); dispatch_group_enter(group); dispatch_async(queue, ^{ // 追加任务2 for (int i = 0; i < 2; ++i) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@“2—%@”,[NSThread currentThread]); // 打印当前线程 } dispatch_group_leave(group); }); dispatch_group_notify(group, dispatch_get_main_queue(), ^{ // 等前面的异步操作都执行完毕后,回到主线程. for (int i = 0; i < 2; ++i) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@“3—%@”,[NSThread currentThread]); // 打印当前线程 } NSLog(@“group—end”); }); // // 等待上面的任务全部完成后,会往下继续执行(会阻塞当前线程) // dispatch_group_wait(group, DISPATCH_TIME_FOREVER); // // NSLog(@“group—end”);}Dispatch Semaphore 线程同步使用Dispatch Semaphore 可以实现线程同步,将异步执行任务转换为同步执行任务。- (void)semaphoreSync { NSLog(@“currentThread—%@”,[NSThread currentThread]); // 打印当前线程 NSLog(@“semaphore—begin”); dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); __block int number = 0; dispatch_async(queue, ^{ // 追加任务1 [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@“1—%@”,[NSThread currentThread]); // 打印当前线程 number = 100; dispatch_semaphore_signal(semaphore); }); dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); NSLog(@“semaphore—end,number = %zd”,number);}NSOperation简介NSOperation、NSOperationQueue 是苹果提供给我们的一套多线程解决方案。实际上 NSOperation、NSOperationQueue 是基于 GCD 更高一层的封装,完全面向对象。但是比 GCD 更简单易用、代码可读性也更高。NSOperation、NSOperationQueue 使用步骤NSOperation 需要配合 NSOperationQueue 来实现多线程。因为默认情况下,NSOperation 单独使用时系统同步执行操作,配合 NSOperationQueue 我们能更好的实现异步执行。NSOperation 实现多线程的使用步骤分为三步:创建操作:先将需要执行的操作封装到一个 NSOperation 对象中。创建队列:创建 NSOperationQueue 对象。将操作加入到队列中:将 NSOperation 对象添加到 NSOperationQueue 对象中。NSOperation 是个抽象类,不能用来封装操作。我们只有使用它的子类来封装操作。我们有三种方式来封装操作。使用子类 NSInvocationOperation使用子类 NSBlockOperation自定义继承自 NSOperation 的子类,通过实现内部相应的方法来封装操作。使用子类 NSInvocationOperation/ * 使用子类 NSInvocationOperation /- (void)useInvocationOperation { // 1.创建 NSInvocationOperation 对象 NSInvocationOperation op = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil]; // 2.调用 start 方法开始执行操作 [op start];}/ * 任务1 /- (void)task1 { for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@“1—%@”, [NSThread currentThread]); // 打印当前线程 }}使用子类 NSBlockOperation/ * 使用子类 NSBlockOperation */- (void)useBlockOperation { // 1.创建 NSBlockOperation 对象 NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{ for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@“1—%@”, [NSThread currentThread]); // 打印当前线程 } }]; // 2.调用 start 方法开始执行操作 [op start];}addExecutionBlock :NSBlockOperation 还提供了一个方法 addExecutionBlock:通过 addExecutionBlock: 就可以为 NSBlockOperation 添加额外的操作。NSOperationQueueNSOperationQueue 一共有两种队列:主队列、自定义队列。其中自定义队列同时包含了串行、并发功能。下边是主队列、自定义队列的基本创建方法和特点。// 主队列获取方法 队列中代码在主线程运行NSOperationQueue queue = [NSOperationQueue mainQueue];// 自定义队列创建方法 队列中代码在子线程运行NSOperationQueue queue = [[NSOperationQueue alloc] init];将操作加入到队列中/ * 使用 addOperation: 将操作加入到操作队列中 */- (void)addOperationToQueue { // 1.创建队列 NSOperationQueue *queue = [[NSOperationQueue alloc] init]; // 2.创建操作 // 使用 NSInvocationOperation 创建操作1 NSInvocationOperation *op1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task1) object:nil]; // 使用 NSInvocationOperation 创建操作2 NSInvocationOperation op2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(task2) object:nil]; // 使用 NSBlockOperation 创建操作3 NSBlockOperation op3 = [NSBlockOperation blockOperationWithBlock:^{ for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@“3—%@”, [NSThread currentThread]); // 打印当前线程 } }]; [op3 addExecutionBlock:^{ for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@“4—%@”, [NSThread currentThread]); // 打印当前线程 } }]; // 3.使用 addOperation: 添加所有操作到队列中 [queue addOperation:op1]; // [op1 start] [queue addOperation:op2]; // [op2 start] [queue addOperation:op3]; // [op3 start]}NSOperationQueue 控制串行执行、并发执行最大并发操作数:maxConcurrentOperationCountmaxConcurrentOperationCount 默认情况下为-1,表示不进行限制,可进行并发执行。maxConcurrentOperationCount 为1时,队列为串行队列。只能串行执行。maxConcurrentOperationCount 大于1时,队列为并发队列。操作并发执行,当然这个值不应超过系统限制,即使自己设置一个很大的值,系统也会自动调整为 min{自己设定的值,系统设定的默认最大值}。/ * 设置 MaxConcurrentOperationCount(最大并发操作数) */- (void)setMaxConcurrentOperationCount { // 1.创建队列 NSOperationQueue *queue = [[NSOperationQueue alloc] init]; // 2.设置最大并发操作数 queue.maxConcurrentOperationCount = 1; // 串行队列// queue.maxConcurrentOperationCount = 2; // 并发队列// queue.maxConcurrentOperationCount = 8; // 并发队列 // 3.添加操作 [queue addOperationWithBlock:^{ for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@“1—%@”, [NSThread currentThread]); // 打印当前线程 } }]; [queue addOperationWithBlock:^{ for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@“2—%@”, [NSThread currentThread]); // 打印当前线程 } }]; [queue addOperationWithBlock:^{ for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@“3—%@”, [NSThread currentThread]); // 打印当前线程 } }]; [queue addOperationWithBlock:^{ for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@“4—%@”, [NSThread currentThread]); // 打印当前线程 } }];}NSOperation 操作依赖NSOperation、NSOperationQueue 最吸引人的地方是它能添加操作之间的依赖关系。通过操作依赖,我们可以很方便的控制操作之间的执行先后顺序-(void)addDependency:(NSOperation *)op; 添加依赖,使当前操作依赖于操作 op 的完成。-(void)removeDependency:(NSOperation )op; 移除依赖,取消当前操 作对操作 op 的依赖。@property (readonly, copy) NSArray<NSOperation > dependencies; 在当前操作开始执行之前完成执行的所有操作对象数组。例:比如说有 A、B 两个操作,其中 A 执行完操作,B 才能执行操作。- (void)addDependency { // 1.创建队列 NSOperationQueue *queue = [[NSOperationQueue alloc] init]; // 2.创建操作 NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{ for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@“1—%@”, [NSThread currentThread]); // 打印当前线程 } }]; NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{ for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@“2—%@”, [NSThread currentThread]); // 打印当前线程 } }]; // 3.添加依赖 [op2 addDependency:op1]; // 让op2 依赖于 op1,则先执行op1,在执行op2 // 4.添加操作到队列中 [queue addOperation:op1]; [queue addOperation:op2]; NSOperation、NSOperationQueue 线程间的通信- (void)communication { // 1.创建队列 NSOperationQueue *queue = [[NSOperationQueue alloc]init]; // 2.添加操作 [queue addOperationWithBlock:^{ // 异步进行耗时操作 for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@“1—%@”, [NSThread currentThread]); // 打印当前线程 } // 回到主线程 [[NSOperationQueue mainQueue] addOperationWithBlock:^{ // 进行一些 UI 刷新等操作 for (int i = 0; i < 2; i++) { [NSThread sleepForTimeInterval:2]; // 模拟耗时操作 NSLog(@“2—%@”, [NSThread currentThread]); // 打印当前线程 } }]; }];}线程安全把这一块单独提出来是因为无论是使用 NSThread、GCD、NSOperation 等,在多个地方异步同时调用同一方法,会造成结果不符合预期,也就是线程不安全。线程不安全解决方案:可以给线程加锁,在一个线程执行该操作的时候,不允许其他线程进行操作。iOS 实现线程加锁有很多种方式。@synchronized、 NSLock、NSRecursiveLock、NSCondition、NSConditionLock、pthread_mutex、dispatch_semaphore、OSSpinLock、atomic(property) 等等各种方式,@synchronized、 NSLock这两种方式比较常用。银行取钱案例(例子是使用NSThread开辟的线程,给执行的代码加锁(或同步代码块),其他两种多线程方式同理)- (void)getMoney { Account *account = [[Account alloc] init]; account.accountNumber = @“1603121434”; account.balance = 1500.0; NSThread *thread1 = [[NSThread alloc] initWithTarget:account selector:@selector(draw:) object:@(1000)]; [thread1 setName:@“Thread1”]; NSThread *thread2 = [[NSThread alloc] initWithTarget:account selector:@selector(draw:) object:@(1000)]; [thread2 setName:@“Thread2”]; [thread1 start]; [thread2 start];}- (void)draw:(id)money{// 当多个线程同时操作的时候,会存在竞争条件,数据结果就无法保证// double drawMoney = [money doubleValue];// //判断余额是否足够// if (self.balance >= drawMoney)// {// //当前线程睡1毫秒// [NSThread sleepForTimeInterval:0.001];// self.balance -= drawMoney;// NSLog(@"%@ draw money %lf balance left %lf", [[NSThread currentThread] name], drawMoney, self.balance);// }else{// //余额不足,提示// NSLog(@"%@ Balance Not Enouth", [[NSThread currentThread] name]);// } // 我们对draw:方法添加了一个同步代码块,使用@synchronized包围的代码即为同步代码块,同步代码块需要一个监听器,我们使用account对象本身作为监听器,因为是account对象产生的竞争条件,当执行同步代码块时需要先获取监听器,如果获取不到则线程会被阻塞,当同步代码块执行完成则释放监听器// @synchronized (self) {// double drawMoney = [money doubleValue];// if (self.balance >= drawMoney)// {// //当前线程睡1毫秒// [NSThread sleepForTimeInterval:1];// self.balance -= drawMoney;// NSLog(@"%@ draw money %lf balance left %lf", [[NSThread currentThread] name], drawMoney, self.balance);// }else{// //余额不足,提示// NSLog(@"%@ Balance Not Enouth", [[NSThread currentThread] name]);// }// } // 我们使用锁机制,创建了一个NSLock类的锁对象,lock方法用于获取锁,如果锁被其他对象占用则线程被阻塞,unlock方法用于释放锁,以便其他线程加锁。 [self.lock lock]; double drawMoney = [money doubleValue]; if (self.balance >= drawMoney) { //当前线程睡1毫秒 [NSThread sleepForTimeInterval:1]; self.balance -= drawMoney; NSLog(@"%@ draw money %lf balance left %lf", [[NSThread currentThread] name], drawMoney, self.balance); }else{ //余额不足,提示 NSLog(@"%@ Balance Not Enouth", [[NSThread currentThread] name]); } [self.lock unlock];}本文所涉及的代码:threads ...

March 13, 2019 · 9 min · jiezi

程序员,金三银四该不该跳槽?

“金三银四”跳槽季,成了职场人跳槽旺季的代名词,同时也给了职场人一个极强的心理暗示:只要在这个旺季跳槽,那也大概率能比其他时间跳槽到一个更好的下家。然而职场规则比职场人想象的还要理性,一个岗位对于应聘者的要求并不会因为求职淡旺季有太大的区别,反而会因为招聘旺季提升选拔标准。就像一池鱼都想跳进一个筐的时候,织筐的人反而会把筐编得更高些,筛选掉那些弹跳力弱的鱼,从而选到最有活力的鱼。据《2018年春季白领跳槽指数调研报告》显示,在18年的“金三银四” 中12.9%的白领正在办理离职/入职手续,56.7%的白领已更新简历正在求职,也就是说在跳槽季积极行动的白领比例高达69.6%;有趣的是,55.8%的跳槽者在跳槽后明确表明对新工作比较失望。这个数据为职场人敲了警铃,“金三银四”为60%的优质人才提供了繁荣的氛围与充足的机会,而剩余40%的求职者在“金三银四”制造出的跳槽泡沫中,被折射的短暂斑斓晃了眼,乱了心,瞎跳比不跳更可怕。金三银四跳不跳:被动原因的跳槽忍则炼 主动原因的跳槽需慎重说到跳槽旺季,不得不说说职场人跳槽的心理,暂且不提被辞退后逼不得已的换工作,大多数跳槽的原因可以分为主动原因及被动原因两种。被动原因跳槽,指跳槽的原因大部分源于被动接收的某些情况,如与同事关系不融洽、在工作中受到了委屈等。大部分的被动原因都不建议贸然跳槽,每个企业的职场状况千奇百态,但糟心事的套路却大同小异。跳槽后,同样的状况未必不会发生,且可能还有其他不可预估的职场困难。而且,有时候不舒服的职场环境反而是职场人成长的加速器。当然,少部分挑战价值观的情况下,如被公司指派做违法职业道德的事等,还是当跳则跳。今天我们更多要聊的是主动原因跳槽的情况,主动原因跳槽最为普遍且重要的驱动因素不外乎三点:想要涨薪、寻求晋升、追求自我发展。在金三银四这样跳槽的活跃期,由这三点主动原因引发的跳槽想法,更需要多角度评估。以下针对涨薪、晋升、自我发展三个主动原因的跳槽tips,希望能够帮助职场人减少跳错的几率。Tip1:高薪3坑,跳槽勿踩2018年金三银四白领跳槽的原因中,排名第一的是薪酬水平,约占55.8%,薪资成了职场人衡量工作职位优劣的重要标准。在互联网行业,一般跳槽的薪资涨幅在20-30%左右,这个可观的数字对金三银四跃跃欲试的职场人来说,是个不小的诱惑,但这诱惑背后隐藏的坑,准备跳槽涨薪的职场人还需擦亮眼后再行动。第1坑,薪资涨幅小于等于其他成本的增加例如:朋友A在某二线城市互联网公司税前薪资10k,在跳槽旺季接到了某一线城市公司offer,允诺工资15k。心动于高薪的A在和一位资深HR朋友聊天时咨询朋友是否应该接受新offer,朋友给A算了一笔账,一线城市每个月房租比目前要增加2k,交通和吃饭等生活成本每月增加1k,往返回家的路费每月增加1k,社交费用每月增加0.5k,算下来和现在的工资并无差别。当跳槽会带来其他连锁成本的增加时,若增加成本>薪资涨幅,因为想涨薪而跳槽的你则要三思而后行了。第2坑,薪资构成里可能藏着圈套例如:朋友B春季跳槽到一家开出高薪的影视公司,待了三个月后怨声载道。原来新公司的高薪是由20%的基本工资+80%的绩效组成,签合同时HR再三口头强调平均每月绩效能拿90%。而实际情况是,无论工作完成多出色,员工的绩效都被各种理由扣去近一半。类似的情况时有发生,有的公司开出高价年薪,却有近一半是所谓的“年底绩效分红“,或使用各种理由进行克扣。所以,当收到一份高薪offer时,先把薪资构成和HR谈清楚后再开心也不迟。第3坑,公司状况不明朗导致降薪危机拿到高薪offer的职场人,在以上两个坑都排除的情况下,最好了解清楚公司现阶段业务及资金状况,特别是创业型公司,确保公司的状况能够长期支付高额薪资且不会发生转部门降薪等情况。Tip2:跳槽≠一定晋升除了涨薪之外,晋升也是职场人跳槽的强动力。对于在金三银四想通过跳槽来晋升的职场人,我们今天主要讨论两个词:职业轨迹和现有机会。例如:朋友C,含实习在内有3家知名互联网公司的工作经验,且每一份经验都在一年左右。当她再次跳槽到一家和前公司体量相似的公司时,依旧没能如愿以偿的升为管理岗。在和她的聊天中得知,她的每一份工作都是基础岗,做了一年没有得到晋升消息,也不愿再寻求内部晋升机会,便赶集似的找起了下家,而下家的评估多是因无管理岗经验,则需从基础岗做起。C的第一个问题在于,职业轨迹的停滞导致晋升的瓶颈。试想一下,如果B在某一份工作中耐住性子积累更多的能力和经验,从基础岗升到初级管理岗后再选择跳槽,有了管理岗经验的C通过跳槽从经理岗晋升到高级经理岗便容易的多。C的另一个问题在于,欠缺寻求现有机会的职场嗅觉。内部晋升的机会在某些方面也是有迹可循的,最为关键的一点是要看上级岗位是否有空缺,且同级的同事中自己是否具有竞争力。若经过评估,现有公司有升职机会且自己有获得晋升的能力,不妨在现有公司多待一段时间,也为下一次跳槽积累更多筹码。所以,当跳槽的最大诉求是晋升时,先回头审视一下自己的职业轨迹是否清晰可发展,再抬头看看现有公司是否藏着努力可得的升职机会。也许,目前的稳当是为了日后跳得更高。Tip3:自我定位与规划要先于跳槽的动作关于自我发展的问题,年后的种种跳槽诱惑也是陷阱颇多,而准确的自我职业发展定位就成了重中之重。一味的求变并不能解决问题,规划好自我职业定位与发展方向后再计划性谋动才是治本之道。很多职场人稀里糊涂就选择了一份工作,比如,自己好像不适合做技术,但偏偏选择了研发部门;自己好像不喜欢做销售,但为了尽快拿到offer,却选择了销售岗。之所以用“好像”这个词,是因为这些人中的绝大部分并不清楚自己想要什么样的工作,适合什么样的岗位,很多职场人把这种情况产生的焦虑感误以为是需要跳槽解决的自我发展需求,却忽略了在职场中自我发展的本质。不论身处职场哪个阶段,自我的审视是很有必要的,这也决定了你在后面长时间的职业规划。首先要自我判断和定位,按阶段制定自我发展的计划,拔高自己的格局,找到自我提升的动力,而盲目的跳槽可能适得其反,扰乱自己的思考。金三银四好似职场人一年一度的跳槽狂欢,更像是中国职场现状的缩影。战略布局的快速变化导致公司岗位需求波动频繁,适应力较弱的职场人被动选择跳槽,适应力强的职场人因为对自我职业要求的提升也总想往高处跳,而一些认知不清晰的职场人受到周围环境氛围的影响,也动起了跳槽的念头。跳槽,本应是在职业道路中往上攀登的助力工具,现在却好像成了大部分职场人不得不做的事。当一件事变得不得不做时,压迫感会阻碍思考的空间,从而出现盲目跳槽的情况。又是一年金三银四,愿每一个想要跳槽的职场人都明确初衷,擦亮眼睛,跳出一片似锦前程。想跳槽推荐一看BAT—最新iOS面试题总结iOS面试题大全(附答案)

March 13, 2019 · 1 min · jiezi

数据库的常用操作

一、技术起源数据库操作,不管是服务端、前端、移动端,都或多或少的会涉及到数据的存储、查询、修改。所以作为一名开发者,数据库操作也是开发必备的一项技能。SQL全称是Structured Query Language,翻译后就是结构化查询语言,是一种数据库查询和设计语言,用于存取数据与及查询、更新和管理关系数据库系统。常见的数据库有MySQL、SQLServer、ORACLE、DB2等等。二、数据库基础数据库操作概览图:数据库的基本操作步骤:1、创建数据库2、连接(打开)数据库3、创建表4、往表中加入数据5、更新数据、查询数据、删除数据6、断开(关闭)数据库1、建表(CREATE TABLE)CREATE TABLE emp ( id int NOT NULL PRIMARY KEY, //添加主键 name varchar(20), gender varchar(2), performance int, salary double)如果创建表后,忘记添加主键或者外键,可以使用ALERT添加。ALERT TABLE emp ADD PRIMARY KEY(id); //添加主键ALERT TABLE orders ADD FOREIGN KEY (e_id) REFERENCE emp(id); //添加外键2、INSERT(插入)向表中加入数据。//向emp 表中插入一条数据,插入字符串时使用’‘INSERT INTO emp VALUES(1, ‘yijie’, ‘male’, 85, 18000.0);向表中插入数据的标准格式是:insert into tableName(column1, column2…) values(‘value1’, ‘value2’…)3、UPDATE(更新)更新表中的数据。update emp set salary=20000 where name=‘yijie’;4、DELETE (删除)delete from emp where id=8;//删除表中的某条数据,where后面的为条件delete * from emp;//删除表中的所有数据,清空表drop table 表名称; //删除某张表注意:在使用delete删除表中数据时,如果该表与其他表有关联关系,如:外键,得先删除关联表中的外键。5、DISTINCT(去重)一张表经过一段时间的操作,避免不了会出现数据重复的情况。重复的数据不仅没有意义,而且占用存储空间。这个时候distinct就悄然登场了。distinct用于根据条件去除表中的重复内容。//查询emp中的name,返回唯一的名字select distinct name from emp;6、Select (查询)查询是数据库操作中最常用的操作,也是最难的。select语句用于从表中查询数据,结果被存储在一个结果表中(称为结果集)。SELECT 语法:SELECT 列名称 FROM 表名称; //查询表中的某列数据SELECT * FROM 表名称; //查询整张表还有更为复杂的条件查询。三、基础函数数据库还为我们提供了一些函数,方便我们进行数据库操作。这些基础函数基本都是列名为函数参数,返回某一列的计算结果。1.AVG()平均值avg()用于返回某列的平均值,NULL不包含在计算中。select AVG(salary) as avg_salary form emp; //查询员工的平均薪水2.COUNT()COUNT函数用于返回匹配指定条件的行数。select COUNT(*) from emp; //返回表的记录数3.MAX()MAX函数返回指定列的最大值,NULL字不包括在计算中。4.MIN()MIN函数返回指定列的最小值,NULL字不包括在计算中。5.SUM()SUM函数返回指定列的总数。6.ROUND()ROUND函数用于把数值字段舍入为指定的小数位数。select ROUND(salary,1) as n_salary from emp; //将salary保留一位小数select ROUND(column_name,decimals) from table_name;参数描述column_name要舍入的字段decimals规定要返回的小数位数7.FORMAT()FORMAT用于对指定字段的显示进行格式化。SELECT FROMAT(column_name,format) FROM table_name;参数描述column_name要格式化的字段format指定的格式四、高级用法还有一些SQL的高级用法,分页、模糊匹配、排序等等。1.分页(LIMIT)分页查询就是返回返回当前页码对应的页面的数据。分页查询的基本公式:(page - 1) * pageSize + 当前页要显示的数据条数select * from emp limit 4; //返回前4条数据2.模糊匹配(LIKE)模糊匹配是配合where条件使用的。//%可以理解为定义通配符select * from emp where name like ‘a%’; //返回以a开头的所有姓名3.IN返回特定列在某个集合中的所有数据。select * from emp where name in (‘AA’, ‘BB’); //返回name为AA、BB的所有数据。4.JOIN联表运算符JOIN,用于将两个或者两个以上的表进行关联,并从这些表中查询数据。常用的几种连接方式:INNER JOIN: 内连接。LETF JOIN:就算右表中没有匹配,也从左表中返回所有的行。RIGHT JOIN:就算左表中没有匹配,也从右表中返回所有的行。FULL JOIN:只要有一个表存在就返回。5.UNIONUNION运算符用于合并两个或多个SELECT语句的结果集。UNION内部的SELECT语句必须具有相同数量的列,列也必须具有相似的数据类型。同时,每条SELECT语句中列的顺序必须相同。6.AUTO_INCREMENT(自增)一般用于修饰主键,使其保持自增。7.ORDER BY (排序)使用order by对查询结果进行排序,默认是升序。ASC:升序(从小到大)DESC:降序(从大到小)select * from emp order by name;8.GROUP BY通常匹配合计函数使用,根据一个或者多个列队结果集进行分组。9.HAVING用于给分组设置条件。10.DEFAULTdefault约束用于向列中插入默认值。写在最后本文是对数据库中经常用到的一些写法与及函数的归纳总结,方便以后用到的时候能够快速查询到。题外话:主要是前段时间去面试的时候,被问到修改一条数据的语句怎么写时,竟然没有回到上来,所以决定对数据库的常用操作做一个总结。 ...

March 13, 2019 · 1 min · jiezi

Cocopods基础使用

一、安装和使用Cocopods网上已有很多教程,参考示例:CocoaPods安装教程二、组件库支持Cocopods方式引入1.创建远程代码仓库创建远程代码仓库(并不是podspec文件的仓库),此仓库放的是源代码。可以在GitHub上创建仓库。2.创建远程podspec仓库如果要发布到Cocopods的官方spec仓库(公开的),那么就不需要创建。当然私有库是需要创建的,在这一步两者不一样。公开库参考示例:发布开源库到Cocopods官方仓库3.创建本地代码工程可以使用pod命令创建,得到一个工程模板,并且可以根据需要配置工程,如下:命令创建工程模板pod lib create <组件库名>工程配置选择选择平台What platform do you want to use?? [ iOS / macOS ] iOS选择语言What language do you want to use?? [ Swift / ObjC ] ObjC是否自动生成一个用来做demo测试的模板库,建议Yes,后面方便测试 Would you like to include a demo application with your library? [ Yes / No ] Yes是否集成测试框架Which testing frameworks will you use? [ Specta / Kiwi / None ] NoneUI 测试Would you like to do view based testing? [ Yes / No ] No指定类前缀What is your class prefix? WT4.编写podspec文件如果用第三步的命令创建工程模板,那么在Podspec Metadata目录下已经自动生成了。如果是已有的工程或者库文件目录,也可以利用Pod命令自己制作.podspec文件,命令如下:pod spec cretae <组件库名>参考链接:podspec文件的具体说明5.验证cocoaPods索引文件命令如下:pod lib lint (从本地验证你的pod能否通过验证) pod spec lint (从本地和远程验证你的pod能否通过验证) pod lib lint –verbose (加–verbose可以显示详细的检测过程,出错时会显示详细的错误信息) pod lib lint –allow-warnings (允许警告,用来解决由于代码中存在警告导致不能通过校验的问题) pod lib lint –help (查看所有可选参数,可选参数可以加多个)6.本地测试库是否可用新建工程,切换到工程目录,执行命令pod init修改podfile文件, 并添加上本地库路径pod ‘库名’, :path => ‘/Users/xxx/Documents/库名’拉取pod代码:成功后可看到我们的库并没有在pods里面,而是在Development Pods里面,可用先检测代码有没有问题。7.提交工程代码提交工程代码到远程代码仓库,可以利用git或者svn进行代码版本管理,提交代码到GitHub等8.提交podspec文件开源库提交podspec文件到Cocopods官方仓库, 当然需要现在ocopods官方仓库中注册账号,命令如下:pod trunk me (检查是否注册trunk) pod trunk register <邮箱> <注册名字> –verbose (注册命令)注册完成之后会给你的邮箱发个邮件,进入邮箱邮件里面有个链接,需要点击确认一下.之后开始提交,切换到有.podspec文件的组件工程根目录执行命令pod trunk push <组件库名>.podspec pod trunk push <组件库名>.podspec –allow-warnings私有库提交podspec文件到远程podspec仓库,和Cocopods官方库不同的是,私有仓库需要先添加到本地仓库,再push到远程仓库,因为Cocopods默认已经添加到了本地仓库(默认为master),Mac系统可以查看文件目录(/.cocoapods/repos), 私有库命令如下:添加到本地仓库, git@git.xxxx.git为远端podspec库的地址,成功之后目录(/.cocoapods/repos)除了master之外,新增了一个文件夹(<组件库名>)pod repo add <组件库名> git@git.xxxx.git查看是否添加成功pod repo listpush到远程podspec仓库pod repo push <podspec远端仓库名> <组件库名>.podspec9. 检查仓库是否发布成功pod搜索一下:pod search <组件库名>如果报错,搜索不到,建议更新下pod:pod update之后仍然搜索不到,那么进入CocoaPods缓存目录,删除缓存索引文件search_index.json:cd ~/Library/Caches/CocoaPods ls rm -f search_index.json10. pod库文件引入如果是开源库(公有的),修改podfile文件:pod ‘组件库名’如果是私有仓库,建议在podfile文件开头添加source源:source ‘https://github.com/CocoaPods/Specs.git' #官方仓库地址source ‘http://xxx/组件库.git’ #私有podspecs仓库地址最后执行命令进行安装:pod install三、Cocopods打包静态库 ...

March 12, 2019 · 1 min · jiezi

Objective-C runtime学习 - 上 - 瞎逼逼

前言入坑iOS开发已经一年半,对所学所用的知识却一直没有好好做梳理,这里梳理一下我对Objective-C runtime的学习和理解。runtime是OC这个语言的核心,想要弄明白runtime,就不得不从语言设计层面开始说起,本篇主要讲讲这些宏观的东西,下篇再关注其实现细节与应用。正文20世纪80年代,大家逐渐意识到在复杂系统开发和GUI编程中,面向对象编程有巨大的价值。由于当时C语言的广泛应用,一个比较直接的想法就是,在C语言的基础上扩展出面向对象能力,能够最大程度上减少开发者的迁移成本。于是Objective-C和C++都在这段时间里诞生了。不过这两种语言在实现上都有着巨大的区别。Objective-C选择使用C语言实现一套面向对象的库(我们称之为Objective-C的runtime),并给Objective-C添加了一些面向对象的语法。编译时,Objective-C的语法被处理为C的语法,然后和runtime库放到一起编译。因此,Objective-C可以基于runtime实现一些动态特性,我的程序里打包了一个运行时系统嘛。而C++选择完全在编译期实现其面向对象特性。这两种截然不同的实现,反映了设计者对语言动态特性的不同考量。其中最重要的一点是,对“面向对象”这一编程范式的理念不同。面向对象思想意味着把程序抽象为各种独立而又互相调用的对象。在语言设计上,这里需要面对一个具体的问题,对象间的通信,应当是种什么形式?直觉上,对象间的通信,直接用方法调用的形式就好了。世界上第一个面向对象的语言Simula就是这么实现的。但实际上会面临一些问题。具体来讲,当你调用一个对象的一个方法时,你怎么知道它有这个方法呢?这意味着你要确定这个对象的类型,也就是说,你必须先知道一个对象的类型才能对它进行调用。在很多场景下,这种限制是不必要的。于是,世界上第二个面向对象的语言Smalltalk进行了一些改进,它实现了一套与目标对象无关的消息传递机制。消息的发送方可以不关注接收方的类型,也不关注接收方是否可以正常处理这条消息。而消息的接收方需要对收到的消息做处理,有对应的方法实现就去调用,没有就走异常处理流程。C++传承了Simula的“静态消息机制”,而Objective-C传承了Smalltalk的“动态消息机制”。这两者的区别看似没那么大,却在很大程度上影响了之后面向对象体系的发展。回到我们的Objective-C,为了实现这种“动态消息机制”,通常需要一个运行时系统来处理。Objective-C的发明者选择了最简单有效的方式,实现一套简单的运行时系统跟程序打包到一起。比起后来Java的虚拟机或JavaScript的执行引擎,Objective-C的实现充分利用了原有的C编译器,只要给Objective-C做个预处理器,把Objective-C代码转成C代码然后和runtime库放一起用C的编译器编译就可以了,实现难度和工作量都是比较低的。此外,这样的实现保证了Objective-C跟C的完全兼容,可以说,Objective-C代码在编译时就是C代码,因此可以和C或C++混编。结语总体而言,Objective-C的语言设计至少在当时还是比较优越的。“动态消息机制”现在看来还是带来了一些好处的。而在C的基础上添加一个很薄的runtime层来为C提供面向对象机制,也是个很合适的做法。后记想要从宏观上理解清楚Objective-C runtime的来龙去脉,其实需要对整个面向对象的发展史有足够的理解,对面向对象的各种实现的优缺点有一定认知,显然我的能力是远远不够的。本文也只能简单梳理我个人的一点理解,可能还有颇多错漏。另外,Objective-C的语言设计在当时有着不少优点的,但是Objective-C的语言发展已经停滞不前多年,比起一些热门的语言它在一定程度上是落后不少的,而苹果已经拥抱swift,因此这种停滞可能是永久性的。扩展阅读function/bind的救赎(上)——孟岩C++多继承有什么坏处,Java的接口为什么可以摈弃这些坏处? - invalid s的回答 - 知乎

March 10, 2019 · 1 min · jiezi

iOS数据持久化方案

技术由来数据持久化是iOS开发中必不可少的一项技能。因为开发中我们多会涉及到用户信息存储、文件存储、应用内容缓存中的一个或者几个场景。数据持久化的几种方式NSUserDefaultsplistkeychain(钥匙串)归档沙盒数据库数据持久化几种方式的一览图:1.NSUserDefaultsNSUserDefaults用于存储用户的偏好设置和用户信息,如用户名,是否自动登录,字体大小等.数据自动保存在沙盒的Libarary/Preferences目录下.NSUserDefaults将输入的数据储存在.plist格式的文件下,这种存储方式就决定了它的安全性几乎为0,所以不建议存储一些敏感信息如:用户密码,token,加密私钥等!它能存储的数据类型为:NSNumber(NSInteger、float、double),NSString,NSDate,NSArray,NSDictionary,BOOL.不支持自定义对象的存储.使用注意点:NSUserDefaults存储的数据都是不可变的,想将可变数据存入需要先转为不可变才可以存储.NSUserDefaults是定时把缓存中的数据写入磁盘的,而不是即时写入,为了防止在写完NSUserDefaults后程序退出导致的数据丢失,可以在写入数据后使用synchronize强制立即将数据写入磁盘.2.plist即属性列表文件,全名是Property List,这种文件的扩展名为.plist,因此,通常被叫做plist文件。它是一种用来存储串行化后的对象的文件,用于存储程序中经常用到且数据量小而不经常改动的数据。可以存储的类型:NSNumber,NSString,NSDate,NSData ,NSArray,NSDictionary,BOOL.不支持自定义对象的存储.使用注意点:如果需要存储自定义类型的数据需要先进行序列化!3.Keychain(钥匙串)用于本地重要数据的存储,将数据加密后存储在本地更安全.如:密码,秘钥,序列号等.当你删除APP后Keychain存储的数据不会删除,所以在重装App后,Keychain里的数据还能使用。从ios 3.0开始,跨程序分享keychain变得可行而NSUserDefaults存储的数据会随着APP而删掉.使用keychain时苹果官方已经为我们封装好了文件KeychainItemWrapper,引入即可使用.当然也可是使用其他优秀的第三方的封装,比如ssKeychain。keychain的使用方法4.归档(NSKeyedArchiver)归档是iOS开发中数据存储常用的技巧,归档可以直接将对象储存成文件,把文件读取成对象。相对于plist或者userdefault形式,归档可以存储的数据类型更加多样,并且可以存取自定义对象。对象归档的文件是保密的,在磁盘上无法查看文件中的内容,更加安全。遵守NSCoding协议,并实现该协议中的两个方法。如果是继承,则子类一定要重写那两个方法。因为子类在存取的时候,会去子类中去找调用的方法,没找到那么它就去父类中找,所以最后保存和读取的时候新增加的属性会被忽略。需要先调用父类的方法,先初始化父类的,再初始化子类的。保存数据的文件的后缀名可以随意命名。存储类型安全性文件名后缀数据量大小应用场景NSUserDefaults不安全plist小用户偏好设置,用户名plist不安全plist小不经常改动keychain安全 小密码、秘钥、序列号归档安全任意大缓存5.沙盒持久化在Document目录下,一般存储非机密数据。当App中涉及到电子书阅读、听音乐、看视频、刷图片列表等时,推荐使用沙盒存储。因为这可以极大的节约用户流量,而且也增强了app的体验效果.Application:存放程序源文件,上架前经过数字签名,上架后不可修改。Documents: 保存应运行时生成的需要持久化的数据,iTunes同步设备时会备份该目录。例如,游戏应用可将游戏存档保存在该目录。tmp: 保存应运行时所需的临时数据,使⽤完毕后再将相应的文件从该目录删除。应用没有运行时,系统也可能会清除该目录下的文件。iTunes同步设备时不会备份该目录。Library/Caches: 保存应用运行时生成的需要持久化的数据,iTunes同步设备时不会备份该目录。一般存储体积大、不需要备份的非重要数据,比如网络数据缓存存储到Caches下。Library/Preference: 保存应用的所有偏好设置,如iOS的Settings(设置) 应会在该目录中查找应⽤的设置信息。iTunes同步设备时会备份该目录。6.数据库存储数据量较大的数据,一般使用数据库来存储。如:FMDB、CoreData、Realm、WCDB。6.1 FMDBFMDB是iOS平台的SQLite数据库框架,FMDB以OC的方式封装了SQLite的C语言API,使用起来更加面向对象,省去了很多麻烦、冗余的C语言代码,对比苹果自带的Core Data框架,更加轻量级和灵活,提供了多线程安全的数据库操作方法,有效地防止数据混乱.6.2 CoreDataCore Data是iOS5之后才出现的一个框架,它提供了对象-关系映射(ORM)的功能,即能够将OC对象转化成数据,保存在SQLite数据库文件中,也能够将保存在数据库中的数据还原成OC对象。在此数据操作期间,我们不需要编写任何SQL语句.但是直接操作CoreData显的不是那么容易,所以我多数的时候会使用MagicRecord来实现.MagicRecord是对CoreData的二次封装,使用起来简单操作方便.6.3 RealmRealm的使用Realm 的GitHub传送门6.4 WCDBWCDB是微信移动端开源的数据库组件。WCDB的使用介绍WCDB的详细介绍WCDB 的GitHub传送门写在最后本文主要是对iOS开发过程中使用到的数据持久化方案的一个归纳整理,有些其实我自己也没有具体使用过,但是附上了相应的链接,有需要的同学可以具体的去学习。参考:http://www.cocoachina.com/ios…

March 9, 2019 · 1 min · jiezi

跨越逐梦路上的荆棘(程序猿生存篇)

时光荏苒,岁月如梭。曾经的小学生,初中生现如今已是社会的中流砥柱。随着科技时代的迅猛发展,曾经的荒土,已是星罗棋布高楼耸立。其中不乏科技相关的楼宇,俗称互联网公司。也许你,现在就正处于其中。一个互联网公司要正常运作,必定就需要一群足智多谋,思维敏捷,逻辑缜密的程序猿。写的一手好代码,注释都能写出花,懂得算法与结构,调试上线都不怕。你还记得美国动漫《超人》吗?超人虽然拥有近乎无限的超能力,但仍有着很多弱点,怕氪石(绿色水晶)就是其中一个。这群能力超凡的人类,其实也有不堪一击的致命弱点 。或许你会想,技术更新快 学不动? 需求总是改 想狗带? 这些都是工作层面所带来的,其实我想说的是我们的身体。 很多时候,我们为了工作,夜以继日,披星戴月。不辞辛苦的开发,测试。为的就是得到他人的认可,同时证明自己,顺便领取一部分薪酬。但往往有些人认为,it行业高薪就业,为了钱不要怂,就是干,我给钱,你就给我干!但我所希望的是这份钱买来的是快乐和健康,而不是病痛与泪水。我们透支了自己的身体,去换取自己应得的报酬。同时我们也该拥有同等的尊重。该休息,陪伴家人的时候,不要用单位的一些条款和福利来约束我应享有的自由。 为你工作,并不代表为你卖命。 我希望每一位程序员都应该爱护自己的身体,多多锻炼,少少加班。最后我想问一个选择题:换做是你,你会选什么?(能分享分享吗?)let salary = (心理预期值 * 3/4) 举例:7500 = 10K * 3/4A.月薪 = salary && 双休 && 不加班复制代码B.月薪 = salary*(1+1/2) && (996工作制)复制代码C.月薪 = (salary*2+股份) && (997工作制)复制代码D.欢迎补充…

March 9, 2019 · 1 min · jiezi

程序员—10条求职的黄金规律

来看一下金三银四的招聘旺季下,10条求职的黄金规律。可以说每一条都很有一定深度01:很多时候,HR不要你,不是因为你水平的问题,也不是因为你专业技能的问题。而是HR自己对自己没信心,HR没把握你这样的候选人,会不会踏实地在部门内做事。HR觉得你够聪明,够优秀,但不敢用你,因为他们担心花了很大的精力去培养你,最后你没花心思放在这份工作上,这对HR和用人部门都是很大的打击。【不要怀疑自己】02:薪水高是否意味着一份好工作,答案无疑是否定的。一般情况下,薪水和期待成正比,既然有人给了你更高的经济回馈,那就意味着对你的期待更高。而一个人创造的价值并不完全由自己决定,还依赖于客观的条件,比如团队、客户、同事、客户、周期等。如果你要先享受更大的收益,然后再去创造价值,往往翻车的概率会很大。03:手里攥着Offer 来谈更高条件的候选人,一般不会被待见。如果单从薪酬上看,永远都有可能比当下更高薪的工作在等着自己,拿着Offer 来谈条件的候选人往往会被认定稳定性存疑。收入不是不重要,但不应该是决定一个人是否加入一家公司的先决条件。特别是工作数年后还对薪酬非常纠结的话,可能压根就没有对自己和外部环境有一个清晰的认识。04:企业对外招聘的时候,大家都不要太在意招聘广告上的薪酬范围数值,这个数值往往并不是公司实际对这个岗位的定薪标准。确实,薪酬写的越高越能吸引人,但职位工作的内容和挑战,会因为薪资的关系被弱化甚至被忽视。像在阿里,看官网上的招聘,不会放出某个职位的薪资范围,销售岗位偶尔例外。05:如果你真的有两把刷子,学历限制、工作年限条件、专业背景要求都不是问题。公司的 JD 是 HR 部门写的,HR希望能够最大程度上用高效率的方式筛选到合适人才。但实际的用人部门的需求更现实,用人部门只在乎来的人能不能解决问题。在阿里也有大专甚至中专的同事,一点都不影响他们成为公司的优秀员工,在职场上的员工优秀与否和学历有时候并不是正向关系。06:如果你现在的领导,排斥异己,容不下不同的声音,搞裙带关系,专心培养自己的所谓派系,评定业绩的时候做不到看业绩说话,那就早点离开,不要把自己有限的人生浪费在无聊的蝇营狗苟上。而且离职也并不是一件坏事,离职在另外一方面有助于提升自己的认知,扩大自己的视野,机会也会更多,所以别总纠结着或依依不舍,成年人都懂得取舍。1条观点07:人有三观,企业也有。但三观约束自己还行,不能用它来界定他人。因为你不是对方,你不了解对方,你对其他一切知之甚少。不要因为局部而否定整体,每家公司都有自己的问题,我们是选择一个适合自己的平台,不是扮演企业的道德和伦理的警察,用自己的三观来判断一家公司的好坏,这很幼稚,所谓“三观正”其实是个简称啦,全称是:“三观正好和我一样”。08:如果真的想好好锻炼自己的能力,那一开始就不要先去环境特别稳定、管理特别健全的公司。我们以HR来举例,现在人力资源工作在一些超大型的企业里,已经分工的非常细,某些环节跟工厂的流水线差不多,流水线一多,就会让HR学习能力不够强,学习速度不够快,影响了个人发展。倘若你已经在超大公司的内部工作,那也尽量选择有挑战的事业部。09:求职受挫,简历被虐,面试碰壁… …这些都不是你可以气馁的理由。求职中的挫折在工作挑战面前有时候都不值得一提,失败的场景以后还会经常遇到,所以你还是提前让自己内心坚强一点,别总玻璃心,没人同情你的脆弱内心。受挫之余,抓紧学习,在别人玩的时候你在偷偷练级,这才是你应该做的事情。10:没有什么企业或单位是完美的,没有缺陷的,每个公司都会有一些自己的问题,就算公司很好,你也有很大概率会遇到一些不那么好的同事,上司或者合作伙伴。你不可避免会和自己不喜欢的人一起共事,但重要的是你的耐心,有耐心的人和任何人都能配合好工作,没耐心的人半年就换一份工作。推荐一看BAT—最新iOS面试题总结iOS面试题大全(附答案)参考原文地址

March 9, 2019 · 1 min · jiezi

iOS开发—音视频入门学习必看

音视频学习从零到整–(2)音视频学习从零到整–(3)音视频学习从零到整–(4)音视频学习从零到整–(5)音视频学习从零到整–(6)音视频学习从零到整–(7)一.音频基础复习1.1 声音的产生相对于视频,可观察这个现象.音频在学习过程,就缺乏了想象的空间.但是如果从原理出发,就不会那么难了.声音是什么?声音是波,靠物体的振动产生1.2 声波的3要素声波的三要素,是频率,振幅,波形.频率代表音阶的高低,振幅代表响度,波形则代表音色.频率越高,波长就会越短.而低频声响的波长则较长.所以这样的声音更容易绕过障碍物,能量衰减就越小.声音就会传播的越远.响度,就是能量大小的反馈.用不同的力度敲打桌面,声音的大小势必发生变换.在生活中,我们用分贝描述声音的响度.==小贴士==分贝(decibel),是度量声音的强度单位,常用dB表示.是由美国发明家亚历山大.格雷厄姆.贝尔 名字命名的.长期在夜晚接受50 分贝的噪音, 容易导致心血管疾病; 55 分贝, 会对儿童学习产生负面影响; 60分贝, 让人从睡梦中惊醒; 70 分贝,心肌梗死的发病率增加30%左右; 超过110 分贝, 可能导致永久性听力损伤.音色,在同样的频率和响度下,不同的物体发出的声音不一样.比如钢琴和古筝声音就完全不同.波形的形状决定了声音的音色.因为不同的介质所产生的波形不同.就会产生不一样的音色.1.3 声音传播声音的发生,来源于振动.人类说话,从声带振动发生声音之后,经过口腔,颅腔等局部区域的反射,在经过空气传播到别人耳朵中.这是我们说话到听到的过程.声音的传播,可以通过空气,液体,固定传播.介质不同,会影响声音的传播速度.吸音棉:通过声音反射而产生的嘈杂感,吸音材料选择使用可以衰减入射音源的反射能量,从而对原有声音的保真效果.比如录音棚墙壁上就会使用吸音材质隔音:主要解决声音穿透而降低主体空间的吵闹感,隔音棉材质可以衰减入射声音的透射能量.从而达到主体空间安静状态,比如KTV墙壁上就会安装隔音棉材料.二.数字音频2.1 模拟信号数字化过程将模拟信号转换为数字信号的过程,分别是采样,量化和编码.音频采样对模型信号进行采样,采样可以理解为在时间轴上对信号进行数字化.而,根据奈斯特定理(采样定理),按比声音最高频率高2倍以上的频率对声音进行采样.这个过程称为AD转换.比如,前面提到高质量音频信号,其频率范围是20Hz-20KHz.所以采样频率一般是44.1KHz.这样可以保证采样声音达到20KHz也能被数字化.而且经过数字化处理后的声音,音质也不会降低.44.1KHZ,指的是1秒会采样44100次奈斯特定理(采样定理) 资料量化量化,指的是在幅度轴上对信号进行数字化.简单的说,就是声音波形的数据是多少位的二进制数据,通常用bit做单位.比如16比特的二进制信号来表示声音的一个采样.它的取值范围[-32768,32767].一共有65536个值.如16bit、24bit。16bit量化级记录声音的数据是用16位的二进制数,因此,量化级也是数字声音质量的重要指标。我们形容数字声音的质量,通常就描述为24bit(量化级)、48KHz采样,比如标准CD音乐的质量就是16bit、44.1KHz采样.既然每个量化都是一个采样,那么声音这么多采样,该如何将这些数据存储起来?编码什么叫编码?按照一定格式记录采样和量化后的数据.音频编码的格式有很多种,而通常所说的音频裸数据指的是脉冲编码调制(PCM)数据.如果想要描述一份PCM数据,需要从如下几个方向出发:量化格式(sampleFormat)采样率(sampleRate)声道数(channel)举例:以CD音质为例,量化格式为16bite,采样率为44100,声道数为2.这些信息描述CD音质.那么可以CD音质数据,比特率是多少?44100 * 16 * 2 = 1378.125kbps那么一分钟的,这类CD音质数据需要占用多少存储空间?1378.125 * 60 /8/1024 = 10.09MB如果sampleFormat更加精确或者sampleRate更加密集,那么所占的存储空间就会越大,同时能够描述的声音细节就会更加精确.存储在这些二进制数据即可理解为将模型信号转化为数字信号.那么转为数字信号之后,就可以对这些数据进行存储播放复制获取其他任何操作.推荐文集* 抖音效果实现* BAT—最新iOS面试题总结* iOS面试题大全(附答案)原文作者:集才华美貌于一身的—C姐

March 7, 2019 · 1 min · jiezi

iOS 动画使用总结

前言以前要做动画方面相关的功能时都是去百度,想着自己也总结下,方便自己记忆和查找,更加深自己对这方面的理解iOS 图形分层日常开发 UIKIt 层和 Core Animation 层的动画基本可以实现绝大多数需求,并且层级越高 API 的封装程度越高,也越简洁。本文主要讲解 View Animation 和 Core Animation。UIKit 层UIView Animation我们来看看通过 UIView Animation 都可以实现那些动画大小动画(frame改变)#pragma mark ———————大小动画,frame 改变- (void)initSizeAnimation { CGRect origin = self.showImage.frame; CGRect terminal = CGRectMake(SCREEN_W/2-100, SCREEN_H/2-100, 200, 200); [UIView animateWithDuration:1 animations:^{ self.showImage.frame = terminal; } completion:^(BOOL finished) { [UIView animateWithDuration:1 animations:^{ self.showImage.frame = origin; }]; }];}拉伸动画 bounds 改变#pragma mark ——————拉伸动画 bounds 改变- (void)initBoundsAnimation { CGRect origin = self.showImage.bounds; //拉伸动画基于view的bound改变,只改变宽高, CGRect terminal = CGRectMake(0, 0, 200, 150); [UIView animateWithDuration:1 animations:^{ self.showImage.bounds = terminal; } completion:^(BOOL finished) { [UIView animateWithDuration:1 animations:^{ self.showImage.bounds = origin; }]; }];}中心位置动画,改变center#pragma mark —————-中心位置动画,改变center- (void)initCenterAnimation { CGPoint origin = self.showImage.center; CGPoint terminal = CGPointMake(self.showImage.center.x, self.showImage.center.y-100); [UIView animateWithDuration:1 animations:^{ self.showImage.center = terminal; } completion:^(BOOL finished) { [UIView animateWithDuration:1 animations:^{ self.showImage.center = origin; }]; }];}旋转动画,改变transform#pragma mark —————-旋转动画,改变transform- (void)initTransformAnimation { CGAffineTransform origin = self.showImage.transform; [UIView animateWithDuration:2 animations:^{// self.showImage.transform = CGAffineTransformMakeScale(0.6, 0.6);//缩放// self.showImage.transform = CGAffineTransformMakeTranslation(60, -60);//偏移 self.showImage.transform = CGAffineTransformMakeRotation(4.0f);//旋转 } completion:^(BOOL finished) { [UIView animateWithDuration:2 animations:^{ self.showImage.transform = origin; }]; }];}透明度动画 改变alpha#pragma mark —————-透明度动画 改变alpha- (void)initAlphaAnimation { [UIView animateWithDuration:2 animations:^{ self.showImage.alpha = 0.3; } completion:^(BOOL finished) { [UIView animateWithDuration:2 animations:^{ self.showImage.alpha = 1; }]; }];}转场动画 transition#pragma mark —————-转场动画 transition- (void)initTransitionAnimation { [UIView transitionWithView:self.showImage duration:2.0 options:UIViewAnimationOptionTransitionFlipFromTop animations:^{ } completion:^(BOOL finished) { [UIView transitionWithView:self.showImage duration:2 options:UIViewAnimationOptionTransitionFlipFromBottom animations:^{ } completion:^(BOOL finished) { }]; }];}spring 动画(弹簧效果)(ios7以上)#pragma mark —————-spring 动画(弹簧效果)- (void)initSpringAnimation { CGRect origin = self.showImage.frame; CGRect terminal = CGRectMake(origin.origin.x+50, origin.origin.y, 150, 150); [UIView animateWithDuration:1 delay:0 usingSpringWithDamping:0.5 initialSpringVelocity:4 options:UIViewAnimationOptionCurveLinear animations:^{ self.showImage.frame = terminal; } completion:^(BOOL finished) { [UIView animateWithDuration:1 delay:1 usingSpringWithDamping:0.5 initialSpringVelocity:4 options:UIViewAnimationOptionCurveLinear animations:^{ self.showImage.frame = origin; } completion:^(BOOL finished) { }]; }];}背景颜色动画#pragma mark —————-背景颜色动画 改变 background- (void)initBackgroundAnimation { self.showImage.image = [UIImage imageNamed:@“example1”]; [UIView animateKeyframesWithDuration:6.0 delay:0.f options:UIViewKeyframeAnimationOptionCalculationModeLinear animations:^{ [UIView addKeyframeWithRelativeStartTime:0.f relativeDuration:1.0 / 4 animations:^{ self.showImage.backgroundColor = [UIColor redColor]; }]; [UIView addKeyframeWithRelativeStartTime:1.0 / 4 relativeDuration:1.0 / 4 animations:^{ self.showImage.backgroundColor = [UIColor greenColor]; }]; [UIView addKeyframeWithRelativeStartTime:2.0 / 4 relativeDuration:1.0 / 4 animations:^{ self.showImage.backgroundColor = [UIColor yellowColor]; }]; [UIView addKeyframeWithRelativeStartTime:2.0 / 4 relativeDuration:1.0 / 4 animations:^{ self.showImage.backgroundColor = [UIColor greenColor]; }]; [UIView addKeyframeWithRelativeStartTime:1.0 / 4 relativeDuration:1.0 / 4 animations:^{ self.showImage.backgroundColor = [UIColor whiteColor]; }]; } completion:^(BOOL finished) { NSLog(@“动画结束”); }];}UIViewAnimationOptionsUIViewAnimationOptionLayoutSubviews //进行动画时布局子控件UIViewAnimationOptionAllowUserInteraction //进行动画时允许用户交互UIViewAnimationOptionBeginFromCurrentState //从当前状态开始动画UIViewAnimationOptionRepeat //无限重复执行动画UIViewAnimationOptionAutoreverse //执行动画回路UIViewAnimationOptionOverrideInheritedDuration //忽略嵌套动画的执行时间设置UIViewAnimationOptionOverrideInheritedCurve //忽略嵌套动画的曲线设置UIViewAnimationOptionAllowAnimatedContent //转场:进行动画时重绘视图UIViewAnimationOptionShowHideTransitionViews //转场:移除(添加和移除图层的)动画效果UIViewAnimationOptionOverrideInheritedOptions //不继承父动画设置UIViewAnimationOptionCurveEaseInOut //时间曲线,慢进慢出(默认值)UIViewAnimationOptionCurveEaseIn //时间曲线,慢进UIViewAnimationOptionCurveEaseOut //时间曲线,慢出UIViewAnimationOptionCurveLinear //时间曲线,匀速UIViewAnimationOptionTransitionNone //转场,不使用动画UIViewAnimationOptionTransitionFlipFromLeft //转场,从左向右旋转翻页UIViewAnimationOptionTransitionFlipFromRight //转场,从右向左旋转翻页UIViewAnimationOptionTransitionCurlUp //转场,下往上卷曲翻页UIViewAnimationOptionTransitionCurlDown //转场,从上往下卷曲翻页UIViewAnimationOptionTransitionCrossDissolve //转场,交叉消失和出现UIViewAnimationOptionTransitionFlipFromTop //转场,从上向下旋转翻页UIViewAnimationOptionTransitionFlipFromBottom //转场,从下向上旋转翻页UIViewKeyframeAnimationOptionsUIViewAnimationOptionLayoutSubviews //进行动画时布局子控件UIViewAnimationOptionAllowUserInteraction //进行动画时允许用户交互UIViewAnimationOptionBeginFromCurrentState //从当前状态开始动画UIViewAnimationOptionRepeat //无限重复执行动画UIViewAnimationOptionAutoreverse //执行动画回路UIViewAnimationOptionOverrideInheritedDuration //忽略嵌套动画的执行时间设置UIViewAnimationOptionOverrideInheritedOptions //不继承父动画设置UIViewKeyframeAnimationOptionCalculationModeLinear //运算模式 :连续UIViewKeyframeAnimationOptionCalculationModeDiscrete //运算模式 :离散UIViewKeyframeAnimationOptionCalculationModePaced //运算模式 :均匀执行UIViewKeyframeAnimationOptionCalculationModeCubic //运算模式 :平滑UIViewKeyframeAnimationOptionCalculationModeCubicPaced //运算模式 :平滑均匀总结UIView 动画主要变化 UIView 自身的属性一个效果有多种方式实现,通过组合也可以实现比较高级的动画效果Core AnimationCore Animation是直接作用在 CALayer 上的,iOS 和 Mac OS 都可以使用,Core Animation 的动画过程都是在后台操作的,不会阻塞主线程。下面是 Core Animation 所涉及的几个类的继承关系CAMediaTiming 协议中定义了时间、速度、重复次数等,包含属性:beginTime:设置延时时间duration:持续时间speed:动画速率timeOffset:动画时间偏移量repeatCount:动画的重复次数等repeatDuration:重复时间autoreverses:结束后是否反过来恢复到初始值fillMode:当前对象在非对象时间段的初始值CAAnimation 核心动画基础类,不能直接使用timingFunction -> 控制动画的节奏。系统提供的包括:kCAMediaTimingFunctionLinear (匀速)kCAMediaTimingFunctionEaseIn (慢进快出)kCAMediaTimingFunctionEaseOut (快进慢出)kCAMediaTimingFunctionEaseInEaseOut (慢进慢出,中间加速)kCAMediaTimingFunctionDefault (默认),当然也可通过自定义创建CAMediaTimingFunctiondelegate -> 代理。emovedOnCompletion -> 是否让图层保持显示动画执行后的状态,默认为YES,也就是动画执行完毕后从涂层上移除,恢复到执行前的状态,如果设置为NO,并且设置fillMode为kCAFillModeForwards,则保持动画执行后的状态。CAPropertyAnimation 属性动画,针对对象的可动画属性进行效果的设置,不可直接使用。keyPath -> CALayer的某个属性名,并通过这个属性的值进行修改,达到相应的动画效果。additive -> 属性动画是否以当前动画效果为基础,默认为NO。cumulative -> 指定动画是否为累加效果,默认为NO。valueFunction -> 此属性配合CALayer的transform属性使用。CABasicAnimation基础动画,通过keyPath对应属性进行控制,需要设置fromValue以及toValue,只能在两个属性间变化。fromValue -> keyPath相应属性的初始值。toValue -> keyPath相应属性的结束值。byValue -> 在不设置toValue时,toValue = fromValue + byValue,也就是在当前的位置上增加多少。CASpringAnimation 带有初始速度以及阻尼指数等物理参数的属性动画。mass -> 小球质量,影响惯性stiffness -> 弹簧的劲度系数damping -> 阻尼系数,地面的摩擦力initialVelocity -> 初始速度,相当于给小球一个初始速度(可正可负,方向不同)settlingDuration -> 结算时间,根据上述参数计算出的预计时间,相对于你设置的时间,这个时间比较准确。CAKeyframeAnimation 关键帧动画,同样通过keyPath对应属性进行控制,但它可以通过values或者path进行多个阶段的控制values -> 关键帧组成的数组,动画会依次显示其中的每一帧path -> 关键帧路径,动画进行的要素,优先级比values高,但是只对CALayer的anchorPoint和position起作用keyTimes -> 每一帧对应的时间,如果不设置,则各关键帧平分设定时间timingFunctions -> 每一帧对应的动画节奏calculationMode -> 动画的计算模式,系统提供了对应的几种模式tensionValues -> 动画张力控制continuityValues -> 动画连续性控制biasValues -> 动画偏差率控制rotationMode -> 动画沿路径旋转方式,系统提供了两种模式。CATransition 转场动画,系统提供了很多酷炫效果type -> 转场动画类型subtype -> 转场动画方向startProgress -> 动画起点进度(整体的百分比)endProgress -> 动画终点进度(整体的百分比)filter -> 自定义转场。CAAnimationGroup 动画组,方便对于多动画的统一控制管理。animations -> 所有动画效果元素的数组。CABasicAnimation在一般的应用开发中,基础动画可以满足大部分的开发需求,主要完成对于对象指定动画属性两个Value之间的动画过渡。下面展示使用 CABasicAnimation 实现 位移、缩放、透明度、旋转、圆角 的核心代码switch (button.tag) { case 0: //位移动画 basicAni = [CABasicAnimation animationWithKeyPath:@“position”]; //到达位置// basicAni.byValue = [NSValue valueWithCGPoint:CGPointMake(100, 100)]; basicAni.toValue = [NSValue valueWithCGPoint:CGPointMake(_mainLayer.position.x+100, _mainLayer.position.y+100)]; break; case 1: //缩放动画 basicAni = [CABasicAnimation animationWithKeyPath:@“transform.scale”]; //到达缩放 basicAni.toValue = @(0.1f); break; case 2: //透明度动画 basicAni = [CABasicAnimation animationWithKeyPath:@“opacity”]; //透明度 basicAni.toValue=@(0.1f); break; case 3: //旋转动画 basicAni = [CABasicAnimation animationWithKeyPath:@“transform”]; //3D basicAni.toValue=[NSValue valueWithCATransform3D:CATransform3DMakeRotation(M_PI_2+M_PI_4, 1, 1, 0)]; break; case 4: //圆角动画 basicAni = [CABasicAnimation animationWithKeyPath:@“cornerRadius”]; //圆角 basicAni.toValue=@(50); break; CASpringAnimationCASpringAnimation 是 iOS9引入的动画类,类似于 UIView 的 spring 动画,但是增加的质量,劲度系数等属相的扩展,继承自 CABaseAnimation,用法也比较简单:#pragma mark ———————–CASpringAniamtion - (void)initSpringAnimation { CASpringAnimation springAni = [CASpringAnimation animationWithKeyPath:@“position”]; springAni.damping = 2; springAni.stiffness = 50; springAni.mass = 1; springAni.initialVelocity = 10; springAni.toValue = [NSValue valueWithCGPoint:CGPointMake(200, 400)]; springAni.duration = springAni.settlingDuration; [_mainLayer addAnimation:springAni forKey:@“springAnimation”];}CAKeyframeAnimationCAKeyframeAnimation和CABasicAnimation一样是CApropertyAnimation的子类,但是CABasicAnimation只能从一个数值(fromValue)变到另一个数值(toValue)或者添加一个增量数值(byValue),而CAKeyframeAnimation使用values数组可以设置多个关键帧,同时可以利用path可以进行位置或者锚点的动画操作- (void)initKeyframeAnimation { CAKeyframeAnimation animation = nil; if (self.animationIndex == 2) {//晃动 animation = [CAKeyframeAnimation animationWithKeyPath:@“transform.rotation”]; animation.duration = 0.3; animation.values = @[@(-(4) / 180.0M_PI),@((4) / 180.0M_PI),@(-(4) / 180.0*M_PI)]; animation.repeatCount=MAXFLOAT; }else {//曲线位移 animation = [CAKeyframeAnimation animationWithKeyPath:@“position”]; UIBezierPath path = [UIBezierPath bezierPath]; [path moveToPoint:_mainLayer.position]; [path addCurveToPoint:CGPointMake(300, 500) controlPoint1:CGPointMake(100, 400) controlPoint2:CGPointMake(300, 450)]; animation.path = path.CGPath; animation.duration = 1; animation.removedOnCompletion = NO; animation.fillMode = kCAFillModeForwards; } [_mainLayer addAnimation:animation forKey:@“keyFrameAnimation”];}CATransition转场动画是一种显示样式向另一种显示样式过渡的效果,系统给出的效果也很多,不过谨慎使用私有API,防止被拒的悲剧.具体有以下效果:cube 方块suckEffect 三角rippleEffect 水波抖动pageCurl 上翻页pageUnCurl 下翻页oglFlip 上下翻转cameraIrisHollowOpen 镜头快门开cameraIrisHollowClose 镜头快门开- (void)initCATransitionAnimation { CATransition transition = [CATransition animation]; transition.type = @“rippleEffect”; transition.subtype = kCATransitionFromLeft; transition.duration = 1; _mainLayer.contents = (__bridge id _Nullable)([UIImage imageNamed:@“example”].CGImage); [_mainLayer addAnimation:transition forKey:@“transtion”];}CAAnimationGroup在我们实际开发中,我们可能需要更加复杂的复合运动,那么需要给图层加多个动画,动画组也就应运而生,创建动画组也很简单,首先创建单个动画,然后将创建的多个动画添加到动画组,最后将动画组添加图层上就可以啦。- (void)initAnimationGroup { //晃动动画 CAKeyframeAnimation keyFrameAni = [CAKeyframeAnimation animationWithKeyPath:@“transform.rotation”]; keyFrameAni.values = @[@(-(4) / 180.0M_PI),@((4) / 180.0M_PI),@(-(4) / 180.0M_PI)]; //每一个动画可以单独设置时间和重复次数,在动画组的时间基础上,控制单动画的效果 keyFrameAni.duration = 0.3; keyFrameAni.repeatCount= MAXFLOAT; keyFrameAni.delegate = self;// keyFrameAni.removedOnCompletion = NO;// keyFrameAni.fillMode = kCAFillModeForwards; //位移动画 CABasicAnimation *basicAni = [CABasicAnimation animationWithKeyPath:@“position”]; //到达位置 basicAni.byValue = [NSValue valueWithCGPoint:CGPointMake(100, 100)]; // basicAni.duration = 1; basicAni.repeatCount = 1; // basicAni.removedOnCompletion = NO; basicAni.fillMode = kCAFillModeForwards; //设置代理 basicAni.delegate = self; //动画时间 basicAni.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; CAAnimationGroup *aniGroup = [CAAnimationGroup animation]; aniGroup.animations = @[keyFrameAni,basicAni]; aniGroup.autoreverses = YES; //动画的表现时间和重复次数由动画组设置的决定 aniGroup.duration = 2; aniGroup.repeatCount= 3; //使动画结束后停留在结束位置// aniGroup.autoreverses = NO;// aniGroup.removedOnCompletion = NO;// aniGroup.fillMode = kCAFillModeForwards; // [_mainLayer addAnimation:aniGroup forKey:@“groupAnimation”]; }总结Core Animation 给我们展示的只是假象;layer 的 frame、bounds、position 不会在动画完毕后发生改变;UIView 封装的动画会真实修改 view 的一些属性。本文总结的内容个人感觉还是比较浅的,但还是能满足日常开发要求的,当然一些要求比较高的,还需要大家对每个动画类进行深入的研究。最后附上本文的 Demo 地址 : animation ...

March 7, 2019 · 4 min · jiezi

GitHub 源码,Framework 框架

https://github.com/CoderLN/Ap...Apple 译文、GitHub 源码,随原作者 (大版本) 迭代注解。— 不知名开发者https://github.com/CoderLN/Fr…iOS_12 Framework 归类整理,框架各分类文件注解,并对每一类中常用的功能进行案例渐进式解析。 — 不知名开发者

March 5, 2019 · 1 min · jiezi

SDWebImage学习

SDWebImage简介SDWebImage是iOS开发中主流的图像加载库,它帮我们处理内存缓存、磁盘缓存与及图像加载的一系列操作。使用起来方便快捷,让我们更好的专注于业务逻辑的开发。组织结构SDWebImage框架组成如下:功能快速一览图:SDWebImageCompat 做机型适配的。SDWebImageManager管理缓存和下载的一个类。SDImageCache处理缓存和内存的类。SDWebImageDownloader异步下载器专用和优化图像加载。SDWebImagePrefetcher图片的预加载。源码解析SDImageCacheSDImageCache是继承自NSObject的。做了cache的一些基本配置和cache的管理。如cache的大小,cache的有效期,添加cache,删除cache等。cache的类型如下:typedef NS_ENUM(NSInteger, SDImageCacheType) { /** * The image wasn’t available the SDWebImage caches, but was downloaded from the web.(不缓存,从web加载) / SDImageCacheTypeNone, /* * The image was obtained from the disk cache.(磁盘缓存) / SDImageCacheTypeDisk, /* * The image was obtained from the memory cache.(内存缓存) */ SDImageCacheTypeMemory};内部的AutoPurgeCache是继承自NSCache的,主要用途是在收到系统的UIApplicationDidReceiveMemoryWarningNotification通知时,清理内存缓存。此外,SDWebCache的内存缓存也是使用的NSCache.设置缓存的核心方法如下:- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk { if (!image || !key) { return; } // if memory cache is enabled if (self.shouldCacheImagesInMemory) { NSUInteger cost = SDCacheCostForImage(image); [self.memCache setObject:image forKey:key cost:cost]; } if (toDisk) { dispatch_async(self.ioQueue, ^{ NSData *data = imageData; if (image && (recalculate || !data)) {#if TARGET_OS_IPHONE // We need to determine if the image is a PNG or a JPEG // PNGs are easier to detect because they have a unique signature (http://www.w3.org/TR/PNG-Structure.html) // The first eight bytes of a PNG file always contain the following (decimal) values: // 137 80 78 71 13 10 26 10 // If the imageData is nil (i.e. if trying to save a UIImage directly or the image was transformed on download) // and the image has an alpha channel, we will consider it PNG to avoid losing the transparency int alphaInfo = CGImageGetAlphaInfo(image.CGImage); BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone || alphaInfo == kCGImageAlphaNoneSkipFirst || alphaInfo == kCGImageAlphaNoneSkipLast); BOOL imageIsPng = hasAlpha; // But if we have an image data, we will look at the preffix if ([imageData length] >= [kPNGSignatureData length]) { imageIsPng = ImageDataHasPNGPreffix(imageData); } if (imageIsPng) { data = UIImagePNGRepresentation(image); } else { data = UIImageJPEGRepresentation(image, (CGFloat)1.0); }#else data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];#endif } [self storeImageDataToDisk:data forKey:key]; }); }}从代码可以看出,先设置的内存缓存,再设置的磁盘缓存。- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion { if (key == nil) { return; } if (self.shouldCacheImagesInMemory) { [self.memCache removeObjectForKey:key]; } if (fromDisk) { dispatch_async(self.ioQueue, ^{ [_fileManager removeItemAtPath:[self defaultCachePathForKey:key] error:nil]; if (completion) { dispatch_async(dispatch_get_main_queue(), ^{ completion(); }); } }); } else if (completion){ completion(); } }删除缓存的时候也是先删除内存缓存,在删除磁盘缓存。UIImageView+WebCache通过UIImageView集成SDWebImage异步下载和缓存远程图像。下载图片的核心方法如下:- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock { [self sd_cancelCurrentImageLoad]; objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC); if (!(options & SDWebImageDelayPlaceholder)) { dispatch_main_async_safe(^{ //保证是在主线程中设置图片 self.image = placeholder; }); } if (url) { // check if activityView is enabled or not if ([self showActivityIndicatorView]) { [self addActivityIndicator]; } __weak __typeof(self)wself = self; id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage image, NSError error, SDImageCacheType cacheType, BOOL finished, NSURL imageURL) { [wself removeActivityIndicator]; if (!wself) return; dispatch_main_sync_safe(^{ if (!wself) return; if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) { completedBlock(image, error, cacheType, url); return; } else if (image) { wself.image = image; [wself setNeedsLayout]; } else { if ((options & SDWebImageDelayPlaceholder)) { wself.image = placeholder; [wself setNeedsLayout]; } } if (completedBlock && finished) { completedBlock(image, error, cacheType, url); } }); }]; [self sd_setImageLoadOperation:operation forKey:@“UIImageViewImageLoad”]; } else { dispatch_main_async_safe(^{ [self removeActivityIndicator]; if (completedBlock) { NSError error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @“Trying to load a nil url”}]; completedBlock(nil, error, SDImageCacheTypeNone, url); } }); }}从代码可以看出,代码使用时runtime为分类添加属性,设置placeholder一定是在住线程中进行的,图片真正的下载操作,使用的是SDWebImageOperation。图片加载的时序图:而且这里有两个值得我们学习的宏定义:#define dispatch_main_sync_safe(block)\ if ([NSThread isMainThread]) {\ block();\ } else {\ dispatch_sync(dispatch_get_main_queue(), block);\ }保证同步线程是在主线程执行的。#define dispatch_main_async_safe(block)\ if ([NSThread isMainThread]) {\ block();\ } else {\ dispatch_async(dispatch_get_main_queue(), block);\ }保证异步线程是在主线程执行的。SDWebImageManagerSDWebImageManager是整个框架的一个核心类,它把SDImageCache和SDWebImageDownloader结合在一起,同时管理图片的下载和缓存操作。SDWebImageOptions这个options,提供了我们下载图片时的很多可选操作。示例如下:/ * By default, when a URL fail to be downloaded, the URL is blacklisted so the library won’t keep trying. * 默认情况下,当一个 URL 下载失败,该URL被列入黑名单,将不会继续尝试下载 * This flag disable this blacklisting. * 此标志取消黑名单 / SDWebImageRetryFailed = 1 << 0, / * By default, image downloads are started during UI interactions, this flags disable this feature, * 默认情况下,在 UI 交互时也会启动图像下载,此标记取消这一功能 * leading to delayed download on UIScrollView deceleration for instance. * 会延迟下载,UIScrollView停止滚动之后再继续下载 * 下载事件监听的运行循环模式是 NSDefaultRunLoopMode / SDWebImageLowPriority = 1 << 1, / * This flag disables on-disk caching * 禁用磁盘缓存 */ SDWebImageCacheMemoryOnly = 1 << 2,注:图片来源 ...

March 5, 2019 · 3 min · jiezi

Weex系列(6) —— web组件和webview

目录Weex系列(序) —— 总要知道原生的一点东东(iOS)Weex系列(序) —— 总要知道原生的一点东东(Android)Weex系列(1) —— Hello World项目Weex系列(2) —— 页面跳转和通信Weex系列(3) —— 单页面还是多页面Weex系列(4) —— 老生常谈的三端统一Weex系列(5) —— 封装原生组件和模块Weex系列(6) —— webview和web组件[Weex系列(7) —— 踩坑填坑的集锦][Weex系列(8) —— 原理流程简析]不知不觉就3月1号了,这段时间在想怎么来收尾这个系列,打算把css小结放在踩坑那一章,那一章以后估计也会不定时更新。最后一章就简单分析一下流程原理。还是言归正传吧,webview是一个基于webkit引擎、展现web页面的控件,app里面是经常用到的,weex官方提供了web组件。webview这块是比较复杂的,所以官方提供的远远不够,但是对原生又不是很熟悉,就找到组件源码,在此基础上再进行二次封装,上一章也是有很详细的提到的。进行了二次封装,我们想添加功能配置什么的就方便很多了。iOSiOS端的webview坑要少一些,几乎没怎么改过,主要就是html和原生的交互。1、可以用到URL Schemes来拦截做一些简单的跳转处理2、实在绕不过,就用到了一个比较复杂的WebViewJavascriptBridge,我用的就是谷歌搜出来第一个,参照例子加在我们自己封装的组件里面了,我这边直接就加在了viewWillAppear方法里面,同理也需要在html里面配置,最后app就能监听到html里面的点击等交互动作了。Android安卓要麻烦许多,网上大多也都是安卓的webview讲解,我也是遇到了好多坑。我把网上需要配置的基本都加上了,每个设置的说明看方法能猜出一二。1、然后就是shouldOverrideUrlLoading,页面跳转遇到的无限加载、白屏等都需要在这个方法里面做处理,由于这块涉及业务处理,也就不截出来了,我也是参照网上的方案解决的,需要耐心,多试几次,会解决的。private void initWebView(WebView wv) { WebSettings settings = wv.getSettings(); settings.setAppCacheEnabled(true); settings.setAllowFileAccess(true);//设置启用或禁止访问文件数据 settings.setDomStorageEnabled(true); settings.setLoadsImagesAutomatically(true); //适应屏幕 settings.setUseWideViewPort(true); // 设置可以支持缩放 settings.setSupportZoom(true); // 设置出现缩放工具 settings.setBuiltInZoomControls(true); //不显示webview缩放按钮 settings.setDisplayZoomControls(false); settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.SINGLE_COLUMN); settings.setLoadWithOverviewMode(true); // 设置与Js交互的权限 settings.setJavaScriptEnabled(true); // 设置允许JS弹窗 settings.setJavaScriptCanOpenWindowsAutomatically(true); //设置字体大小 settings.setTextZoom(100); wv.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) {2、接着就是安卓的上传图片文件,用到了如下的方法,最后用到的是WXWebView.mUploadCallbackAboveL 回传图片的// For Android < 3.0 public void openFileChooser(ValueCallback<Uri> valueCallback) { mUploadMessage = valueCallback; openImageChooserActivity(); } // For Android >= 3.0 public void openFileChooser(ValueCallback valueCallback, String acceptType) { mUploadMessage = valueCallback; openImageChooserActivity(); } //For Android >= 4.1 public void openFileChooser(ValueCallback<Uri> valueCallback, String acceptType, String capture) { //Log.e(TAG, “onShowFileChooser: “+acceptType); mUploadMessage = valueCallback; openImageChooserActivity(); } // For Android >= 5.0 @Override public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback, WebChromeClient.FileChooserParams fileChooserParams) { //Log.e(TAG, “onShowFileChooser: “+fileChooserParams); mUploadCallbackAboveL = filePathCallback; openImageChooserActivity(); return true; }3、还有一个点是webview里面音视频播放,退出页面,还会有声音等,可以在原生.java的onPause方法里面做处理,我记得这个没处理的时候有的安卓应用商店都是审核不过的。@Override public void onPause() { super.onPause(); if(WXWebView.mWebView != null){ WXWebView.mWebView.pauseTimers(); //这里只对页面中只有一个音频的情况做了处理,如果有多个音频需要遍历整个数组记录状态 WXWebView.mWebView.loadUrl( “javascript:audioEty = document.getElementsByTagName(‘audio’)[0]; audioEty.pause();” ); WXWebView.mWebView.loadUrl( “javascript:videoEty = document.getElementsByTagName(‘video’)[0]; videoEty.pause();” ); WXWebView.mWebView.onPause(); } }页面宽高适配最后本来是打算想把app的宽高适配等问题放在css那个小结的,但是现在归类在了踩坑里面,就把这个放在这儿讲吧。1、iOS我是在viewDidLayoutSubviews里面重新绘制了一下,在适配iPhoneX、XR、XMAX的时候会用到,安卓倒是没有怎么处理。_instance.frame = CGRectMake(safeArea.left, safeArea.top, self.view.frame.size.width-safeArea.left-safeArea.right, _weexHeight-safeArea.top);2、weex官网config这一章里面有讲到这个例子,大家也可以扫码看一下,主要就是weex.config.env.deviceWidth和weex.config.env.deviceHeight。最后这个系列就剩下两章了,下一篇也会尽快发布出来,感谢大家,如果喜欢欢迎收藏点赞啊~ ...

March 1, 2019 · 1 min · jiezi

腾讯—最新iOS面试题总结

关于面试题,可能没那么多时间来总结答案,有什么需要讨论的地方欢迎大家指教。主要记录一下准备过程,和面试的一些总结,希望能帮助到正在面试或者将要面试的同学吧。腾讯一面1、介绍一下实习的项目,任务分工,做了哪些工作?介绍实习内容2、网络相关的:项目里面使用到什么网络库,用过ASIHTTP库吗3、断点续传怎么实现?需要设置什么?4、在杭州HTTP请求服务器响应快,可能离服务器距离近,而在深圳访问就很慢很慢,会是什么原因?如果用户投诉,怎么分析这个问题?5、HTTP请求的哪些方法用过?什么时候选择get、post、put?6、TCP建立连接的过程,断开连接的过程,为什么是四次握手?7、项目里面的数据存储都用了哪些?知道iOS里面有哪些数据存储方法?什么时候该用哪些方法存储?8、MVVM如何实现绑定9、block和通知的区别,分别适用什么场景10、算法。连续问了好几个,都是数组,层层递进的,但是我忘了,只记得最后是找出数组11、中重复的数字12、进程和线程的区别13、程序在运行时操作系统除了分配内存空间还有什么14、进程间通信的方式15、如何检测应用是否卡顿16、发布出去的版本,怎么收集crash日志?不使用bugly等第三方平台或者这些第三方平台是怎么收集crash日志的?17、在block里面使用_property会造成循环引用吗?怎么解决?除了使用self->_property,可以使用valueforkey来访问吗 在block里面可以修改它的值吗setvalueforkey?可以修改它的值,可以用valueforkey来解决,显式的的使用self,block外先持有self的弱引用。二面1、OC中对象的结构2、多态3、Ping是什么协议4、知道MTU吗5、ARC和MRC的本质区别是什么?6、NSThread,GCD,NSOperation相关的。开启一条线程的方法?线程可以取消吗?7、子线程中调用connection方法,为什么不回调?因为没有加入runloop,执行完任务就销毁了,所以没有回调。8、MVC和MVVM的区别9、了解哪些设计模式10、存一个通讯录,包括增删改查,用什么数据结构11、autorelease变量什么时候释放?手动添加的是大括号结束的时候释放,系统自动释放是在12、当前runloop循环结束的时候13、那子线程中的autorelease变量什么时候释放?14、子线程里面,需要加autoreleasepool吗15、GCD和NSOperation的区别?16、项目里面遇到过死锁吗?怎么解决?数据库访问本来就是线程安全的,不会造成死锁啊。什么是死锁?17、Viewcontroller的生命周期?18、在init方法里面,设置背景颜色,会生效吗 会生效。为什么会?19、WWDC2016公布了哪些新特性?对苹果系列的最新特性有关注吗20、看过哪些源码,讲讲思路21、两个链表找第一个相同结点22、字符串旋转23、找链表的倒数第k个结点24、把一个链表比某个值大的放在左边,比它小的放在右边25、二叉树的中序遍历,非递归更多:iOS面试题(附答案)另外附上一份收集的各大厂面试题(附答案) ! 要的可加iOS高级技术群:624212887,群文件直接获取

February 28, 2019 · 1 min · jiezi

ios 自制framework遇到 _OBJC_CLASS_$_XXX, referenced from:

目录该错误解决方案合成framework的脚本错误信息Undefined symbols for architecture x86_64:"OBJC_CLASS$_XXX", referenced from:objc-class-ref in XXX.o前情提要这个问题在维护老代码,使用第三方framework的时候经常出现,网上解决方案不尽相同,但和作者遇到的情况不一样。如果你和作者原因不一样,出门左转。出现场景作者是在制作自己的framework的时候,并应用到工程中,使用真机编译时遇到这个问题。解决过程因为编译出错信息出现x86字眼,作者误认为是制作出来的framework不支持x86,多次查看了工程配置,最终通过lipo -info xxx.framework命令验证,是支持x86的。后来在网上检阅,回想起来,我并没有合成真机和模拟器的framework,最终猜想大概率是没有正确合成framework。说起没有合成framework,看了网上分享的合成步骤比较烦,然后用了错误的脚本输出为空的framework,就没管了,后来又一度怀疑配置问题,结果造成悲剧。正确姿势需要将真机和模拟器环境编译出来的framework合并,并将所属目录下的两个文件进行合并。脚本问题网上以前的脚本不适用当前xcode版本。脚本访问的真机和模拟器的路径有误,所以最终合成出来的是空的framework。正确脚本:FMK_NAME=${PROJECT_NAME}if [ “${ACTION}” = “build” ]thenINSTALL_DIR=${SRCROOT}/Products/${FMK_NAME}.frameworkDEVICE_DIR=${BUILD_ROOT}/${CONFIGURATION}-iphoneos/${FMK_NAME}.frameworkSIMULATOR_DIR=${BUILD_ROOT}/${CONFIGURATION}-iphonesimulator/${FMK_NAME}.frameworkif [ -d “${INSTALL_DIR}” ]thenrm -rf “${INSTALL_DIR}“fimkdir -p “${INSTALL_DIR}“cp -R “${DEVICE_DIR}/” “${INSTALL_DIR}/“lipo -create “${DEVICE_DIR}/${FMK_NAME}” “${SIMULATOR_DIR}/${FMK_NAME}” -output “${INSTALL_DIR}/${FMK_NAME}"#这个是合并完成后打开对应的文件夹,你就可以直接看到文件了open “${SRCROOT}/Products"最后TABAnimated原生骨架库交流群:304543771可以讨论各种技术问题,欢迎您的加入。

February 28, 2019 · 1 min · jiezi

iOS数字倍数动画

前言一个简单的利用 透明度和 缩放 实现的 数字倍数动画实现思路上代码 看比较清晰// 数字跳动动画- (void)labelDanceAnimation:(NSTimeInterval)duration { //透明度 CABasicAnimation *opacityAnimation = [CABasicAnimation animationWithKeyPath:@“opacity”]; opacityAnimation.duration = 0.4 * duration; opacityAnimation.fromValue = @0.f; opacityAnimation.toValue = @1.f; //缩放 CAKeyframeAnimation *scaleAnimation = [CAKeyframeAnimation animationWithKeyPath:@“transform.scale”]; scaleAnimation.duration = duration; scaleAnimation.values = @[@3.f, @1.f, @1.2f, @1.f]; scaleAnimation.keyTimes = @[@0.f, @0.16f, @0.28f, @0.4f]; scaleAnimation.removedOnCompletion = YES; scaleAnimation.fillMode = kCAFillModeForwards; CAAnimationGroup *animationGroup = [CAAnimationGroup animation]; animationGroup.animations = @[opacityAnimation, scaleAnimation]; animationGroup.duration = duration; animationGroup.removedOnCompletion = YES; animationGroup.fillMode = kCAFillModeForwards; [self.comboLabel.layer addAnimation:animationGroup forKey:@“kComboAnimationKey”];}利用一个透明度从 0 ~ 1之间的alpha,然后缩放 之后加到动画组实现一下就好了切记动画完成最好移除 否则可能引起动画内存问题这里设置斜体字体self.comboLabel.font = [UIFont fontWithName:@“AvenirNext-BoldItalic” size:50];看着比较明显最后按钮点击的时候调用- (IBAction)clickAction:(UIButton *)sender { self.danceCount++; [self labelDanceAnimation:0.4]; self.comboLabel.text = [NSString stringWithFormat:@"+ %tu",self.danceCount];}如果实现 dozen动画的话很简单, danceCount % 10 == 0 求模就行了.总结这个动画比较适合 有些直播场景的点击操作计数相关.iOS数字倍数动画Demo获取,可加iOS开发交流群:624212887,获取Demo,以及更多iOS技术资料 ...

February 27, 2019 · 1 min · jiezi

Objective-C 中关联引用的概念

关联引用概念利用 OC 语言的动态性,借助运行时(runtime)的功能,我们可以为已存在的实例对象增加实例变量,这个功能叫做关联引用。添加、检索和断开关联objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,id _Nullable value, objc_AssociationPolicy policy)该方法为对象 object 添加以 key 指定的地址作为关键字、以value为值的关联引用,第四个参数policy指定关联引用的存储策略。objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key) OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);返回 object 以 Key 为关键字的关联对象,如果没有关联对象,则返回 nilobjc_removeAssociatedObjects(id _Nonnull object) OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0, 2.0);断开关联存储策略typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) { OBJC_ASSOCIATION_ASSIGN = 0, /弱引用对象保存对象/ OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /强引用对象保存对象,非原子性/ OBJC_ASSOCIATION_COPY_NONATOMIC = 3, /复制一份原对象,非原子性/ OBJC_ASSOCIATION_RETAIN = 01401, /*引用对象保存对象,默认原子性,多线程安全 */ OBJC_ASSOCIATION_COPY = 01403 /复制一份原对象,默认原子性,多线程安全/};实例假设我们为 NSArray 增加了一个新的随机取元素的方法,并且取得的元素不可以连续相同,我们利用范畴(category)为 NSArray 扩展一个方法。NSArray+Random.h#import <Foundation/Foundation.h>@interface NSArray (Random)- (id)anyOne;@endNSArray+Random.m#import “NSArray+Random.h”#import <objc/runtime.h>@implementation NSArray (Random)static char prevKey;- (id)anyOne { id item; NSUInteger count = [self count]; if (count == 0) { return nil; }else if(count == 1){ return [self lastObject]; }else{ id prev = objc_getAssociatedObject(self, &prevKey);//获取关联对象所引用的值,初次使用返回 nil NSUInteger index = random()%count; item = self[index]; if (item == prev) {//索引相同情况下,取下一个元素,若该索引是数组最后一个,则取第一个值 if (++index >= count) { index = 0; } item = self[index]; } printf(“item:%s,prevItem:%s\n”,[item UTF8String],[prev UTF8String]); } objc_setAssociatedObject(self, &prevKey, item, OBJC_ASSOCIATION_RETAIN);//存储最后返回的对象 return item;}main.m#import <UIKit/UIKit.h>#import “AppDelegate.h”#import “NSArray+Random.h"int main(int argc, char * argv[]) { id arr1 = @[@“1”,@“2”,@“3”,@“4”,@“5”,@“6”,@“7”]; id arr2 = @[@“a”,@“b”,@“c”,@“d”,@“e”,@“f”,@“g”]; for (int i=0; i<15; i++) { printf(“arr1:%s,arr2:%s\n”,[[arr1 anyOne] UTF8String], [[arr2 anyOne] UTF8String]); } @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); }}运行结果item:2,prevItem:(null)item:e,prevItem:(null)arr1:2,arr2:eitem:3,prevItem:2item:f,prevItem:earr1:3,arr2:fitem:2,prevItem:3item:d,prevItem:farr1:2,arr2:ditem:4,prevItem:2item:c,prevItem:darr1:4,arr2:citem:2,prevItem:4item:d,prevItem:carr1:2,arr2:ditem:3,prevItem:2item:f,prevItem:darr1:3,arr2:fitem:7,prevItem:3item:e,prevItem:farr1:7,arr2:eitem:1,prevItem:7item:a,prevItem:earr1:1,arr2:a结语综合使用关联引用和范畴,可以大大增强 OC 编程的灵活性,但也不能滥用,会导致程序不好理解。 ...

February 27, 2019 · 1 min · jiezi

iOS | NSProxy

Objective-C作为一种动态消息型语言,其机制不同于Java ,C#等编译型语言.它将数据类型的确定等工作推迟到了运行时期来执行,并且它调用方法的方式实质是像对象发送消息,根据selector在对象的本类以及父类中的方法列表进行查找,如果都找不到就会启动消息转发机制.回到正题,这个话题我想谈下OC的单继承原则.OC确实是只能单继承的语言,但是基于运行时的机制,却有一种方法让它来实现一下"伪多继承".就是利用NSProxy这个类.NSProxy是和NSObject同级的一个类,可以说它是一个虚拟类,它只是实现了<NSObject>的协议.它的作用有点类似于一个复制类,有人曾经笑谈它是卡卡西的复制忍术,想想其实也挺贴切的,其实原理确实如此.过程:用一个继承于NSProxy的子类,在它内部实现一些方法,暴露一个公开方法transform,这个方法是使它变身的关键.然后它变身之后可以对那些对象发送消息,并且可以在内部拦截消息的内容并修改.可以说,几乎可以变身成为任何对象.直接上个代码来展示下JanProxy.h#import <Foundation/Foundation.h>@interface JanProxy : NSProxy- (void)transformObjc:(NSObject *)objc;@endJanProxy.m#import “JanProxy.h”@interface JanProxy ()@property(nonatomic,strong)NSObject *objc;@end@implementation JanProxy- (void)transformObjc:(NSObject *)objc{ //复制对象 self.objc = objc;}//2.有了方法签名之后就会调用方法实现- (void)forwardInvocation:(NSInvocation *)invocation{ if (self.objc) { //拦截方法的执行者为复制的对象 [invocation setTarget:self.objc]; if ([self.objc isKindOfClass:[NSClassFromString(@“Cat”) class]]) { //拦截参数 Argument:表示的是方法的参数 index:表示的是方法参数的下标 NSString *str = @“拦截消息”; [invocation setArgument:&str atIndex:2]; } //开始调用方法 [invocation invoke]; } }//1.查询该方法的方法签名- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{ NSMethodSignature *signature = nil; if ([self.objc methodSignatureForSelector:sel]) { signature = [self.objc methodSignatureForSelector:sel]; } else { signature = [super methodSignatureForSelector:sel]; } return signature;}@end使用方法 Dog *dog = [[Dog alloc]init]; //OC中方法的调用本质上是给这个对象发送一个消息 Cat *cat = [[Cat alloc] init]; //开始复制拦截方法 JanProxy *proxy = [JanProxy alloc]; //开始变身成猫 [proxy transformObjc:cat]; //开始调猫的方法 [proxy performSelector:@selector(eat:) withObject:@“猫发出消息”]; //开始变身成狗 [proxy transformObjc:Dog]; //开始调用学生的方法 [proxy performSelector:@selector(shut)];最后的结果发现没有,猫发出消息已经被子类的内部拦截并且做出了修改.总结OC中存在这么一个默默无闻的类NSProxy,填补了"多继承"这个空白区. ...

February 26, 2019 · 1 min · jiezi

Block中可以修改全局变量,全局静态变量,局部静态变量吗?

原文:iOS面试题大全可以.深入研究Block捕获外部变量和__block实现原理全局变量和静态全局变量的值改变,以及它们被Block捕获进去,因为是全局的,作用域很广静态变量和自动变量,被Block从外面捕获进来,成为__main_block_impl_0这个结构体的成员变量自动变量是以值传递方式传递到Block的构造函数里面去的。Block只捕获Block中会用到的变量。由于只捕获了自动变量的值,并非内存地址,所以Block内部不能改变自动变量的值。Block捕获的外部变量可以改变值的是静态变量,静态全局变量,全局变量Block就分为以下3种_NSConcreteStackBlock:只用到外部局部变量、成员属性变量,且没有强指针引用的block都是StackBlock。 StackBlock的生命周期由系统控制的,一旦返回之后,就被系统销毁了,是不持有对象的_NSConcreteStackBlock所属的变量域一旦结束,那么该Block就会被销毁。在ARC环境下,编译器会自动的判断,把Block自动的从栈copy到堆。比如当Block作为函数返回值的时候,肯定会copy到堆上_NSConcreteMallocBlock:有强指针引用或copy修饰的成员属性引用的block会被复制一份到堆中成为MallocBlock,没有强指针引用即销毁,生命周期由程序员控制,是持有对象的_NSConcreteGlobalBlock:没有用到外界变量或只用到全局变量、静态变量的block为_NSConcreteGlobalBlock,生命周期从创建到应用程序结束,也不持有对象ARC环境下,一旦Block赋值就会触发copy,__block就会copy到堆上,Block也是__NSMallocBlock。ARC环境下也是存在__NSStackBlock的时候,这种情况下,__block就在栈上ARC下,Block中引用id类型的数据有没有__block都一样都是retain,而对于基础变量而言,没有的话无法修改变量值,有的话就是修改其结构体令其内部的forwarding指针指向拷贝后的地址达到值的修改

February 25, 2019 · 1 min · jiezi

如何优化 App 的启动耗时?

原文:iOS面试题大全iOS 的 App 启动主要分为以下步骤:打开 App,系统内核进行初始化跳转到 dyld 执行。这个过程包括这些步骤:1)分配虚拟内存空间;2)fork 进程;3)加载 MachO (自身所有的可执行 MachO 文件的集合)到进程空间;4)加载动态链接器 dyld 并将控制权交给 dyld 处理。在这个过程中内核会产生 ASLR(Address space layout randomization) 随机数值,这个值用于加载的 MachO 起始地址在内存中的偏移,随机的地址可防止 MachO 代码扫描并被 hack,提升安全性。通过 ASLR 虽然可随机化各内存区基地址,但无法将程序内的代码段和数据段随机化,如果绕过(bypass) ASLR 依然可进行篡改,就需要结合 PIE(Position Independent Executable) 共同使用。与之相似的还有 PIC(Position Independent Code),位置无关代码,作用于共享库代码。PIE/PIC 技术需要在编译阶段开启。顾名思义,PIC 可将程序代码装载到任意地址,这样就内部的指针不能靠固定的绝对地址访问,而通过相对地址指令如 adrp 来获取代码和数据。进入 dyld 动态链接器,它负责将一个 App 处理为一个可运行的状态,包含:加载 MachO 的依赖库(这些依赖库也是 MachO 格式的文件)。dyld 从可执行 MachO 文件的依赖开始, 递归加载所有依赖的动态库。 动态库包括:iOS 中用到的所有系统动态库:加载 OC runtime 方法的 libobjc,系统级别的 libSystem(例如 libdispatch(GCD) 和 libsystem_blocks(Block));其他 App 自己的动态库。根据 Apple 的描述,大部分 App 所加载的库在 100~400 个。不过 iOS 系统库已经被特殊优化过,如提前加入共享缓存,提前做好地址修正等。Fix-ups(地址修正),包括 rebasing 和 binding 等。ASLR + PIE 技术增强了程序的安全性,使得依赖固定地址进行攻击的方法失效,但也增加了程序自身的复杂度,MachO 文件的 rebase 和 bind info 等部分以及启动时的 fix-ups 地址修正阶段就是配合它而产生的。ObjC 环境配置。经过了 MachO 程序和依赖库的加载以及地址修正之后,dyld 所做的大部分事情已经完成了。在这一阶段,dyld 开始对主程序的依赖库进行初始化工作,而初始化的执行部分会回调到依赖库内部执行,如 ObjC 的运行时环境所在的 libobjc.A.dylib 以及 libdispatch.dylib 等。ObjC Setup 的过程,主要是对 ObjC 数据进行关联注册:1)dyld 将主程序 MachO 基址指针和包含的 ObjC 相关类信息传递到 libobjc;2)ObjC Runtime 从 __DATA 段中获取 ObjC 类信息,由于 ObjC 是动态语言,可以通过类名获取其实例,所以 Runtime 维护了一个映射所有类的全局类名表。当加载的数据包含了类的定义,类的名字就需要注册到全局表中;3)获取 protocol、category 等类相关属性并与对应类进行关联;4)ObjC 的调用都是基于 selector 的,所以需要对 selector 全局唯一性进行处理。以上步骤由 dyld 启动 libSystem.dylib 统一对基础库进行调用执行,这里面就包含了 libobjc 的 Runtime,同时 Runtime 会在 dyld 绑定回调,当 dyld 处理完相关数据后就会调用 ObjC Runtime 执行 Setup 工作。执行各模块初始化器。从这一步就开始接近上(业务)层:1)通过 ObjC Runtime 在 dyld 注册的通知,当 MachO 镜像准备完毕后,dyld 会回调到 ObjC 中执行 +load() 方法,包括以下步骤:a)获取所有 non-lazy class 列表;b)按继承以及 category 的顺序将类排入待加载列表;c)对待加载列表中的类进行方法判断并调用 +load() 方法。2)执行 C/C++ 初始化构造器,如通过 attribute((constructor)) 注解的函数。3)如果包含 C++,则 dyld 同样会回调到 libc++ 库中对全局静态变量、隐式初始化等进行调用。查找并跳转到 main() 函数入口。到了最后,dyld 回到 Load command,找到 LC_MAIN,拿到 entryoff 再加上 MachO 在内存的加载首地址(首地址就是内核传来的 slide 偏移)就得到了 main() 的入口地址,从而进入我们显式的程序逻辑。进入 main() -> UIApplicationMain -> 初始化回调 -> 显示UI。iOS 的 App 启动时长大概可以这样计算:t(App 总启动时间) = t1(main 调用之前的加载时间) + t2(main 调用之后的加载时间)。t1 = 系统 dylib(动态链接库)和自身 App 可执行文件的加载。t2 = main 方法执行之后到 AppDelegate 类中的 application:didFinishLaunchingWithOptions:方法执行结束前这段时间,主要是构建第一个界面,并完成渲染展示。在 t1 阶段加快 App 启动的建议:尽量使用静态库,减少动态库的使用,动态链接比较耗时。如果要用动态库,尽量将多个 dylib 动态库合并成一个。尽量避免对系统库使用 optional linking,如果 App 用到的系统库在你所有支持的系统版本上都有,就设置为 required,因为 optional 会有些额外的检查。减少 Objective-C Class、Selector、Category 的数量。可以合并或者删减一些 OC 类。删减一些无用的静态变量,删减没有被调用到或者已经废弃的方法。将不必须在 +load 中做的事情尽量挪到 +initialize 中,+initialize 是在第一次初始化这个类之前被调用,+load 在加载类的时候就被调用。尽量将 +load 里的代码延后调用。尽量不要用 C++ 虚函数,创建虚函数表有开销。不要使用 atribute((constructor)) 将方法显式标记为初始化器,而是让初始化方法调用时才执行。比如使用 dispatch_once(),pthread_once() 或 std::once()。在初始化方法中不调用 dlopen(),dlopen() 有性能和死锁的可能性。在初始化方法中不创建线程。在 t2 阶段加快 App 启动的建议:尽量不要使用 xib/storyboard,而是用纯代码作为首页 UI。如果要用 xib/storyboard,不要在 xib/storyboard 中存放太多的视图。对 application:didFinishLaunchingWithOptions: 里的任务尽量延迟加载或懒加载。不要在 NSUserDefaults 中存放太多的数据,NSUserDefaults 是一个 plist 文件,plist 文件被反序列化一次。避免在启动时打印过多的 log。少用 NSLog,因为每一次 NSLog 的调用都会创建一个新的 NSCalendar 实例。每一段 SQLite 语句都是一个段被编译的程序,调用 sqlite3_prepare 将编译 SQLite 查询到字节码,使用 sqlite_bind_int 绑定参数到 SQLite 语句。为了防止使用 GCD 创建过多的线程,解决方法是创建串行队列, 或者使用带有最大并发数限制的 NSOperationQueue。线程安全:UIKit只能在主线程执行,除了 UIGraphics、UIBezierPath 之外,UIImage、CG、CA、Foundation 都不能从两个线程同时访问。不要在主线程执行磁盘、网络、Lock 或者 dispatch_sync、发送消息给其他线程等操作。 ...

February 21, 2019 · 2 min · jiezi

黑魔法(method-swizzling)解决第三方库引发的问题

需求最近做一个项目中,有个需求,所有网络请求,都不显示 NetworkActvityIndicator(也就是状态栏里旋转的小圈圈).解决过程1:全局搜索 NetworkIndicator 关键字, 把所有涉及 NetworkIndicator 的代码去除,比如 [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES]; 。测试并发现新问题所有界面都不再显示NetworkActvityIndicator了,唯独一个播放视频的界面依然显示。猜想: 第三方库引发的问题无论是哪些第三方库,正常情况都会通过 setNetworkActivityIndicatorVisible 来 显示状态栏小圈圈。验证过程1通过继承 UIApplication 来重写了 setNetworkActivityIndicatorVisible 方法。(如何继承UIApplication,请看这里)并把断点打在这个方法体内。测试了正常调用 [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES]; 是会触发断点的。但是唯独那个视频界面,没有触发该断点的情况下,正常显示小圈圈。验证过程2通过 KVO 监听 UIApplication 的 networkActivityIndicatorVisible 属性,结果还是和 验证过程1 的情况一样。正常调用 [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES]; 是会触发监听, 唯独那个视频界面,没有触发监听的情况下,正常显示小圈圈。所以, 视频界面里显示的小圈圈,肯定不是通过常规调用 setNetworkActivityIndicatorVisible 方法显示出来的。更新猜想: 第三方库引发的问题,并且不是通过常规方法调用验证过程3显示小圈圈的情况下,分析了该界面的视图层级,发现在 statusBar 上,有 类型为UIActivityIndicatorView的视图存在(并且怪异的存在了两个)。那正常情况下,通过 [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:YES]; 显示小圈圈时,视图层级是如何的呢? 通过分析验证, 也是一样的。 层级都是 UIStatusBarView -> UIStatusBarForegroundView -> UIStatusBarActivityItemView -> UIActivityIndicatorView想到解决方案:既然小圈圈都是 UIActivityIndicatorView 类型的视图,而 UIActivityIndicatorView 开始动画常规都是调用 startAnimation 方法。那何不使用黑魔法(method swizzling)来重写它的 startAnimation 方法, 判断它的superView是否为 “UIStatusBarActivityItemView”类型,如果是,则直接跳出。否则,执行原有的 startAnimation方法。Talk is cheap. Show me the code.以下是 .m 文件的代码@implementation UIActivityIndicatorView (HideNetworkActivityIndicator)+ (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Class class = [self class]; SEL originalSelector = @selector(startAnimating); SEL swizzledSelector = @selector(xxx_startAnimating); Method originalMethod = class_getInstanceMethod(class, originalSelector); Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); // When swizzling a class method, use the following: // Class class = object_getClass((id)self); // … // Method originalMethod = class_getClassMethod(class, originalSelector); // Method swizzledMethod = class_getClassMethod(class, swizzledSelector); BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) { class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); } });}#pragma mark - Method Swizzling- (void)xxx_startAnimating{ if (self.superview != nil && [NSStringFromClass([self.superview class]) isEqualToString: @“UIStatusBarActivityItemView”]) { NSLog(@“黑魔法禁止状态栏的loading显示: %@”, self); } else { [self xxx_startAnimating]; }}@end成功了!!!(在xxx_startAnimation方法体内打断点,程序进入视频播放界面,触发断点,看调用栈,果然是第三方库引发的问题。)参考资料:https://nshipster.cn/method-s… ...

February 21, 2019 · 1 min · jiezi

Objective-C-如何选择@property-和-Instance-Variable(iVar)

简述在Objective-C的类中,有两种方式可以声明变量@property: // 在 .h文件@interface Hello : NSObject@property (nonatomic, strong) UIView *view;@end或者 // 在 .m文件@interface Hello()@property (nonatomic, strong) UIView *view;@end实例变量 Instance Variable (iVar)://在 .h文件里 @interface Hello () {UIView *_view;}@end或者//在 .m文件 的interface里@interface Hello () {UIView *_view;}@end或者//在 .m文件 的implement里@implement Hello {UIView *_view;}@end什么时候用@property, 什么时候用 iVar呢?区别可见性如果想要定义私有(private)变量, 可以考虑使用iVar; 定义公开(public)变量,则使用@property;iVar虽然可以用 @private, @protected 和 @public 修饰, 但只会对影响到子类的可见性.也就是说,即使你用 @public修饰iVar, 其它类也是无法访问到该变量的.属性(attributes)@property 可以使用strong, weak, nonatomic, readonly 等属性进行修饰.iVar默认都是strong.书写习惯通常, iVar名称使用下划线开头, 如 _view, _height, _width.但这并非强制要求.getter/setter编译器自动为@property生成访问器(getter/setter).效率iVar 运行效率更高.结论如果只是在类的内部访问, 既不需要weak、retain等修饰词,也不需要编译器自动生成getter/setter方法, 则使用 variable就可以.否则就使用 @property.参考资料:http://hongchaozhang.github.i…https://www.quora.com/Objecti…

February 21, 2019 · 1 min · jiezi

iOS App卡顿监控(Freezing/Lag)

iOS App卡顿监控(Freezing/Lag)笔记(写在前面):关于应用的性能监控,需要从多方面进行综合考虑,此处仅从其中一个方面,进行学习研究。如何判断主线程卡顿:监测NSRunLoop耗时情况。NSRunLoop的调用主要在kCFRunLoopBeforeSources和kCFRunLoopBeforeWaiting之间,以及kCFRunLoopAfterWaiting之后。因此,若是发现这个两个时间内耗时过长,就可以判定此时主线程出现卡顿情况。一、监控NSRunLoop状态变化使用CFRunLoopObserverRef,实时获得这些状态值的变化,如下:/// RunLoop状态观察回调static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void info){ <#MyClass#> object = (__bridge <#MyClass#>)info; // 记录状态值 object->activity = activity;}/// 注册RunLoop状态观察- (void)registerRunLoopObserver { CFRunLoopObserverContext context = {0,(__bridge void)self,NULL,NULL}; CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &runLoopObserverCallBack, &context); CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);}二、RunLoop耗时计算另外开启一个线程,实时计算两个状态区域之间的耗时,是否达到阈值。dispatch_semaphore_t让子线程更及时地获知主线程NSRunLoop状态变化卡顿覆盖范围:多次连续小卡顿、单次长时间卡顿添加计算逻辑,如下:/// RunLoop状态观察回调static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void info){ <#MyClass#> object = (__bridge <#MyClass#>)info; // 记录状态值 object->activity = activity; // 发送信号 dispatch_semaphore_t semaphore = object->semaphore; dispatch_semaphore_signal(semaphore);}/// 注册RunLoop状态观察,并计算是否卡顿- (void) registerRunLoopObserver { CFRunLoopObserverContext context = {0,(__bridge void)self,NULL,NULL}; CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &runLoopObserverCallBack, &context); CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes); // 创建信号 semaphore = dispatch_semaphore_create(0); // 在子线程监控时长 dispatch_async(dispatch_get_global_queue(0, 0), ^{ while (YES) { // 假定连续5次超时50ms认为卡顿(当然也包含了单次超时250ms) long st = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC)); if (st != 0) { if (activity==kCFRunLoopBeforeSources || activity==kCFRunLoopAfterWaiting) { if (++timeoutCount < 5) { continue; } // 发现卡顿 NSLog(@“卡、卡、卡、顿、顿、了”); } } timeoutCount = 0; } });}三、记录卡顿的函数调用目击卡顿现场,记录此时的调用函数信息,作为卡顿证据。此处,使用第三方Crash收集组件PLCrashReporter,它不仅可以收集Crash信息,也可用于实时获取各线程的调用堆栈,使用示例如下:PLCrashReporterConfig *config = [[PLCrashReporterConfig alloc] initWithSignalHandlerType:PLCrashReporterSignalHandlerTypeBSD symbolicationStrategy:PLCrashReporterSymbolicationStrategyAll];PLCrashReporter *crashReporter = [[PLCrashReporter alloc] initWithConfiguration:config];NSData *data = [crashReporter generateLiveReport];PLCrashReport *reporter = [[PLCrashReport alloc] initWithData:data error:NULL];NSString *report = [PLCrashReportTextFormatter stringValueForCrashReport:reporter withTextFormat:PLCrashReportTextFormatiOS];NSLog(@"————\n%@\n————", report);特别注意:PLCrashReporter虽然能提供较为准确的堆栈信息,用于定位问题,特别是使用符号化策略PLCrashReporterSymbolicationStrategyAll时,能够对堆栈信息进行符号化,但会消耗大量资源,需要占用较多时间,导致卡死现象(自测时,耗时超过7s,层多次到10s以上)。不使用符号化策略PLCrashReporterSymbolicationStrategyNone,测试时,平均耗时也接近3s。因此,加入该信息采集,需要特别注意,建议仅在开发调试阶段使用。为了投入线上使用,还需要再想想如何解决该问题。四、上报服务器检测到卡顿,获取到调用堆栈信息,客户端再根据实际情况进行一定程度的过滤处理,将有价值的信息上报服务器。后续对服务器收集到的数据进行分析,定位需要优化的功能逻辑。 ...

February 5, 2019 · 1 min · jiezi

过年了,给亲朋好友解释「啥是程序员」

前言一年一度的春节又到了,作为程序员的你是怎么告诉亲朋好友自己日常到底是做什么工作的呢?程序员是一个新兴的职业,我在上大学以前不了解程序员到底是做什么的,对编程语言也一窍不通,也不关心玩过的「游戏」和天天逛的「网站」究竟是怎么来的。当年加了计算机科学与技术专业的新生群,听着师兄和师姐们在聊天,其中就聊到了「程序员」这份职业。当时脑海里第一反应想象的竟是「复印机、打印机等办公」乱七八糟的东西外行人看程序员有的「初高中」同学知道我读了计算机专业,于是过来问我:计算机专业应该会组装电脑的吧,我想组装一台新电脑,能不能帮帮忙给点意见,比如电脑配置清单什么的。在我大学期间我爸希望我多考一些证书,比如有关「电气和会计」之类证书。他认为:只要证书多,那找工作肯定不成问题,工资也会比没有证书的人要高。临毕业了,跟同学出去吃宵夜,我当时正忙着找工作,同学也多问了一句:“你在找什么工作啊”。于是我说:”就IT呗,程序员了解不“。他说:”不懂啊,你会不会盗QQ、做游戏外挂那些东西的啊?“。前几天跟亲戚去吃饭,得知我年后要离开广州去杭州做软件相关的工作,几杯酒过后,深长意味告诉我:“还是做「行政」相关的工作有前途,走那么远做软件有啥用啊,我来给你介绍去xxx工作”。我这里想表达的是:老一辈的人大多不太了解互联网行业,认为行政、银行、老师、医生这些行业才是“正道”。我对这些行业都持有敬畏之心,并没有讽刺的意思。程序员喜欢自嘲外行人对程序员的误解,也有可能只是看到网上有很多关于程序员的言论,真以为「大多数」程序员都那样。比如说程序员都爱穿格子衫:程序员没有女朋友:程序员人傻话少死得快:其实很多的言论都是程序员们在闲暇之余的时候「自嘲」罢了。大多数程序员跟其他行业的人都一样,并没有像网上说得那么玄乎。程序员在网上一般称自己为「码农」(编码的农民)程序员加班暂且不管关于程序员其他的言论,但对于互联网行业来说,加班的确是「普遍」存在的现象,就在前阵子,「有赞」在年会上公开宣布996工作制。有人可能不知道996是什么意思,这里我解释一下:从早上9点工作至晚上9点,一周上六天班。简称996我在秋招跑了几个广州游戏公司的宣讲会,大多在宣讲会上明确公司需要「加班」。「苏宁」之前也闹出一事「华为」的狼性文化也是家喻户晓的啥是编程语言基本大家能想到程序员能做的事,而程序员们几乎都不会每个程序员都会有一门自己拿手的编程语言,有的时候一门编程语言可以对应不同的「岗位」。(比如说,Java程序员、PHP程序员)程序员每天都是跟「计算机」打交道,让计算机干什么,计算机就会去干什么,很是听话那计算机咋能听懂程序员让它去干啥呀。有点计算机基础的同学可能会知道,计算机只认「二进制」「二进制」:逢二进一。「十进制」:逢十进一看到这里,大家可能都看不懂了。没事,我们继续往下看程序员也是人,人与人的交流,「常用的语言」有阿拉伯语、汉语、英语、法语、俄语和西班牙语等,而不是啥「二进制」的东西。为了方便,聪明的「程序员」企图让计算机去读懂「自然语言」。(能够让计算机去读懂阿拉伯语、汉语、英语、法语、俄语和西班牙语等这些语言)所以程序员们就搞出了一个「编译器」,编译器说白了就是能将这些「自然语言」翻译成二进制,让计算机读得懂!在计算机世界里,对任何事物都不含糊,说一就是一,说二就是二。想要让「自然语言」能够准确地翻译成二进制,就必须定下相应的「规则」。而使用不同的「规则」,就出现了不同的编程语言。因为大多数编程语言都是「外国人」发明的,并且由于英语的「广泛性」和「易用性」。所以,绝大多数的编程语言都由「英文」组成。(以下就是各种常见的编程语言,由于「规则」不同,所以看起来都不一样。但所做的事都是相同的)print_r(‘点个好看、转发吧!’);var_dump(‘点个好看、转发吧!’);NSLog(@“点个好看、转发吧!”);System.out.println(“点个好看、转发吧!”);console.log(“点个好看、转发吧!”);print(“点个好看、转发吧!”);printf(“点个好看、转发吧!”);cout << “点个好看、转发吧!” << endl;Console.WriteLine(“点个好看、转发吧!”);fmt.Println(“点个好看、转发吧!”);Response.Write(“点个好看、转发吧!”);alert(“点个好看、转发吧!")echo “点个好看、转发吧!“解释程序员干啥活别人问到我是做什么的,我往往会说“你平时用过的「微信」,「百度」这些软件啊,网站啊就是我们程序员做的”。(一般到这里就不会继续往下问细节了,但还是遇到过继续问究竟是怎么做的)接下来,为了继续演示,我们可以掏出手机,打开微信:“我们都是玩微信的人,要进入微信先得有个账户,所以要先注册““注册实际上就是把你的账号和密码「保存」起来”。“如果你注册成功了,那小本本「数据库」就有你的账号和密码了。等你登录的时候,就去小本本「数据库」查一下你输入的账号和密码是否匹配”“看到这里,有没有觉得「程序员」真是容易当,不就把输入的数据记录下小本本,等用到的时候再查一下而已嘛。其实不然,就好比微信的注册页面,拿IOS系统来说,从IOS 6 到现在的IOS 12已经有6个版本了。Iphone手机屏幕的尺寸也一直在变,从3.5英寸到现在的6.5英寸。微信团队需要对不同的「版本」,不同的「屏幕大小」进行兼容,让iPhone4s还是iPhoneXS Max都能用上微信。(起码在不同的手机上,看到的注册功能都是完整的,而不是东一块西一块不整齐的)”“还比如说,小本本「数据库」记下的密码不能是你输入的「原始密码」。举个例子:某一天,一个在微信团队的小伙子拿到了存放账号和密码的小本本「数据库」,把小本本「数据库」的内容给公开了。(或者说一个黑客把微信给攻破了,然后公开小本本「数据库」),那岂不是谁都可以上我的微信号了?”“程序员们当然可以考虑到这点,他们会这样做,将你输入的密码进行「加密」存到小本本「数据库」中。无论是谁,拿到加密后的密码,都不能「反推」你原始输入的密码。”“所以,每次比对的都是加密后的密码”“当然啦,程序员考虑的远远不止这些。不管怎么样,每个程序员都希望自己写的东西「简单易用,受人喜欢」”最后文章首发公众号Java3y:过年了,给亲朋好友解释「啥是程序员」乐于输出干货的Java技术公众号:Java3y。公众号内有200多篇原创技术文章、海量视频资源、精美脑图,不妨来关注一下!觉得我的文章写得不错,不妨点一下赞!

February 2, 2019 · 1 min · jiezi

阿里云Kubernetes容器服务上体验Knative

概述Knative Serving是一种可缩放至零、请求驱动的计算运行环境,构建在 Kubernetes 和 Istio 之上,支持为 serverless 应用、函数提供部署与服务。Knative Serving的目标是为Kubernetes提供扩展功能,用于部署和运行无服务器工作负载。下面讲述一下在阿里云Kubernetes容器服务基础之上,如何快速搭建一套Knative Serving环境并进行自动扩容的体验。搭建Knative Serving1.准备Kubernetes环境阿里云容器服务Kubernetes 1.11.5目前已经上线,可以通过容器服务管理控制台非常方便地快速创建 Kubernetes 集群。具体过程可以参考创建Kubernetes集群。2.部署IstioKnative serving运行需要基于Istio,目前阿里云容器服务Kubernetes已提供了快速一键部署的方式来安装配置Istio。具体过程可以参考部署Istio。登录 容器服务管理控制台,单击左侧导航栏中的集群,进入集群列表页面。选择所需的集群并单击操作列更多 > 部署Istio。根据需要进行配置,然后点击部署按钮。稍等几十秒钟之后,Istio环境就可以部署完毕,可以通过查看Pod运行状态进行确认,如下所示。3.部署Istio IngressGateway登录容器服务管理控制台,点击左侧的应用目录,在右侧选中ack-istio-ingressgateway,如下:点击参数, 可以通过修改参数配置进行定制化,默认参数提供了Istio IngressGateway的配置项,然后点击创建按钮。查看命令空间 istio-system 下的Pod列表,确认运行状态,如下所示。4.部署Knative CRD登录容器服务管理控制台,点击左侧的应用目录,在右侧选中ack-knative-init,如下:点击创建按钮安装Knative初始化所需的内容,包括安装CRD等。5.部署Knative Serving登录容器服务管理控制台,点击左侧的应用目录,在右侧选中ack-knative-serving,如下:点击参数, 可以通过修改参数配置进行定制化,默认参数提供了使用Istio IngressGateway的配置项,然后点击创建按钮。至此,安装Knative Serving所需的4个Helm chart都已经安装完毕,如下所示:体验Knative安装autoscale示例通过执行如下命令部署示例自动伸缩应用程序的 Knative Service:kubectl create -f autoscale.yaml其中autoscale.yaml文件内容如下所示:apiVersion: serving.knative.dev/v1alpha1kind: Servicemetadata: name: autoscale-go namespace: defaultspec: runLatest:configuration: revisionTemplate: metadata: annotations: # Target 10 in-flight-requests per pod. autoscaling.knative.dev/target: “10” autoscaling.knative.dev/class: kpa.autoscaling.knative.dev spec: container: image: registry.cn-beijing.aliyuncs.com/wangxining/autoscale-go:0.1访问autoscale示例找到入口主机名和IP并导出为环境变量:export IP_ADDRESS=kubectl get svc istio-ingressgateway –namespace istio-system –output jsonpath="{.status.loadBalancer.ingress[*].ip}“向自动伸缩应用程序发出请求,并检查消耗资源情况:curl –header “Host: autoscale-go.default.{domain.name}” “http://${IP_ADDRESS?}?sleep=100&prime=10000&bloat=5"注意,替换你的域名后缀替换{domain.name}。默认示例中为 aliyun.com。curl –header “Host: autoscale-go.default.aliyun.com” “http://${IP_ADDRESS?}?sleep=100&prime=10000&bloat=5"Allocated 5 Mb of memory.The largest prime less than 10000 is 9973.Slept for 100.16 milliseconds.通过以下命令安装负载生成器:go get -u github.com/rakyll/hey维持50个并发请求,发送30秒的流量:hey -z 30s -c 50 \ -host “autoscale-go.default.aliyun.com” \ “http://${IP_ADDRESS?}?sleep=100&prime=10000&bloat=5” \ && kubectl get pods可以看到运行30秒的流量请求状态,Knative服务随着请求数量的增大也自动扩容:Summary: Total: 30.1126 secs Slowest: 2.8528 secs Fastest: 0.1066 secs Average: 0.1216 secs Requests/sec: 410.3270 Total data: 1235134 bytes Size/request: 99 bytesResponse time histogram: 0.107 [1] | 0.381 [12305] |■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■ 0.656 [0] | 0.930 [0] | 1.205 [0] | 1.480 [0] | 1.754 [0] | 2.029 [0] | 2.304 [0] | 2.578 [27] | 2.853 [23] |Latency distribution: 10% in 0.1089 secs 25% in 0.1096 secs 50% in 0.1107 secs 75% in 0.1122 secs 90% in 0.1148 secs 95% in 0.1178 secs 99% in 0.1318 secsDetails (average, fastest, slowest): DNS+dialup: 0.0001 secs, 0.1066 secs, 2.8528 secs DNS-lookup: 0.0000 secs, 0.0000 secs, 0.0000 secs req write: 0.0000 secs, 0.0000 secs, 0.0023 secs resp wait: 0.1214 secs, 0.1065 secs, 2.8356 secs resp read: 0.0001 secs, 0.0000 secs, 0.0012 secsStatus code distribution: [200] 12356 responsesNAME READY STATUS RESTARTS AGEautoscale-go-00001-deployment-5fb497488b-2r76v 2/2 Running 0 29sautoscale-go-00001-deployment-5fb497488b-6bshv 2/2 Running 0 2mautoscale-go-00001-deployment-5fb497488b-fb2vb 2/2 Running 0 29sautoscale-go-00001-deployment-5fb497488b-kbmmk 2/2 Running 0 29sautoscale-go-00001-deployment-5fb497488b-l4j9q 1/2 Terminating 0 4mautoscale-go-00001-deployment-5fb497488b-xfv8v 2/2 Running 0 29s总结在阿里云Kubernetes容器服务基础之上,可以快速搭建一套Knative Serving环境并进行自动扩容的体验。欢迎大家使用阿里云上的容器服务,快速搭建Knative环境,比较简单地集成到自己项目开发中。本文作者:osswangxining阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

January 31, 2019 · 2 min · jiezi

OC基础-单例的实现 & 提醒自己注意多线程问题

做客户端开发应当时刻考虑多线程问题。我最初是做前端开发的,在这方面考虑得往往不够。谨记。单例的常见写法单例的常见写法其实就两种1. 依赖锁+ (id)sharedInstance { static testClass *sharedInstance = nil; @synchronized(self) { if (!sharedInstance) { sharedInstance = [[self alloc] init]; } } return sharedInstance; }2. 依赖dispatch_once+ (id)sharedInstance { static testClass *sharedInstance = nil; static dispatch_once_t once; dispatch_once(&once, ^{ sharedInstance = [[self alloc] init]; }); return sharedInstance; }dispatch_once的写法更推荐一些。一方面是性能上好一点,另一方面是语义上更直观。once,执行一次嘛。不管是用锁还是dispatch_once,本质上都是为了避免单例创建过程出现线程安全问题。更进一步,我们经常会有懒加载某些属性的写法:- (id<InterfaceEngineA>)engineA{ if (_engineA == nil) { _engineA = [EngineA new]; } return _engineA;}其实跟单例的实现是类似的,这种时候要格外注意线程安全问题。如果存在多线程场景,一定要做好保护- (id<InterfaceEngineA>)engineA{ @synchronized(self) { if (_engineA == nil) { _engineA = [EngineA new]; } } return _engineA;}一些废话多线程问题的表现可能是各种各样难以预料的。这里我遇到的是,_engineA在多线程场景下小概率被重复创建,其实例1在init时注册了网络层命令字cmd1的回包,而这个网络层框架的实现是,只接受第一个注册这一命令字的对象。导致实例2注册失败。后面调用实例2发送请求,回包都被实例1接收了。从日志上看,一切都挺正常的。但是下次取数据就是取不到。这个bug第一次提过来的时候,没分析出根本原因,只在表面上做了保护。结果第二次提过来才真正改掉。丢人呐。还是要好好学习才是。 ...

January 24, 2019 · 1 min · jiezi

iOS播放器、Flutter高仿书旗小说、卡片动画、二维码扫码、菜单弹窗效果等源码

iOS精选源码全网最详细购物车强势来袭一款优雅易用的微型菜单弹窗(类似QQ和微信右上角弹窗)swift, UITableView的动态拖动重排CCPCellDragger高仿书旗小说 Flutter版,支持iOS、AndroidNKAVPlayer 轻量级视频播放、控制,iOS AVPlayerRN 仿微信朋友圈SwiftScan 二维码/条形码扫描、生成,仿微信、支付宝Mac上解压Assets.car文件的小工具cartooltispr-card-stack - swift 卡片风格动画切换组件及完整交互示例。Flutter仿写单读App,同时支持iOS和AndroidiOS优质博客CAEmitterLayer 粒子动画最近有点时间,研究了一下CAEmitterLayer粒子动画效果,分享出来,以备自己以后使用,先看一下基本的效果吧:首先,说一下CALayer 经常使用到的一些类然后说一下管理CALayer内容的几个函数addSublayer: 添加子图层removeFromSuperlayer将自己从… 阅读原文线程安全: 互斥锁和自旋锁(10种)无并发,不编程.提到多线程就很难绕开锁.iOS开发中较常见的两类锁:1. 互斥锁: 同一时刻只能有一个线程获得互斥锁,其余线程处于挂起状态.2. 自旋锁: 当某个线程获得自旋锁后,别的线程会一直做循环,尝试加锁,当超过了限定的次数仍然没有成功获得锁时,线程也会被挂起.自旋锁较适用于锁的持有者保存时间较短的情况下,实际使… 阅读原文深入浅出iOS编译前言两年前曾经写过一篇关于编译的文章《iOS编译过程的原理和应用》,这篇文章介绍了iOS编译相关基础知识和简单应用,但也很有多问题都没有解释清楚:Clang和LLVM究竟是什么源文件到机器码的细节Linker做了哪些工作编译顺序如何确定头文件是什么?XCode是如何找到头文件的?Clang Module签名是什么?为什… 阅读原文iOS | 多态的实际运用一句话概括多态:子类重写父类的方法,父类指针指向子类。或许你对多态的概念比较模糊,但是很可能你已经在不经意间运用了多态。比如说:有一个tableView,它有多种cell,cell的UI差异较大,但是它们的model类型又都是一样的。由于这几种cell都具有相同类型的model,那么你肯定会先建一个基类cell,如:@… 阅读原文iOS开发之多种Cell高度自适应实现方案的UI流畅度分析本篇博客的主题是关于UI操作流畅度优化的一篇博客,我们以TableView中填充多个根据内容自适应高度的Cell来作为本篇博客的使用场景。当然Cell高度的自适应网上的解决方案是铺天盖地呢,今天我们的重点不是如何讨论Cell高度的自适应,而是给出几种Cell高度自适应的解决方案,然后对比起UI流畅度,从而得出一些UI优… 阅读原文更多源码更多博文

January 23, 2019 · 1 min · jiezi

如何将OGV格式的视频转换成MP4格式

在日常生活中,看视频是必定会做的事情,但有时因为无法播放视频,有些可能是视频文件损坏,但一般都是视频格式比较特殊,播放器不支持某种视频格式的编码所以才无法播放。比如非常少见的OGV格式。OGV视频HTML5中的一种OGG容器视频格式,且不受专利保护,而且可以纳入音效、视讯、文字字幕的处理。那么,如何将视频OGV转换MP4格式呢?以下是使用两种方法将OGV转换MP4格式。一、在线OGV转换MP41、首先点击网站导航栏中的视频转换,然后点击视频转换左边的导航栏进入OGV转换MP4在线页面中。2、进入之后,点击选择文件将电脑上的OGV视频上传到网站服务器中进行转换,选择输出格式(默认选择MP4输出)为MP4格式。3、OGV文件添加完成后点击开始转换,然后等待网站转换成功,点击立即下载就下载到电脑上存储,如果需要在手机移动端播放,可以扫描二维码下载到手机上观看。二、迅捷视频转换器转换1、首先下载迅捷视频转换器,下载3MB的迅捷视频转换器安装程序到电脑上,然后双击或者右键以管理员身份运行,选择安装位置,然后点击立即安装,等待安装程序释放文件即可。2、视频转换器安装完成后,双击打开软件进入其中,然后全选OGV视频文件将其拖拽到入软件中,也可使用添加文件或者添加文件夹功能导入待转换的OGV视频文件。3、软件默认将转换成功的文件保存到C:UsersAdministratorVideos,可以修改输出视频的保存路径, 选择自定义,然后点击文件夹图标将保存路径选择为桌面。4、然后选择选择输出文件的格式,在软件右上角选择视频格式中的MP4格式,然后再选择输出MP4视频的分辨率为同原文件的原分辨率输出,分辨率太大,视频清晰度就会降低。5、视频转换器还可以给输出视频设置更多的参数,在高级设置中可以设置输出文件的标题、视频比特率、视频分辨率、音频取样频率以及音频文件的声道等众多重要参数。6、最后就是将OGV转换MP4格式了。点击开始转换,然后等待转换视频格式的进度条到达100%,OGV转换MP4格式就成功了。以上就是使用迅捷视频转换器将视频格式OGV转换MP4的步骤了,如有其他格式的转换需求可查看下面的相关推荐文章,相信你会在这里找到属于你自己的方法。本文来源:https://www.xunjieshipin.com/…

January 23, 2019 · 1 min · jiezi

沉浸式图片轮播器--DDGBannerScrollView

写在前面几乎每个app都会用到图片轮播器,而且图片轮播器也越来越高大上,沉浸式等拉高了APP的档次,没有一个高大上的图片轮播器,都不好意思上架。 像一些知名的app都采用了图片轮播的背景渐变色,举几个栗子:优酷的首页,喜马拉雅,蜻蜓fm,哔哩哔哩漫画等, page索引也是玩的很高大上,系统的早已满足不了了需求。 鉴于此和项目的需要,在前人的基础上,整理了一个这个库,志在简单的几句代码,就能让应用看上去高大上。 github:DDGBannerScrollViewDDGBannerScrollView 此库的功能 1、无限图片轮播功能 2、每个图片的相对偏移量,方便开发者自己封装东西 3、pageControl的几个动画,(旋转,跳跃等慢慢会增加) DDGBannerScrollView 用到的知识 1、图片轮播器(UICollectionView + 定时器) 2、一种颜色向另一种颜色线性的渐变。 3、简单的旋转动画(frame动画 CABasicAnimation) 4、简单的贝塞尔曲线(半圆)与动画的组合(UIBezierPath + CAKeyframeAnimation)来看看效果(虽然效果不太明显)动画的模块也可单独使用!模块分解图片轮播器图片轮播器(UICollectionView + 定时器),这个参考了知名的第三方库SDCycleScrollView,并在此基础上做了修改,文末附有链接所以在性能和稳定性上有了保证,在此表示感谢。两种颜色的线性渐变!我们都知道,一个像素点有三原色加上透明度组成,也就是所说的RGBA(红,绿,蓝,透明度),改变其中的任意一个值,给我们呈现的颜色就不一样。比如,一个点的R1为10,另一个颜色的R2为30,那么R1->R2的线性变化的差值就是20 ,如果滑块的偏移量为100,那么渐变系数为0.2,那么R2 = 10 + 100 * 0.2,当我们在拉滑块的过程中,R在颜色变化中就是线性的,同理剩余颜色也是渐变的。如上图中的中间View,就是在两个颜色之间过度。这个关于颜色的扩展,我已经封装到库中,大家可以直接使用。关键函数为下面,具体实现可参考代码/** 得到一个颜色的原始值 RGBA @param originColor 传入颜色 @return 颜色值数组 */+ (NSArray )getRGBDictionaryByColor:(UIColor )originColor;/ 得到两个值的色差 @param beginColor 起始颜色 @param endColor 终止颜色 @return 色差数组 */+ (NSArray *)transColorBeginColor:(UIColor )beginColor andEndColor:(UIColor )endColor;/ 传入两个颜色和系数 @param beginColor 开始颜色 @param coe 系数(0->1) @param endColor 终止颜色 @return 过度颜色 */+ (UIColor )getColorWithColor:(UIColor )beginColor andCoe:(double)coe andEndColor:(UIColor )endColor;简单的旋转动画和贝塞尔半圆动画简单的旋转动画和贝塞尔半圆动画(比较基础和简单,直接上代码)/ 添加旋转动画 @param imageView 旋转的目标图片 @param duration 旋转持续时间 @param clockwise 旋转的方向(正向还是逆向) /- (void)startrRotationImageView:(UIImageView )imageView duration:(CGFloat)duration clockwise:(BOOL)clockwise { CABasicAnimation rotationAnimation; //动画的方式,绕着z轴 rotationAnimation = [CABasicAnimation animationWithKeyPath:@“transform.rotation.z”]; //旋转的弧度 rotationAnimation.toValue = [NSNumber numberWithFloat: clockwise ? M_PI * 2.0 : -M_PI * 2.0 ]; //动画持续的时间 rotationAnimation.duration = duration; //动画角度值是否累加(默认为NO) rotationAnimation.cumulative = NO; //重复次数 rotationAnimation.repeatCount = 1; //动画添加到layer上 [imageView.layer addAnimation:rotationAnimation forKey:@“rotationAnimation”];}/ 沿着UIBezierPath运动 @param imageView 目标b图片 @param duration 动画持续时间 @param controlPoint 控制点 @param clockwise 旋转方向(正向还是逆向) */- (void)startrRotationImageView:(UIImageView *)imageView duration:(CGFloat)duration controlPoint:(CGPoint)controlPoint clockwise:(BOOL)clockwise { CAKeyframeAnimation *animation = [CAKeyframeAnimation animation]; //设置动画属性,因为是沿着贝塞尔曲线动,所以要设置为position animation.keyPath = @“position”; //设置动画时间 animation.duration = duration; // 告诉在动画结束的时候不要移除 animation.removedOnCompletion = YES; // 始终保持最新的效果 //animation.fillMode = kCAFillModeForwards; //贝塞尔曲线 UIBezierPath *circlePath = [UIBezierPath bezierPathWithArcCenter:controlPoint radius:((_dotMargin + _dotNomalSize.width ) /2.0) startAngle: clockwise ? M_PI : 0 endAngle: clockwise ? 0 : M_PI clockwise: clockwise]; // 设置贝塞尔曲线路径 animation.path = circlePath.CGPath; // 将动画对象添加到视图的layer上 [imageView.layer addAnimation:animation forKey:@“position”];}如何使用1,下载本demo,直接将DDGBannerScrollView文件夹下的文件拖入即可,详细使用见demo和源码2,pod ‘DDGBannerScrollView’简单代码//头部banner图片@property (nonatomic, strong) DDGBannerScrollView *bannerScrollView;//头部banner背景图片@property (nonatomic, strong) UIView *bgHeaderView;- (UIView *)bgHeaderView {if (!_bgHeaderView) {_bgHeaderView = [[UIView alloc]init];_bgHeaderView.frame = CGRectMake(0,0, screen_width , screen_width * 0.37 + 120);}return _bgHeaderView;}- (DDGBannerScrollView *)bannerScrollView { if (!_bannerScrollView) { CGRect frame = CGRectMake(30, 88, self.view.frame.size.width - 60, screen_width * 0.37); _bannerScrollView = [DDGBannerScrollView cycleScrollViewWithFrame:frame delegate:self placeholderImage:[UIImage imageNamed:@“cache_cancel_all”]]; _bannerScrollView.imageURLStringsGroup = @[@“3”,@“1”,@“2”,@“1”,@“3”]; } return _bannerScrollView;}[self.bgHeaderView addSubview:self.bannerScrollView];self.bannerScrollView.pageControlAliment = DDGBannerScrollViewPageContolAlimentRight;self.bannerScrollView.pageControlStyle = DDGBannerScrollViewPageControlHorizontal;self.bannerScrollView.pageDotColor = UIColor.greenColor;self.bannerScrollView.currentPageDotColor = UIColor.redColor;//根据偏移量计算设置banner背景颜色- (void)handelBannerBgColorWithOffset:(NSInteger )offset { if (1 == self.changeColors.count) return; NSInteger offsetCurrent = offset % (int)self.bannerScrollView.bounds.size.width ; float rate = offsetCurrent / self.bannerScrollView.bounds.size.width ; NSInteger currentPage = offset / (int)self.bannerScrollView.bounds.size.width; UIColor *startPageColor; UIColor *endPageColor; if (currentPage == self.changeColors.count - 1) { startPageColor = self.changeColors[currentPage]; endPageColor = self.changeColors[0]; } else { if (currentPage == self.changeColors.count) { return; } startPageColor = self.changeColors[currentPage]; endPageColor = self.changeColors[currentPage + 1]; } UIColor *currentToLastColor = [UIColor getColorWithColor:startPageColor andCoe:rate andEndColor:endPageColor]; self.bgHeaderView.backgroundColor = currentToLastColor;}写在最后奉上github地址:DDGBannerScrollView掘金地址:DDGBannerScrollView简书地址:DDGBannerScrollView最后,再次感谢下SDCycleScrollView的作者,也感谢大家的关心和支持,如果对你有帮助,希望你不吝✨star一下。 ...

January 22, 2019 · 2 min · jiezi

iOS个人中心渐变动画、微信对话框、标签选择器、自定义导航栏、短信验证输入框等源码

iOS精选源码简单的个人中心页面-自定义导航栏并予以渐变动画程序员取悦女票的正确姿势—Tip1(iOS美容篇)iOS 前台重启应用和清除角标的问题微信原生提醒对话框3.0JHLikeButton - 有趣的点赞动画,抖音点赞动画WKWebView交互LeeTagView2.0<一行代码集成标签选择器>CRBoxInputView-短信验证码输入框自定义导航栏和标签栏iOS优质博客iOS开发之线程间的MachPort通信与子线程中的Notification转发如题,今天的博客我们就来记录一下iOS开发中使用MachPort来实现线程间的通信,然后使用该知识点来转发子线程中所发出的Notification。简单的说,MachPort的工作方式其实是将NSMachPort的对象添加到一个线程所对应的RunLoop中,并给NSMachPort对象设置相应的代理。在其他线程中调用该… 阅读原文基于MQTT和ProtocolBuffer的推送系统(iOS)由于业务需求,需要实现实时获取服务端更新的数据功能,基于这个需求,进行调研及技术方案的实施,最终决定采用MQTT +ProtocolBuffer基于长连接的数据实时推送的方案;具体实现方案见本文;本文包括三个部分:1.技术选型 2.技术方案实践 3.未来优化方向技术选型在调研过程中,发现需求功能可以使用推送来… 阅读原文[[性能优化]DateFormatter深度优化探索](http://www.code4app.com/blog-…前言在iOS开发中,对日期进行格式化处理通常有三个步骤:创建DateFormatter对象设置日期格式使用DateFormatter对象对日期进行处理我们通过创建单例对象的方式对创建DateFormatter对象,设置日期格式两个步骤进行了缓存,将方法耗时降低为不缓存的方案的10%左右,但是这种优化方法受制于DateF… 阅读原文[[App探索]JSBox中幽灵触发器的实现原理探索](http://www.code4app.com/blog-…前言幽灵触发器是钟颖大神的JSBox中的一个功能,在app进程被杀死的情况下,也可以将通知固定在通知栏,即便用户点击清除,也能马上再弹出,永远不消失,除非用户关闭App的通知权限或者卸载App,才可以消失。这个功能确实比较有意思,而且钟颖大神在介绍视频里有提到是目前JSBox独有的,说明实现得非常巧妙,自己研究的话还是… 阅读原文iOS可视化动态绘制八种排序过程(Swift版)前面几篇博客都是关于排序的,在之前陆陆续续发布的博客中,我们先后介绍了冒泡排序、选择排序、插入排序、希尔排序、堆排序、归并排序以及快速排序。俗话说的好,做事儿要善始善终,本篇博客就算是对之前那几篇博客的总结了。而本篇博客的示例Demo也是在之前那些博客Demo的基础上做的,也算是集成了各种排序的方法,然后给出了可视化的… 阅读原文更多源码更多博文

January 16, 2019 · 1 min · jiezi

中国上海,极客生涯: 一个开发者的 2018, 2019

总结 2018技术进步:做项目涨知识,没有什么比来个难一点的任务,更快了。为了解决他,又是翻书、又是 google , 有时候还发 stackOverFlow ,寻找各种可能的技术,各种场景,综合记忆理解。解决一个,对一类问题的解决套路印象,都挺深。工程师,本来就是解决问题的。做不出来,也别忘了他。说不定哪天,灵光一闪项目简单,怎么办?笔者 iOS ,用 Objective-C 需求容易实现,有时间做做代码优化,翻一下三方库源代码。譬如:AFN 的 AFURLSessionManager 文件中,有一个 _AFURLSessionTaskSwizzling 类。他在 + load 中做了两个方法交换。笔者的工程,用不到网络会话任务继续和网络会话任务挂起这两个通知,就把 _AFURLSessionTaskSwizzling 类的相关代码删了。做启动时间优化的时候,翻到这里有一个 + load 又譬如:SDW 的 HTTPS 挑战,笔者项目网络图片方面,不需要 ATS ,就去掉了SDWebImageDownloaderOperation类和SDWebImageDownloader类 NSURLSessionTaskDelegate 的 ATS 方法- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler 不见得删了好,处理掉更加贴近自己的项目。删了,可以让自己离底层近一点。(笔者独立开发,有这个条件)…..写博客,下笔有益写博客,写一下的好笔者写博客中,发现技术点,本来是很懵懂的,写着写着就通了一些。写一下,既然自己是真的这样思考的,写下思路。下次加载,就快了很多。写博客的好处,就是不太会自欺欺人。自己都骗不过去,蒙谁( 后来又发现,文如其人知识点,东一下,下一下的。完全看心情 )已经写了几篇,就把写给我自己看的完善和严谨化了一些,有一些赞,挺高兴的。感觉大体有两种写法:第一种是 cookbook.要做什么事情,( 需要长什么样的功能 ) ,怎么设计的,( 弄几个概念出来 ),怎么实现的,写到这里,差不多原理和基本的 demo 都清楚了。再处理一下异常情况和边界条件。套路就是,为什么这样做,怎么做,需要什么还有一种,感觉像是念经。有什么东西,这些是做什么的,东搞西搞,搞出来了。( 在学校上课,这种情况,略常见一些 )第一种,感觉比较友好,原理充沛一些,需要准备的时间比较长,对技术点把握的比较好。第二种,感觉仓促了一些。读起来滞涩。比如,有博客里面的代码,不太好 run 起来。看到了,也能感觉是这么个意思。我努力加大第一种的比例,减少第二种的看博客,开卷有益有公司的老法师说: “中文博客不要看,如果不能分辨好坏"很有道理,要学就学最强的。英文资料大法好,可能英文技术世界积累深厚,现在发展的相当成熟了 ( 不必要的事情,比较少 )中文技术博客,发展中,完善中,有小坑看了一些,感觉做好两点,即可首先是,如果一个老法师说什么技术方向不可能,他往往是错的。如果一个老法师说什么技术方向是可能的,他往往是对的。技术途径是丰富多彩的,编程语言本来就是人设计出来解决问题。一般方法是很多的,取舍的余地比较大。其次是,有博客中,有一种情绪。( 常见的是 BS 新手,新手一般脾气高于技术。线下操作,有一定的危险性。本文建议尊重每一个人 )取出我们关心的技术,就好了。老法师的世界,笔者不怎么懂StackOverFlow国内的,还有 segmentFault.借别人的需求,磨练自己的技术。回答出来一些别人的问题,对方采纳。笔者没能总结出来,什么规律。感觉当时,有状态,有灵感这几天,发现别人的需求,NaN职业发展每一份工作,一般都有困境期和舒适期。困境期,各种展现价值,图表现,求生存。不断的拼命完成任务舒适期,就是温水区,稳定下来了,升职又看不到光。好一点的,会把舒适期转变为积累期,给自己持续的刺激,成长。感觉进入了套路的轮回,温水中折腾,看到了社会上的能力似乎有成百成千种笔者惨淡经营中,加薪靠跳槽展望 2019=缺什么,就补什么。想要什么,努力呗技术和知识,不能用来改造自己,改变自己的生活,技术世界,似乎没有了十二分的精彩途径: 自求,才能多福希望技术上,有大的突破,写出来,分享出来,水到渠成的事技术聚会笔者参加技术聚会挺多的,上海地区挺上档次的有,谷歌开发者日感觉其宣讲的技术,有点着力于通俗易懂,传播福祉。参加这个好,跟电影里面的一样,坚定了技术信念。类似的技术主题聚会,有嘉宾主讲,去开拓下眼界,好笔者,听了就忘。惭愧惭愧听多了,感觉冷静了很多。人外,人真多推荐: 饿科技、携程技术(微信公众)技术闲聊,有 Linux , 参加了一下,好像不够大胆和进取感觉反反复复那几句,很安全另一半现状,我妈强烈建议,我减肥,以免孤独一生愿景: 情投意合,心心相印有什么比两颗心,在一起,还要好。女性本来就温柔、善良,唤醒她物质,感觉似乎好像,不是很重要。颜值,不需要。聪明,不需要。正直,还有人喜欢,厉害了真实,还不丑陋,强认识大佬,感觉不怎么需要大佬也不容易啊, ( 有写博客,求啊球 )人家也是很努力,得来的。大佬盛名之下,上面的那个老法师说: “没那么难"羡慕人家吃鸡,不如自己努力有幸认识了几位大佬,感觉生活中,普通人。刚开始从众,后来自己的生活,都搞不定。人家的私生活,兴趣无群里看看消息,笔者就满足了找大佬请教?大佬博客写得好,先看十遍关于标题:上海地区的极客,就像江水里面的鲫鱼一样,数不胜数希望多我一个本文描述下: 一个普通的 iOS 开发者看到的,以及相关理解偏见多,发小号写了很多,阵痛的困惑 ...

January 15, 2019 · 1 min · jiezi

iOS筛选菜单、分段选择器、导航栏、悬浮窗、转场动画、启动视频等源码

iOS精选源码APP启动视频自定义按钮,图片可调整图文间距SPButton一款定制性极高的轮播图,可自定义轮播图Item的样式(或只…iOS 筛选菜单分段选择器仿微信导航栏的实现,让你的导航栏过渡平滑,赏心悦目。iOS仿微信的悬浮窗,自定义转场动画,使用超级简单<br/>iOS优质博客2018 我的技术进阶之路窗外雨夹着雪,2018 的冬天似乎格外寒冷。国内经济下行,资金链断裂、人员优化、冻结 HC 等消息不绝于耳,2018 的冬天被一层阴霾笼罩,互联网从业人员一度人心惶惶。大潮退去,才知道谁在裸泳,这是说企业,也是说个人。毫无疑问的是,互联网从业人员的竞争更加激烈了,作为一名普通的开发者,在没有更好的机遇时,首先应该想的是… 阅读原文iOS开发之UIRefreshControl使用踩坑问题描述接上一个话题,实现了TabBar的点击刷新以后,开始继续写完成功能,刷新UITableView,于是考虑到iOS 10以后,UIScrollView已经有UIRefreshControl的属性了,干脆用自带的写。于是就有了如下的代码:添加UIRefreshControl到UITableView上去UIRefre… 阅读原文iOS开发-WKWebView与JS的交互iOS8以后,Apple公司退出了WKWebView,对比之前的UIWebView不论是处理速度还是内存性能,都有了大幅度的提升!那么下面我就分享一下WKWebView与JS的交互.首先使用WKWebView.你需要导入WebKit #import然后初始化一个WKWebView,设置代理,并且执行代理的方法.在网页加… 阅读原文iOS内存管理的那些事儿-原理及实现为什么要写这篇文章最近在做内存优化相关的问题,趁着这个机会把内存相关知识捋一捋。虽然现在语言设计的趋势之一就是让程序员不在关心内存管理这件事。但是作为一名程序开发,如果因为语言这个特性,而忽略这方面的知识的话,那是非常不可取的,不懂这方面知识,遇到问题会让我们知其然还不知其所以然。因为内存设计的知识比较多,因此我把他做… 阅读原文<br/>更多源码更多博文<br/>

January 9, 2019 · 1 min · jiezi

Git 使用总结-常用命令 (一)

Git 常用命令//初始化代码仓库$ git init//添加当前文件夹下所有文件 $ git add .//提交代码$ git commit -m ‘修改信息’// 将远程代码拷贝到本地$ git clone [远程库地址]// 关联远程仓库$ git remote add origin [远程库地址]// 查看本地分支$ git branch// 分支创建并切换到该分支$ git checkout -b [分支名] 实例 :git checkout -b Dev // 分支创建并切换到该分支 同步本地分支到远程分支 ,确保远程分支有该分支名$ git checkout -b origin/[远程分支名]// 删除 本地分支 注意不要在该分支上操作,切换到其他分支操作$ git branch -D [本地分支名]// 将本地分支删除后 同步git push –delete origin [本地分支名]//提交记录查看$ git log//会退到某个版本号 版本号可以通过’版本记录’ 查看 $ git reset –hard [版本号名称]/组件化中Git常用*****/// 组件快速创建$ pod lib create [组件名]// 本地验证 $ pod lib lint // 本地验证 忽略警告$ pod lib lint –allow-warnings// 远程验证$ pod spec lint// 远程验证 忽略警告$ pod spec lint –allow-warnings// 查看本地索引库$ pod repo// 向本地端提交索引库 也就是把.spec 提交到本地创建的索引库中$ pod repo push [本地索引库名称] xxxx.spec// 忽略警告$ pod repo push [本地索引库名称] xxxx.spec –allow-warnings// 移除本地索引库$ pod repo remove [本地索引库名称]// 移除cocopod 索引文件$ rm ~/Library/Caches/CocoaPods/search_index.json// 打tag$ git tag -a ‘版本号’ -m ‘描述信息’// 推送taggit push –tags ...

January 9, 2019 · 1 min · jiezi

Flutter通用基础库flutter_luakit_plugin

使用flutter_luakit_plugin作为基础库开发flutter应用文章开头我们先开门见山给出使用flutter_luakit_plugin作为基础库开发和普通flutter的区别。由于flutter定位是便携UI包,flutter提供的基础库功能是不足以满足复杂数据的app应用的,一般flutter开发模式如下图所示,当flutter满足不了我们的需求的时候,使用methodchannel和eventchannel调用native接口。而使用flutter_luakit_plugin作为基础库的开发模式如下图所示,用lua来写逻辑层代码,用flutter写UI代码。luakit 提供了丰富的功能支持,可以支持大部分app的逻辑层开发,包括数据库orm,线程管理,http请求,异步socket,定时器,通知,json等等。用户只需要写dart代码和lua代码,不需要写oc、swift或java、kotlin代码,从而大幅提升代码的一致性(所有运行代码都是跨平台的)。flutter_luakit_plugin由来Flutter诞生的时候我很兴奋,因为我对跨平台开发UI的看法一直是不看好的,最主要的原因是无法获得体验一致性,但是Flutter前无古人的解决了这个问题,真正做到一端开发的UI,无论多复杂,在另一端是可以得到一致的体验的,做不到这点的跨平台UI方案实际上并没有达到跨平台节省工作量的效果,Flutter做到了。Flutter1.0.0 发布了,我认为移动端跨平台开发所需要所有元素都已经齐备了,我们尝试使用Flutter做一些功能,一个版本之后我们总结了一些问题。Flutter是一套UI解决方案,但一个功能除了UI,还需要很多支持,网络请求,长连接,短连接,数据库,线程控制等等,这些方面Flutter生态中提供得比较差,没有ios 或者android那么多成熟的解决方案。Flutter 为了克服这问题,提供了一个解决方案,利用methodchannel和eventchannel调用ios和android的接口,利用原生成熟的方案做底层逻辑支撑。我们一开始也是这样解决,但后续的麻烦也来了,由于methodchannel和eventchannel实现的方法是不跨平台的,Flutter从ios和android得到的数据的格式,事件调用的时机等,两个平台的实现是不一样的,基本不可能完全统一,可以这样说,一个功能在一个端能跑通,在另一个端第一次跑一定跑不通,然后就要花大量的时间进行调试,适配,这样做之后跨平台的优势荡然无存,大家就会不断扯皮。相信我,下面的对话会成为你们的日常。ios开发:“你们android写的界面ios跑不起来”Android 开发:“我们android能跑啊,iOS接口写得不对吧”ios开发:“哪里不对,android写的界面,android帮忙调吧”Android 开发:“我又不是ios开发,我怎么调”当一个已有的app要接入flutter,必然会产生一种情况,就是flutter体系里面的数据和逻辑,跟外部原生app的逻辑是不通的,简单说明一下,就是flutter写的业务逻辑通常是用dart语言写的,我们在原生用object-c、swift或者java、kotlin写的代码是不可以脱离flutter的界面调用dart写的逻辑的,这种互通性的缺失,会导致很多数据联动做不到,譬如原生界面要现实一个flutter页面存下来的数据,或者原生界面要为flutter页面做一些预加载,这些都很不方便,主要是下图中,当flutter界面没调用时,从原生调用flutter接口是不允许的。之前我曾经开源一个纯逻辑层的跨平台解决方案luakit(附上luakit的起源),里面提供一个业务开发所需要的基本能力,包括网络请求,长连接,短连接,orm数据库,线程,通知机制等等,而且这些能力都是稳定的、跨平台而且经过实际业务验证过的方案。做完一个版本纯flutter之后,我意识到可以用一种新的开发模式来进行flutter开发,这样可以避免我上面提到的两个问题,我们团队马上付诸实施,做了另一个版本的flutter+luakit的尝试,即用flutter做界面,用lua来写逻辑,结构图如下。新的方案开发效率得到极大的提升,不客气的说真正实现了跨平台,一个业务,从页面到逻辑,所有的代码一气呵成全部由脚本完成(dart+lua),完全不用object-c、swift或者java、kotlin来写逻辑,这样一个业务基本就可以无缝地从一端直接搬到另一端使用,所以我写了这篇文章来介绍我们团队的这个尝试,也把我们的成果flutter_luakit_plugin开源了出来,让这种开发模式帮助到更多flutter开发团队。细说开发模式下一步我们一起看看如何用flutter配合lua实现全部代码都是跨平台的。我们提供了一个 demo project,供大家参考。dart写界面在demo中所有的ui都写在了main.dart,当然在真实业务中肯定复杂很多,但是并不影响我们的开发模式。dart调用lua逻辑接口FlutterLuakitPlugin.callLuaFun(“WeatherManager”, “getWeather”).then((dynamic d) { print(“getWeather” + d.toString()); setState(() { weathers = d; });});上面这段代码的意思是调用WeatherManager的lua模块,里面提供的getWeather方法,然后把得到的数据以future的形式返回给dart,上面的代码相当于调用下面一段lua代码require(‘WeatherManager’).getWeather( function (d) end)然后剩下的事情就到lua,在lua里面可以使用luakit提供的所有强大功能,一个app所需要的绝大部分的功能应该都提供了,而且我们还会不断扩展。大家可能会担心dart和lua的数据格式转换问题,这个不用担心,所有细节在flutter_luakit_plugin都已经做好封装,使用者尽管像使用dart接口那样去使用lua接口即可。在lua中实现所有的非UI逻辑这个demo(WeatherManager.lua)已经演示了如何使用luakit的相关功能,包括,网络,orm数据库,多线程,数据解析,等等如果实在有flutter_luakit_plugin没有支持的功能,可以走回flutter提供的methodchannel和eventchannel的方式实现如何接入flutter_luakit_plugin经过了几个月磨合实践,我们团队已经把接入flutter_luakit_plugin的成本降到最低,可以说是非常方便接入了。我们已经把flutter_luakit_plugin发布到flutter官方的插件仓库。首先,要像其他flutter插件一样,在pubspec.yaml里面加上依赖,可参考demo配置flutter_luakit_plugin: ^1.0.0然后在ios项目的podfile加上ios的依赖,可参考demo配置source ‘https://github.com/williamwen1986/LuakitPod.git'source ‘https://github.com/williamwen1986/curl.git'pod ‘curl’, ‘> 1.0.0’pod ‘LuakitPod’, ‘> 1.0.13’然后在android项目app的build.gradle文件加上android的依赖,可参考demo配置repositories { maven { url “https://jitpack.io” }}dependencies { implementation ‘com.github.williamwen1986:LuakitJitpack:1.0.6’}最后,在需要使用的地方加上import就可以使用lua脚本了import ‘package:flutter_luakit_plugin/flutter_luakit_plugin.dart’;lua脚本我们默认的执行根路径在android是 assets/lua,ios默认的执行根路径是Bundle路径。flutter_luakit_plugin开发环境IDE–AndroidStudioflutter 官方推荐的IDE是androidstudio和visual studio code。我们在开发中觉得androidstudio更好用,所有我们同步也开发了luakit的androidstudio插件,名字就叫luakit。luakit插件提供了以下的一些功能。远程lua调试查找函数使用跳到函数定义跳到文件参数名字提示代码自动补全代码格式化代码语法检查标准lua api自动补全luakit api自动补全大部分功能,跟其他IDE没太多差别,这里我就不细讲了,我重点讲一下远程lua调试功能,因为这个跟平时调试ios和android设备有点不一样,下面我们详细介绍androidstudio luakit插件的使用。androidstudio安装luakit插件AndroidStudio->Preference..->Plugins->Browse reprositories…搜索Luakit并安装Luakit插件然后重启androidstudio配置lua项目打开 Project Struture 窗口选择 Modules、 Mark as Sources添加调试器选择 Edit Configurations …Select plus添加Lua Remote(Mobdebug)远程lua调试在开始调试lua之前,我们要在需要调试的lua文件加上下面一句lua代码。然后设上断点,即可调试。lua代码里面有两个参数,第一个是你调试用的电脑的ip地址,第二个是调试端口,默认是8172。require(“mobdebug”).start(“172.25.129.165”, 8172)luakit的调试是通过socket来传递调试信息的,所有调试机器务必我电脑保持在同一网段,有时候可能做不到,这里我们给出一下办法解决,我们日常调试也是这样解决的。首先让你的手机开热点,然后你的电脑连上手机的热点,现在就可以保证你的手机和电脑是同一网段了,然后查看电脑的ip地址,填到lua代码上,就可以实现调试了。flutter_luakit_plugin提供的api介绍(1) 数据库orm操作这是flutter_luakit_plugin里面提供的一个强大的功能,也是flutter现在最缺的,简单高效的数据库操作,flutter_luakit_plugin提供的数据库orm功能有以下特征面向对象自动创建和更新表结构自带内部对象缓存定时自动transaction线程安全,完全不用考虑线程问题具体可参考demo lua,下面只做简单介绍。定义数据模型– Add the define table to dbData.lua– Luakit provide 7 colum types– IntegerField to sqlite integer – RealField to sqlite real – BlobField to sqlite blob – CharField to sqlite varchar – TextField to sqlite text – BooleandField to sqlite bool– DateTimeField to sqlite integeruser = { dbname = “test.db”, tablename = “user”, username = {“CharField”,{max_length = 100, unique = true, primary_key = true}}, password = {“CharField”,{max_length = 50, unique = true}}, age = {“IntegerField”,{null = true}}, job = {“CharField”,{max_length = 50, null = true}}, des = {“TextField”,{null = true}}, time_create = {“DateTimeField”,{null = true}} },– when you use, you can do just like belowlocal Table = require(‘orm.class.table’)local userTable = Table(“user”)插入数据local userTable = Table(“user”)local user = userTable({ username = “user1”, password = “abc”, time_create = os.time()})user:save()更新数据local userTable = Table(“user”)local user = userTable.get:primaryKey({“user1”}):first()user.password = “efg"user.time_create = os.time()user:save()删除数据local userTable = Table(“user”)local user = userTable.get:primaryKey({“user1”}):first()user:delete()批量更新数据local userTable = Table(“user”)userTable.get:where({age__gt = 40}):update({age = 45})批量删除数据local userTable = Table(“user”)userTable.get:where({age__gt = 40}):delete()select数据local userTable = Table(“user”)local users = userTable.get:all()print(“select all ———–")local user = userTable.get:first()print(“select first ———–")users = userTable.get:limit(3):offset(2):all()print(“select limit offset ———–")users = userTable.get:order_by({desc(‘age’), asc(‘username’)}):all()print(“select order_by ———–")users = userTable.get:where({ age__lt = 30, age__lte = 30, age__gt = 10, age__gte = 10, username__in = {“first”, “second”, “creator”}, password__notin = {“testpasswd”, “new”, “hello”}, username__null = false }):all()print(“select where ———–")users = userTable.get:where({“scrt_tw”,30},“password = ? AND age < ?”):all()print(“select where customs ———–")users = userTable.get:primaryKey({“first”,“randomusername”}):all()print(“select primaryKey ———–")联表操作local userTable = Table(“user”)local newsTable = Table(“news”)local user_group = newsTable.get:join(userTable):all()print(“join foreign_key”)user_group = newsTable.get:join(userTable,“news.create_user_id = user.username AND user.age < ?”, {20}):all()print(“join where “)user_group = newsTable.get:join(userTable,nil,nil,nil,{create_user_id = “username”, title = “username”}):all()print(“join matchColumns “)(2) 通知机制通知机制提供了一个低耦合的事件互通方法,即在原生或者lua或者dart注册消息,在任何地方抛出的消息都可以接收到。Flutter 添加监听消息void notify(dynamic d) {}FlutterLuakitPlugin.addLuaObserver(3, notify);Flutter 取消监听FlutterLuakitPlugin.removeLuaObserver(3, notify);Flutter抛消息FlutterLuakitPlugin.postNotification(3, data);lua 添加监听消息demo codelocal listenerlua_notification.createListener(function (l) listener = l listener:AddObserver(3, function (data) print(“lua Observer”) if data then for k,v in pairs(data) do print(“lua Observer”..k..v) end end end )end);lua抛消息demo codelua_notification.postNotification(3,{ lua1 = “lua123”, lua2 = “lua234”})ios 添加监听消息demo code_notification_observer.reset(new NotificationProxyObserver(self));_notification_observer->AddObserver(3);- (void)onNotification:(int)type data:(id)data{ NSLog(@“object-c onNotification type = %d data = %@”, type , data);}ios抛消息demo codepost_notification(3, @{@“row”:@(2)});android 添加监听消息demo codeLuaNotificationListener listener = new LuaNotificationListener();INotificationObserver observer = new INotificationObserver() { @Override public void onObserve(int type, Object info) { HashMap<String, Integer> map = (HashMap<String, Integer>)info; for (Map.Entry<String, Integer> entry : map.entrySet()) { Log.i(“business”, “android onObserve”); Log.i(“business”, entry.getKey()); Log.i(“business”,”"+entry.getValue()); } }};listener.addObserver(3, observer);android抛消息demo codeHashMap<String, Integer> map = new HashMap<String, Integer>();map.put(“row”, new Integer(2));NotificationHelper.postNotification(3, map);(3) http requestflutter本身提供了http请求库dio,不过当项目的逻辑接口想在flutter,原生native都可用的情况下,flutter写的逻辑代码就不太合适了,原因上文已经提到,原生native是不可以随意调用flutter代码的,所以遇到这种情况,只有luakit合适,lua写的逻辑接口可以在所有地方调用,flutter 、ios、android都可以方便的使用lua代码,下面给出luakit提供的http接口,demo code。– url , the request url– isPost, boolean value represent post or get– uploadContent, string value represent the post data– uploadPath, string value represent the file path to post– downloadPath, string value to tell where to save the response– headers, tables to tell the http header– socketWatcherTimeout, int value represent the socketTimeout– onResponse, function value represent the response callback– onProgress, function value represent the onProgress callbacklua_http.request({ url = “http://tj.nineton.cn/Heart/index/all?city=CHSH000000", onResponse = function (response) end})(4) Async socket异步socket长连接功能也是很多app开发所依赖的,flutter只支持websocket协议,如果app想使用基础的socket协议,那就要使用flutter_luakit_plugin提供的socket功能了,使用也非常简单,demo code,在callback里面拿到数据后可以使用上文提到的通知机制把数据传回到flutter层。local socket = lua_asyncSocket.create(“127.0.0.1”,4001)socket.connectCallback = function (rv) if rv >= 0 then print(“Connected”) socket:read() endend socket.readCallback = function (str) print(str) timer = lua_timer.createTimer(0) timer:start(2000,function () socket:write(str) end) socket:read()endsocket.writeCallback = function (rv) print(“write” .. rv)endsocket:connect()(5) json 解析json是最常用数据类型,使用可参考demolocal t = cjson.decode(responseStr)responseStr = cjson.encode(t)(6) 定时器timer定时器也是项目开发中经常用到的一个功能,定时器我们在orm框架的lua源码里面有用到,demolocal _timer_timer = lua_timer.createTimer(1)//0代表单次,1代表重复_timer:start(2000,function () end)_timer:stop()(7) 还有所有普通适合lua用的库都可以在flutter_luakit_plugin使用flutter技术积累相关链接flutter通用基础库flutter_luakit_pluginflutter_luakit_plugin使用例子《手把手教你编译Flutter engine》《手把手教你解决 Flutter engine 内存漏》修复内存泄漏后的flutter engine(可直接使用)修复内存泄漏后的flutter engine使用例子持续更新中… ...

January 9, 2019 · 3 min · jiezi

Weex系列(5) —— 封装原生组件和模块

目录Weex系列(序) —— 总要知道原生的一点东东(iOS)Weex系列(序) —— 总要知道原生的一点东东(Android)Weex系列(1) —— Hello World项目Weex系列(2) —— 页面跳转和通信Weex系列(3) —— 单页面还是多页面Weex系列(4) —— 老生常谈的三端统一Weex系列(5) —— 封装原生组件和模块[Weex系列(6) —— css相关小结][Weex系列(7) —— web组件和webview][Weex系列(8) —— 是时候简析一下流程原理了][Weex系列(9) —— 踩坑填坑的集锦][Weex系列(10) —— 先这么多吧想到再写。。。]哇,2019年了,时间总是那么快,快过新年了,忙点了也懒点了,还有点想家了,先祝大家新年快乐吧。这一章官网上有介绍,但还是单独拎出来讲一讲,因为后期这块用的还是挺多的。官网的所有组件和模块的截图:在官网 扩展版块,是可以找到封装的方法步骤的。自定义模块iOS:第一步:新建 myModule.h#import <Foundation/Foundation.h>#import <WeexSDK/WXModuleProtocol.h>@interface myModule : NSObject<WXModuleProtocol>@end新建 myModule.m#import “myModule.h”@implementation myModuleWX_EXPORT_METHOD(@selector(log:))- (void)log:(NSString *)inputParam{ NSLog(@"%@",inputParam);}@end第二步:AppDelegate.m里面注册module[WXSDKEngine registerModule:@“myModule” withClass:[myModule class]];Android:第一步:新建myModule.javapublic class MyModule extends WXModule { //run JS thread @JSMethod (uiThread = false) public void log(String inputParam) { Log.d(“自定义模块:”, inputParam); }}第二步:WXApplication.java里面注册moduleWXSDKEngine.registerModule(“MyModule”, MyModule.class);最后:在上层vue里面,我们可以require我们自己封装的module,就能够调用原生的log方法,分别在xcode和Android Studio的控制台,看到hello weex的消息了。这里需要强调一点的是:iOS和Android的module的名字方法要一致,这样在vue里面才能统一的。weex.requireModule(“myModule”).log(“hello weex”)自定义组件组件封装起来比模块是麻烦许多的,一开始也是摸不着头脑,后来就找到weexsdk里面封装的组件,依样画葫芦的开始了。iOS:第一步:新建myComponent.h#import “WXComponent.h”@interface myComponent : WXComponent<UIWebViewDelegate>- (void)notifyWebview:(NSDictionary *) data;- (void)reload;- (void)goBack;- (void)goForward;@end新建myComponent.m#import “myComponent.h”#import <WeexSDK/WXComponent.h>#import <WeexSDK/WXComponentManager.h>#import “WXUtility.h”#import “WXURLRewriteProtocol.h”#import “WXSDKEngine.h”#import <JavaScriptCore/JavaScriptCore.h>@interface WXWebView : UIWebView@end@implementation WXWebView- (void)dealloc{ if (self) {// self.delegate = nil; }}@end@interface myComponent ()@property (nonatomic, strong) JSContext *jsContext;@property (nonatomic, strong) WXWebView *webview;@property (nonatomic, strong) NSString *url;@property (nonatomic, assign) BOOL startLoadEvent;@property (nonatomic, assign) BOOL finishLoadEvent;@property (nonatomic, assign) BOOL failLoadEvent;@property (nonatomic, assign) BOOL notifyEvent;@end@implementation myComponentWX_EXPORT_METHOD(@selector(goBack))WX_EXPORT_METHOD(@selector(reload))WX_EXPORT_METHOD(@selector(goForward))- (instancetype)initWithRef:(NSString *)ref type:(NSString *)type styles:(NSDictionary *)styles attributes:(NSDictionary *)attributes events:(NSArray *)events weexInstance:(WXSDKInstance *)weexInstance{ if (self = [super initWithRef:ref type:type styles:styles attributes:attributes events:events weexInstance:weexInstance]) { self.url = attributes[@“src”]; } return self;}- (UIView *)loadView{ return [[WXWebView alloc] init];}- (void)viewDidLoad{ _webview = (WXWebView *)self.view; _webview.delegate = self; _webview.allowsInlineMediaPlayback = YES; _webview.scalesPageToFit = YES; [_webview setBackgroundColor:[UIColor clearColor]]; _webview.opaque = NO; _jsContext = [_webview valueForKeyPath:@“documentView.webView.mainFrame.javaScriptContext”]; __weak typeof(self) weakSelf = self; _jsContext[@"$notifyWeex"] = ^(JSValue *data) { if (weakSelf.notifyEvent) { [weakSelf fireEvent:@“notify” params:[data toDictionary]]; } }; if (_url) { [self loadURL:_url]; }}- (void)updateAttributes:(NSDictionary *)attributes{ if (attributes[@“src”]) { self.url = attributes[@“src”]; }}- (void)addEvent:(NSString *)eventName{ if ([eventName isEqualToString:@“pagestart”]) { _startLoadEvent = YES; } else if ([eventName isEqualToString:@“pagefinish”]) { _finishLoadEvent = YES; } else if ([eventName isEqualToString:@“error”]) { _failLoadEvent = YES; }}- (void)setUrl:(NSString )url{ NSString newURL = [url copy]; WX_REWRITE_URL(url, WXResourceTypeLink, self.weexInstance) if (!newURL) { return; } if (![newURL isEqualToString:_url]) { _url = newURL; if (_url) { [self loadURL:_url]; } }}- (void)loadURL:(NSString *)url{ if (self.webview) { NSURLRequest *request =[NSURLRequest requestWithURL:[NSURL URLWithString:url]]; [self.webview loadRequest:request]; }}- (void)reload{ [self.webview reload];}- (void)goBack{ if ([self.webview canGoBack]) { [self.webview goBack]; }}- (void)goForward{ if ([self.webview canGoForward]) { [self.webview goForward]; }}- (void)notifyWebview:(NSDictionary *) data{ NSString *json = [WXUtility JSONString:data]; NSString *code = [NSString stringWithFormat:@"(function(){var evt=null;var data=%@;if(typeof CustomEvent===‘function’){evt=new CustomEvent(’notify’,{detail:data})}else{evt=document.createEvent(‘CustomEvent’);evt.initCustomEvent(’notify’,true,true,data)}document.dispatchEvent(evt)}())", json]; [_jsContext evaluateScript:code];}#pragma mark Webview Delegate- (NSMutableDictionary<NSString *, id> *)baseInfo{ NSMutableDictionary<NSString *, id> *info = [NSMutableDictionary new]; [info setObject:self.webview.request.URL.absoluteString ?: @"" forKey:@“url”]; [info setObject:[self.webview stringByEvaluatingJavaScriptFromString:@“document.title”] ?: @"" forKey:@“title”]; [info setObject:@(self.webview.canGoBack) forKey:@“canGoBack”]; [info setObject:@(self.webview.canGoForward) forKey:@“canGoForward”]; return info;}- (void)webViewDidStartLoad:(UIWebView *)webView{ }- (void)webViewDidFinishLoad:(UIWebView *)webView{ if (_finishLoadEvent) { NSDictionary *data = [self baseInfo]; [self fireEvent:@“pagefinish” params:data domChanges:@{@“attrs”: @{@“src”:self.webview.request.URL.absoluteString}}]; }}- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error{ if (_failLoadEvent) { NSMutableDictionary *data = [self baseInfo]; [data setObject:[error localizedDescription] forKey:@“errorMsg”]; [data setObject:[NSString stringWithFormat:@"%ld", (long)error.code] forKey:@“errorCode”]; NSString * urlString = error.userInfo[NSURLErrorFailingURLStringErrorKey]; if (urlString) { // webview.request may not be the real error URL, must get from error.userInfo [data setObject:urlString forKey:@“url”]; if (![urlString hasPrefix:@“http”]) { return; } } [self fireEvent:@“error” params:data]; }}- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{ if (_startLoadEvent) { NSMutableDictionary<NSString *, id> data = [NSMutableDictionary new]; [data setObject:request.URL.absoluteString ?:@"" forKey:@“url”]; [self fireEvent:@“pagestart” params:data]; } return YES;}@end第二步:AppDelegate.m里面注册component[WXSDKEngine registerComponent:@“myComponent” withClass:[myComponent class]];这里需要说明:上面基本上是照着weexsdk里面的webview组件改的,而且就是改了一下名字,方法什么的大家就可以自由发挥了。Android:第一步:新建myComponent.java/ * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * “License”); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */package com.taobao.weex.ui.component;import android.content.Context;import android.net.Uri;import android.support.annotation.NonNull;import android.text.TextUtils;import android.view.View;import com.taobao.weex.WXSDKInstance;import com.taobao.weex.annotation.Component;import com.taobao.weex.adapter.URIAdapter;import com.taobao.weex.common.Constants;import com.taobao.weex.dom.WXDomObject;import com.taobao.weex.ui.view.IWebView;import com.taobao.weex.ui.view.WXWebView;import com.taobao.weex.utils.WXUtils;import java.util.HashMap;import java.util.Map;@Component(lazyload = false)public class myComponent extends WXComponent { public static final String GO_BACK = “goBack”; public static final String GO_FORWARD = “goForward”; public static final String RELOAD = “reload”; protected IWebView mWebView; @Deprecated public myComponent(WXSDKInstance instance, WXDomObject dom, WXVContainer parent, String instanceId, boolean isLazy) { this(instance,dom,parent,isLazy); } public myComponent(WXSDKInstance instance, WXDomObject dom, WXVContainer parent, boolean isLazy) { super(instance, dom, parent, isLazy); createWebView(); } protected void createWebView(){ mWebView = new WXWebView(getContext()); } @Override protected View initComponentHostView(@NonNull Context context) { mWebView.setOnErrorListener(new IWebView.OnErrorListener() { @Override public void onError(String type, Object message) { fireEvent(type, message); } }); mWebView.setOnPageListener(new IWebView.OnPageListener() { @Override public void onReceivedTitle(String title) { if (getDomObject().getEvents().contains(Constants.Event.RECEIVEDTITLE)) { Map<String, Object> params = new HashMap<>(); params.put(“title”, title); fireEvent(Constants.Event.RECEIVEDTITLE, params); } } @Override public void onPageStart(String url) { if ( getDomObject().getEvents().contains(Constants.Event.PAGESTART)) { Map<String, Object> params = new HashMap<>(); params.put(“url”, url); fireEvent(Constants.Event.PAGESTART, params); } } @Override public void onPageFinish(String url, boolean canGoBack, boolean canGoForward) { if ( getDomObject().getEvents().contains(Constants.Event.PAGEFINISH)) { Map<String, Object> params = new HashMap<>(); params.put(“url”, url); params.put(“canGoBack”, canGoBack); params.put(“canGoForward”, canGoForward); fireEvent(Constants.Event.PAGEFINISH, params); } } }); return mWebView.getView(); } @Override public void destroy() { super.destroy(); getWebView().destroy(); } @Override protected boolean setProperty(String key, Object param) { switch (key) { case Constants.Name.SHOW_LOADING: Boolean result = WXUtils.getBoolean(param,null); if (result != null) setShowLoading(result); return true; case Constants.Name.SRC: String src = WXUtils.getString(param,null); if (src != null) setUrl(src); return true; } return super.setProperty(key,param); } @WXComponentProp(name = Constants.Name.SHOW_LOADING) public void setShowLoading(boolean showLoading) { getWebView().setShowLoading(showLoading); } @WXComponentProp(name = Constants.Name.SRC) public void setUrl(String url) { if (TextUtils.isEmpty(url) || getHostView() == null) { return; } if (!TextUtils.isEmpty(url)) { loadUrl(getInstance().rewriteUri(Uri.parse(url), URIAdapter.WEB).toString()); } } public void setAction(String action) { if (!TextUtils.isEmpty(action)) { if (action.equals(GO_BACK)) { goBack(); } else if (action.equals(GO_FORWARD)) { goForward(); } else if (action.equals(RELOAD)) { reload(); } } } private void fireEvent(String type, Object message) { if (getDomObject().getEvents().contains(Constants.Event.ERROR)) { Map<String, Object> params = new HashMap<>(); params.put(“type”, type); params.put(“errorMsg”, message); fireEvent(Constants.Event.ERROR, params); } } private void loadUrl(String url) { getWebView().loadUrl(url); } private void reload() { getWebView().reload(); } private void goForward() { getWebView().goForward(); } private void goBack() { getWebView().goBack(); } private IWebView getWebView() { return mWebView; }}第二步:WXApplication.java里面注册componentWXSDKEngine.registerComponent(“myComponent”, myComponent.class);最后:在上层vue里面,我们就可以直接使用封装好的组件。这里需要强调一点的是:iOS和Android的组件名字一定要一致,这样在vue里面才能统一的。<myComponent src=“url” class=“webview” :style="{ height : screenHeight+‘px’ }"></myComponent>小结1、从上面可以看出不管是组件还是模块,都是要iOS和Android各封装一套的,而且名字还要一致,如果兼容web端,还要做web的扩展,这样才能三端统一的。2、封装组件的版块,我把weex sdk里面的web组件代码拿出来了,也是为了后面webview章节做铺垫吧。3、建议大家可以多看看weex sdk的源码,(这里请忘掉我只是一个前端,我干嘛还要学习oc、java的这些想法吧)其实也还好,也可能是目前我们的项目没有太复杂,封装的还不是很多,也还算简单,谷歌上一搜基本都能解决吧。最后祝大家新的一年,少点bug,多点money,越来越好吧。如果喜欢就请点个赞收藏一下啦~~~ ...

January 8, 2019 · 5 min · jiezi

非对称加密算法--RSA加密原理及运用

密码学是在编码与破译的斗争实践中逐步发展起来的,并随着先进科学技术的应用,已成为一门综合性的尖端技术科学。密码学发展史在说RSA加密算法之前, 先说下密码学的发展史。其实密码学的诞生,就是为了运用在战场,在公元前,战争之中出现了秘密书信。在中国历史上最早的加密算法的记载出自于周朝兵书《六韬.龙韬》中的《阴符》和《阴书》。在遥远的西方,在希罗多德(Herodotus)的《历史》中记载了公元前五世纪,希腊城邦和波斯帝国的战争中,广泛使用了移位法进行加密处理战争通讯信息。相传凯撒大帝为了防止敌人窃取信息,就使用加密的方式传递信息。那么当时的加密方式非常的简单,就是对二十几个罗马字母建立一张对照表,将明文对应成为密文。那么这种方式其实持续了很久。甚至在二战时期,日本的电报加密就是采用的这种原始加密方式。早期的密码学一直没有什么改进,几乎都是根据经验慢慢发展的。直到20世纪中叶,由香农发表的《秘密体制的通信理论》一文,标志着加密算法的重心转移往应用数学上的转移。于是,逐渐衍生出了当今重要的三类加密算法:非对称加密、对称加密以及哈希算法(HASH严格说不是加密算法,但由于其不可逆性,已成为加密算法中的一个重要构成部分)。1976年以前,所有的加密方法都是同一种模式:加密和解密使用同样规则(简称"密钥"),这被称为"对称加密算法",使用相同的密钥,两次连续的对等加密运算后会回复原始文字,也有很大的安全隐患。1976年,两位美国计算机学家Whitfield Diffie 和 Martin Hellman,提出了一种崭新构思,可以在不直接传递密钥的情况下,完成解密。这被称为"Diffie-Hellman密钥交换算法"。也正是因为这个算法的产生,人类终于可以实现非对称加密了:A给B发送信息B要先生成两把密钥(公钥和私钥)。公钥是公开的,任何人都可以获得,私钥则是保密的。A获取B的公钥,然后用它对信息加密。B得到加密后的信息,用私钥解密。理论上如果公钥加密的信息只有私钥解得开,那么只要私钥不泄漏,通信就是安全的。1977年,三位数学家Rivest、Shamir 和 Adleman 设计了一种算法,可以实现非对称加密。这种算法用他们三个人的名字命名,叫做RSA算法。从那时直到现在,RSA算法一直是最广为使用的"非对称加密算法"。毫不夸张地说,只要有计算机网络的地方,就有RSA算法。这种算法非常可靠,密钥越长,它就越难破解。根据已经披露的文献,目前被破解的最长RSA密钥是232个十进制位,也就是768个二进制位,因此可以认为,1024位的RSA密钥基本安全,2048位的密钥极其安全,当然量子计算机除外。RSA算法的原理下面进入正题,解释RSA算法的原理,其实RSA算法并不难,只需要一点数论知识就可以理解。素数:又称质数,指在一个大于1的自然数中,除了1和此整数自身外,不能被其他自然数整除的数。互质,又称互素。若N个整数的最大公因子是1,则称这N个整数互质。模运算即求余运算。“模”是“Mod”的音译。和模运算紧密相关的一个概念是“同余”。数学上,当两个整数除以同一个正整数,若得相同余数,则二整数同余。欧拉函数任意给定正整数n,请问在小于等于n的正整数之中,有多少个与n构成互质关系?(比如,在1到8之中,有多少个数与8构成互质关系?)计算这个值的方法就叫做欧拉函数,以(n)表示。计算8的欧拉函数,和8互质的 1、2、3、4、5、6、7、8(8) = 4如果n是质数的某一个次方,即 n = p^k (p为质数,k为大于等于1的整数),则(n) = (p^k) = p^k - p^(k-1)。也就是(8) = (2^3) =2^3 - 2^2 = 8 -4 = 4计算7的欧拉函数,和7互质的 1、2、3、4、5、6、7(7) = 6如果n是质数,则 (n)=n-1 。因为质数与小于它的每一个数,都构成互质关系。比如5与1、2、3、4都构成互质关系。计算56的欧拉函数(56) = (8) (7) = 4 6 = 24如果n可以分解成两个互质的整数之积,即 n = p k ,则(n) = (p k) = (p1)(p2)欧拉定理:如果两个正整数m和n互质,那么m的(n)次方减去1,可以被n整除。费马小定理:欧拉定理的特殊情况,如果两个正整数m和n互质,而且n为质数!那么(n)结果就是n-1。模反元素还剩下最后一个概念,模反元素:如果两个正整数e和x互质,那么一定可以找到整数d,使得 ed-1 被x整除,或者说ed被x除的余数是1。那么d就是e相对于x的模反元素。等式转换根据欧拉定理由于1^k ≡ 1,等号左右两边都来个k次方由于1 m ≡ m,等号左右两边都乘上m根据模反元素,因为ed 一定是x的倍数加1。所以如下:通过多次的等式转换。终于可以将这两个等式进行合并了!如下:这个等式成立有一个前提!就是关于模反元素的,就是当整数e和(n)互质!一定有一个整数d是e相对于(n)的模反元素。我们可以测试一下。m取值为4n取值为15(n)取值为8e 如果取值为3d 可以为 11、19…(模反元素很明显不止一个,其实就是解二元一次方程)如果你测试了,那么你可以改变m的值试一下,其实这个等式不需要m和n 互质。只要m小于n 等式依然成立。这里需要注意的是,我们可以看做 m 通过一系列运算得到结果仍然是 m。这一系列运算中,分别出现了多个参数n、(n)、e还有d。m 的 e乘上d 次方为加密运算,得到结果 cc 模以 n 为解密运算,得到结果 m这似乎可以用于加密和解密。但这样,加密的结果会非常大。明文数据将非常小(虽然RSA用于加密的数据也很小,但是没这么大悬殊),真正的RSA要更加强大,那么RSA是怎么演变来的呢??早期很多数学家也停留在了这一步!直到1967年迪菲赫尔曼密钥交换打破了僵局!迪菲赫尔曼密钥交换这个密钥交换当时轰动了整个数学界!而且对人类密码学的发展非常重要,因为这个伟大的算法能够拆分刚才的等式。当非对称加密算法没有出现以前,人类都是用的对称加密。所以密钥的传递,就必须要非常小心。迪菲赫尔曼密钥交换 就是解决了密钥传递的保密性,我们来看一下假设一个传递密钥的场景。算法就是用3 的次方去模以17。 三个角色服务器 随机数 15这个15只有服务器才知道。通过算法得到结果 6 因为 3的15次方 mod 17 = 6 。然后将结果 6 公开发送出去,拿到客户端的 12 ,然后用12^15 mod 17 得到结果10(10就是交换得到的密钥)客户端 随机数13客户端用3 的 13次方 mod 17 = 12 然后将得到的结果12公布出去。拿到服务器的 6 ,然后用6^13 mod 17 得到结果10(10就是交换得到的密钥)第三者第三者只能拿到6 和 12 ,因为没有私密数据13、15,所以它没法得到结果10。为什么 6的13次方会和12的15次方得到一样的结果呢?因为这就是规律,我们可以用小一点的数字测试一下3^3 mod 17 = 10和10 ^ 2 mod 17 ; 3 ^ 2 mod 17 = 9和9^3 mod 17结果都是15。迪菲赫尔曼密钥交换最核心的地方就在于这个规律RSA的诞生现在我们知道了m^e % n = c是加密,c^d % n = m是解密,m就是原始数据,c是密文,公钥是n和e,私钥是n和d,所以只有n和e是公开的。加密时我们也要知道(n)的值,最简单的方式是用两个质数之积得到,别人想破解RSA也要知道(n)的值,只能对n进行因数分解,那么我们不想m被破解,n的值就要非常大,就是我们之前说的,长度一般为1024个二进制位,这样就很安全了。但是据说量子计算机(用于科研,尚未普及)可以破解,理论上量子计算机的运行速度无穷快,大家可以了解一下。以上就是RSA的数学原理检验RSA加密算法我们用终端命令演示下这个加密、解密过程。假设m = 12(随便取值,只要比n小就OK),n = 15(还是随机取一个值),(n) = 8,e = 3(只要和(n)互质就可以),d = 19(3d - 1 = 8,d也可以为3,11等等,也就是d = (8k + 1)/3 )终端分别以m=12,7输入结果OpenSSL进行RSA的命令运行Mac可以直接使用OpenSSL,首先进入相应文件夹生成公私钥// 生成RSA私钥,文件名为private.pem,长度为1024bitopenssl genrsa -out private.pem 1024// 从私钥中提取公钥openssl rsa -in private.pem -pubout -out publick.pem// 查看刚刚生成好的私钥cat private.pem// 查看刚刚生成好的公钥cat publick.pem我们可以看到base64编码,明显私钥二进制很大,公钥就小了很多。这时候我们的文件夹内已经多了刚刚生成好的公私钥文件了// 将私钥转换为明文openssl rsa -in private.pem -text -out private.txt里面就是P1、P2还有KEY等信息。对文件进行加密、解密// 编辑文件message内容为hello Vincent!!!// 刚刚的public.pem写成了publick.pem(哎。。。) $ vi message.txt $ cat message.txt hello Vincent!!!// 通过公钥加密数据时,使用encrypt对文件进行加密 $ openssl rsautl -encrypt -in message.txt -inkey publick.pem -pubin -out enc.txt// 此时查看该文件内容为乱码 $ cat enc.txtj��E]a��d�kUE�&< ��I��V/��pL[����O�+�-�M��K��&⪅O��2���o34�:�$���6��C�L��,b�‘M�S�k�0���A��3%�[I���1�����ps"%// 通过私钥解密数据 $ openssl rsautl -decrypt -in enc.txt -inkey private.pem -out dec.txt// 已成功解密,正确显示文件内容 $ cat dec.txt hello Vincent!!!// 通过私钥加密数据时,要使用sign对文件进行重签名$ openssl rsautl -sign -in message.txt -inkey private.pem -out enc.bin// 此时查看该文件内容同样为乱码$ cat enc.bin{���Ew�3�1E��,8-OA2�Is�:���:�@MU���� �i1B���#��6���m�D(�t#/��� ��������>(�>�^@�C��3�MQ�O%// 通过公钥解密数据$ openssl rsautl -verify -in enc.bin -inkey publick.pem -pubin -out dec.bin// 已成功解密,正确显示文件内容$ cat dec.bin hello Vincent!!!RSA用途及特点到这里,大家都知道RSA通过数学算法来加密和解密,效率比较低,所以一般RSA的主战场是加密比较小的数据,比如对大数据进行对称加密,再用RSA给对称加密的KEY进行加密,或者加密Hash值,也就是数字签名。关于RSA数字签名后面再慢慢阐述。该文章为记录本人的学习路程,希望能够帮助大家,也欢迎大家点赞留言交流!!!https://www.jianshu.com/p/ad3… ...

January 4, 2019 · 2 min · jiezi

iOS高仿微信项目、阴影圆角渐变色效果、卡片动画、波浪动画、路由框架等源码

iOS精选源码iOS高仿微信完整项目源码Khala: Swift 编写的iOS/macOS 路由框架微信左滑删除效果的实现与TableViewCell的常用样式介绍实现阴影圆角并存,渐变色背景等功能HJViewStyle卡片动画的封装,易于使用常用的UI组件TLKeyboardTableViewCell卡片展开利用正弦曲线写的波浪动画CCPWaveAnimation<br/>iOS优质博客iOS开发之线程间的MachPort通信与子线程中的Notification转发如题,今天的博客我们就来记录一下iOS开发中使用MachPort来实现线程间的通信,然后使用该知识点来转发子线程中所发出的Notification。简单的说,MachPort的工作方式其实是将NSMachPort的对象添加到一个线程所对应的RunLoop中,并给NSMachPort对象设置相应的代理。在其他线程中调用该… 阅读原文iOS 如何高效的使用多线程写在前面多线程技术在移动端开发中应用广泛,GCD 让 iOS 开发者能轻易的使用多线程,然而这并不意味着代码就一定高效和可靠。深入理解其原理并经常结合业务思考,才能在有限的线程控制 API 中最大化发挥并发编程的能力,也能轻易的察觉到代码可能存在的安全问题并优雅的解决它。本文不会讲解 GCD 和各种“锁”的基本用法,而… 阅读原文TikTok(抖音国际版)逆向,全球的小姐姐们,我来啦!开源地址首先抛出GitHub地址吧~多多支持指点,谢谢。https://github.com/AYJk/AYTik…。在一般的软件开发流程中,都是过程导向结果。在逆向中,你首先拿到的是结果,然后是去分析实现这个结果的过程。理清过程之后,才开始进行逆向的代码编… 阅读原文iOS线性、径向渐变;渐变动画;Lable字体渐变及动画前言你是否注意到,“渐变设计”已经成为了一种美的体验。无论是APP的icon,还是PPT的背景,渐变色都比纯色要多一分丰富。渐变,一位不愿意透露姓名的的设计师PigUpTree表示可以吹一下午——线性渐变、径向渐变、相近色渐变、半透明色渐变、选区和羽化、高斯模糊、扁平化配色与渐变等等。程序员小生一枚的我,拜倒树下。本文… 阅读原文<br/>更多源码更多博文<br/>

January 3, 2019 · 1 min · jiezi

[App探索]JSBox中幽灵触发器的实现原理探索

前言幽灵触发器是钟颖大神的JSBox中的一个功能,在app进程被杀死的情况下,也可以将通知固定在通知栏,即便用户点击清除,也能马上再弹出,永远不消失,除非用户关闭App的通知权限或者卸载App,才可以消失。这个功能确实比较有意思,而且钟颖大神在介绍视频里有提到是目前JSBox独有的,说明实现得非常巧妙,自己研究的话还是很难想到的,非常值得学习,而且当你了解它的实现原理的话,会发现其实可以做很多其他的事情。当某天产品经理对App推送点击率不满意时,可以向她祭出这件大杀器(哈哈,开玩笑的,无线推送这种功能其实苹果很不推荐,因为确实有可能会被一些不良App采用,然后无限推送,让用户反感)。以下内容仅供学习讨论,JSBox是一个很强大的App,有很多值得学习的地方,强烈推荐大家去购买使用。简短的效果视频完整的介绍视频https://weibo.com/tv/v/G79vjv…:1f37179499e39dbc8a7472897b9e056c从2分6秒开始探索历程因为没有可以用来砸壳的越狱手机,而且PP助手也没有JSBox的包,一开始是去搜幽灵触发器,无限通知的实现,发现没找到答案,stackoverflow上的开发者倒是对无限通知比较感兴趣,问答比较多,但是没有人给出答案,基本上也是说因为苹果不希望开发者用这种功能去骚扰用户。所以只能自己阅读通知文档,查资料来尝试实现了。难道是使用时间间隔触发器UNTimeIntervalNotificationTrigger来实现的吗?因为看通知清除了还是一个接一个得出现,很自然就能想到是通过绕过苹果的检测,去改UNTimeIntervalNotificationTrigger的timeInterval属性来实现的,所以写出了一下代码:UNTimeIntervalNotificationTrigger timeTrigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:1.0f repeats:YES];UNMutableNotificationContent content = [[UNMutableNotificationContent alloc] init];content.title = @“推送标题”;UNNotificationRequest request = [UNNotificationRequest requestWithIdentifier:@“requestIdentifier” content:content trigger:timeTrigger];[center addNotificationRequest:request withCompletionHandler:nil];通过传入创建时间间隔为1s的实际间隔触发器来实现,运行后,第一个通知能正常显示出来,清除第一个通知后,显示第二个通知时,app崩溃了,时间间隔不能小于60s。UserNotificationsDemo[14895:860379] *** Terminating app due to uncaught exception ‘NSInternalInconsistencyException’, reason: ’time interval must be at least 60 if repeating’ First throw call stack:(0x1ae2a3ea0 0x1ad475a40 0x1ae1b9c1c 0x1aeca7140 0x1b8738d0c 0x1b8738bdc 0x102d508ac 0x1db487658 0x1dad09a18 0x1dad09720 0x1dad0e8e0 0x1dad0f840 0x1dad0e798 0x1dad13684 0x1db057090 0x1b0cd96e4 0x1030ccdc8 0x1030d0a10 0x1b0d17a9c 0x1b0d17728 0x1b0d17d44 0x1ae2341cc 0x1ae23414c 0x1ae233a30 0x1ae22e8fc 0x1ae22e1cc 0x1b04a5584 0x1db471054 0x102d517f0 0x1adceebb4)libc++abi.dylib: terminating with uncaught exception of type NSExceptiontimeInterval是只读属性,看来苹果早有防范@property (NS_NONATOMIC_IOSONLY, readonly) NSTimeInterval timeInterval;但是这年头,还能活着做iOS开发的谁没还不会用KVC呀,所以很自然得就能想到使用KVC来改UNTimeIntervalNotificationTrigger *timeTrigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:1.0f repeats:YES];UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];content.title = @“推送标题”;UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:@“requestIdentifier” content:content trigger:timeTrigger];[timeTrigger setValue:@1 forKey:@“timeInterval”];[center addNotificationRequest:request withCompletionHandler:nil];而且我打断点看,确实改成功了,但是,很快,当我把第一个通知清除时,手机变成这样了有那么一刻,我心里很慌,我一定好好做人,不去改苹果爸爸的只读属性了。苹果是在显示第二个通知的时候才去判断的,而我们的代码只能控制到将通知请求request添加到UNUserNotificationCenter这一步,所以不太好绕过。难道是使用地点触发器UNLocationNotificationTrigger来实现的吗?UNLocationNotificationTrigger可以通过判断用户进入某一区域,离开某一区域时触发通知,但是我去看了一下设置里面的权限,发现只使用这个功能的时候JSBox并没有请求定位的权限,所以应该不是根据地点触发的。继续阅读文档然后我就去钟颖大神的JSBox社区仔细查看开发者文档,查看关于通知触发相关的api,结果发现不是通过repeats字段,而是通过renew这个字段来决定是否需要重复创建通知的,所以很有可能不是通过时间触发器来实现的,是通过自己写代码去创建一个通知,然后将通知进行发送。在大部分iOS开发同学心中(包括我之前也是这么认为的),普遍都认为当app处于运行状态时,这样的实现方案自然没有问题,因为我们可以获取到通知展示,用户对通知操作的回调。当app处于未运行状态时,除非用户点击通知唤醒app,我们无法获取到操作的回调,但其实在iOS 10以后,苹果公开的UserNotifications框架,允许开发者通过实现UNUserNotificationCenter的代理方法,来处理用户对通知的各种点击操作。具体可以看苹果的这篇文章Handling Notifications and Notification-Related Actions,翻译其中主要的一段:你可以通过实现UNUserNotificationCenter的代理方法,来处理用户对通知的各种点击操作。当用户对通知进行某种操作时,系统会在后台启动你的app并且调用UNUserNotificationCenter的代理对象实现的userNotificationCenter:didReceiveNotificationResponse:withCompletionHandler:方法,参数response中会包含用户进行的操作的actionIdentifier,即便是系统定义的通知操作也是一样,当用户对通知点击取消或者点击打开唤醒App,系统也会上报这些操作。核心就是这个方法// The method will be called on the delegate when the user responded to the notification by opening the application, dismissing the notification or choosing a UNNotificationAction. The delegate must be set before the application returns from application:didFinishLaunchingWithOptions:.- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler __IOS_AVAILABLE(10.0) __WATCHOS_AVAILABLE(3.0) __OSX_AVAILABLE(10.14) __TVOS_PROHIBITED;所以我就写了一个demo来实现这个功能,核心代码如下:AppDelegate.m- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. [[UIApplication sharedApplication] setApplicationIconBadgeNumber:0]; [self applyPushNotificationAuthorization:application];//请求发送通知授权 [self addNotificationAction];//添加自定义通知操作扩展 return YES;}//请求发送通知授权- (void)applyPushNotificationAuthorization:(UIApplication *)application{ if (([[[UIDevice currentDevice] systemVersion] floatValue] >= 10.0)) { UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; center.delegate = self; [center requestAuthorizationWithOptions:(UNAuthorizationOptionBadge | UNAuthorizationOptionSound | UNAuthorizationOptionAlert) completionHandler:^(BOOL granted, NSError * _Nullable error) { if (!error && granted) { NSLog(@“注册成功”); }else{ NSLog(@“注册失败”); } }]; [center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) { NSLog(@“settings========%@",settings); }]; } else if (([[[UIDevice currentDevice] systemVersion] floatValue] >= 8.0)){ [[UIApplication sharedApplication] registerUserNotificationSettings:[UIUserNotificationSettings settingsForTypes:(UIUserNotificationTypeAlert | UIUserNotificationTypeBadge | UIUserNotificationTypeSound ) categories:nil]]; } [application registerForRemoteNotifications];}//添加自定义通知操作扩展- (void)addNotificationAction { UNNotificationAction *openAction = [UNNotificationAction actionWithIdentifier:@“NotificationForeverCategory.action.look” title:@“打开App” options:UNNotificationActionOptionForeground]; UNNotificationAction *cancelAction = [UNNotificationAction actionWithIdentifier:@“NotificationForeverCategory.action.cancel” title:@“取消” options:UNNotificationActionOptionDestructive]; UNNotificationCategory *notificationCategory = [UNNotificationCategory categoryWithIdentifier:@“NotificationForeverCategory” actions:@[openAction, cancelAction] intentIdentifiers:@[] options:UNNotificationCategoryOptionCustomDismissAction]; [[UNUserNotificationCenter currentNotificationCenter] setNotificationCategories:[NSSet setWithObject:notificationCategory]];}# pragma mark UNUserNotificationCenterDelegate//app处于前台时,通知即将展示时的回调方法,不实现会导致通知显示不了- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler{ completionHandler(UNNotificationPresentationOptionBadge| UNNotificationPresentationOptionSound| UNNotificationPresentationOptionAlert);}//app处于后台或者未运行状态时,用户点击操作的回调- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler { [[UIApplication sharedApplication] setApplicationIconBadgeNumber:0]; if ([response.actionIdentifier isEqualToString:UNNotificationDismissActionIdentifier]) {//点击系统的清除按钮 UNTimeIntervalNotificationTrigger *timeTrigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:0.0001f repeats:NO]; UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init]; content.title = @“App探索-NotFound”; content.body = @"[App探索]JSBox中幽灵触发器的实现原理探索”; content.badge = @1; content.categoryIdentifier = @“NotificationForeverCategory”; UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:response.notification.request.identifier content:content trigger:timeTrigger]; [[UNUserNotificationCenter currentNotificationCenter] addNotificationRequest:request withCompletionHandler:nil]; } completionHandler();}- (void)applicationWillResignActive:(UIApplication *)application { // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game.}- (void)applicationDidEnterBackground:(UIApplication *)application { // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.}- (void)applicationWillEnterForeground:(UIApplication *)application { // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.}- (void)applicationDidBecomeActive:(UIApplication *)application { // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.}- (void)applicationWillTerminate:(UIApplication *)application { // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.}ViewController.m- (void)viewDidLoad { [super viewDidLoad]; UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem]; [button addTarget:self action:@selector(sendNotification) forControlEvents:UIControlEventTouchUpInside]; [button setTitle:@“发送一个3s后显示的通知” forState:UIControlStateNormal]; button.frame = CGRectMake(0, 200, [UIScreen mainScreen].bounds.size.width, 100); [self.view addSubview:button];}//发送一个通知- (void)sendNotification { UNTimeIntervalNotificationTrigger *timeTrigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:3.0f repeats:NO]; UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init]; content.title = @“App探索-NotFound”; content.body = @"[App探索]JSBox中幽灵触发器的实现原理探索"; content.badge = @1; content.categoryIdentifier = @“NotificationForeverCategory”; UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:@“requestIdentifier” content:content trigger:timeTrigger]; UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; [center addNotificationRequest:request withCompletionHandler: nil];}必须在didFinishLaunchingWithOptions的方法返回前设置通知中心的代理,这个文档里面都有提及,大家都知道,但是有两个文档里面未曾提及的难点需要注意:隐藏关卡一 必须给通知添加自定义的通知操作1.必须给通知添加自定义的通知操作,并且给发送的通知指定自定义的通知操作的categoryIdentifier,这样系统在用户对通知进行操作时才会调用这个代理方法,- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void(^)(void))completionHandler自定义通知操作是用户长按通知,下方弹出的actionSheet,在我们的Demo中,是“打开App”和“取消”两个操作,其实不添加这些自定义操作的话,系统的这些“管理”,”“查看”,“清除”也是有的,但是当用户点击“清除”时,我们的代理方法didReceiveNotificationResponse就不会被调用了,文档里面没有提及这个,我也是试了好久才试出来的。隐藏关卡二 必须使用上一个通知的requestIdentifier当用户点击“清除”按钮时,即便app处于未运行状态,系统也会在后台运行我们的app,并且执行didReceiveNotificationResponse这个代理方法,在这个方法里面我们会创建一个UNNotificationRequest,把他添加到通知中心去,然后通知会展示出来。但是系统好像对于在app正常运行时添加的UNNotificationRequest跟在didReceiveNotificationResponse方法里添加的UNNotificationRequest做了区分,后者在被用户点击“清除”按钮后,app不会收到didReceiveNotificationResponse回调方法,可能系统也是考虑到开发者可能会利用这个机制去实现无限通知的功能。所以我在创建UNNotificationRequest时,使用的identifier是前一个通知的identifier,这也是实现无限通知的最巧妙的地方,可能很多开发者是知道实现这个代理方法来接受用户点击“清除”的回调,然后做一些通知上报,隔一段时间再次发送通知事情,但是再次创建并发送的通知在被点击“清除”时已经不会再执行didReceiveNotificationResponse回调了。 UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:response.notification.request.identifier content:content trigger:timeTrigger];扩展如果我们做的是效率工具类型的App,利用这个功能做一些固定通知之类的功能,如果我们做的是一些资讯类的App,可以做一些不定间隔推送的功能,而不需要每次用户点击“清除”后,将用户操作通过网络请求上报给服务器,然后服务器根据情况给用户发推送。更多的玩法有待我们探索。Demo https://github.com/577528249/...Demo 演示Gif写文章太耗费时间了,可以的话,求大家给我点个关注吧,会定期写原创文章,谢谢了! ...

January 2, 2019 · 3 min · jiezi

iOS雪花动画、音频图、新闻界面框架、2048游戏、二维码条形码扫码生成等源码

iOS精选源码粒子雪花与烟花的动画iOS 2048游戏JHSoundWaveView - 简单地声波图、音波图一个可快速集成的新闻详情界面框架,类似今日头条,腾讯新闻二维码/条形码扫描及扫描与输入形式切换和二维码的生成页面多tableView滑动悬停数值加减小控件(购物车、商品类数值更改)iOS国际化语言切换iOS优质博客新手也看得懂的 iOS Runtime 教程关于 Runtime ,网上已经有很多很好的文章,写得很详尽。本篇主要是从新手的角度出发,逐步介绍 Runtime 的原理、常用方法、应用场景等。一、Runtime 是什么在 C 语言中,将代码转换为可执行程序,一般要经历三个步骤,即编译、链接、运行。在链接的时候,对象的类型、方法的实现就已经确定好了。而在 Objec… 阅读原文iOS可视化动态绘制连通图(Swift版)今天的博客我们有易到难大致分为三个部分。第一部分我们会画出相应的图,并该图是可以对每个点进行拖动的,在拖动的过程中,我们对其进行重绘。第二部分会取消拖动,使用UIView自带的动画来让其自己变换,当然本部分你也可以使用Timer或者GCD的TimerSource让其运动。第三部分则是第二部分的升级,再第二部分的基础上我… 阅读原文iOS 架构组件:让你的 TableView 优雅起来GitHub 地址:https://github.com/indulgeIn/…、传统方式的弊端UITableView是出场率极高的视图组件,开发者通过实现和协议方法来配置布局逻辑,面向协议设计模式在苹果的代码设计中很常见,它能适应大部分的业务场景且足够灵活。这种方式优点很多,比如某一时… 阅读原文iOS 关于全面屏适配的方案及UI在不同尺寸下适配方案前言全面屏刚出时,网上有说反人类。但过去这么久了,趋于技术的进步或看久了,大家也都慢慢习惯了(只是笔者还是买不起全面屏)。官方适配中文版文档也出来了。回想起刚开始适配全面屏用了一种暴力、并不优雅的方法,以至于后来出了XS(MAX)和XR后出了bug。所以选择一种可靠的、优雅的方案是很有必要的。如今网上关于探讨适配全面屏… 阅读原文iOS Charles 抓包 https 实战并篡改返回数据没需求?No!不想拦截某个软件的接口数据瞧瞧到底干了啥?是否遇到想把返回数据更改下,来测试临界值情况,得找个后端来一起调试下?发个 Post 请求调试只能一步步来码?这些何曾不是 iOS 开发中面临的!只需要拥有它 Charles 抓包,一切都帮你搞定。 Charles 那么iOS … 阅读原文更多源码更多博文

December 26, 2018 · 1 min · jiezi

WKCrashSDK - crash拦截工具

前言由于线上始终出现部分未知原因崩溃问题,遂遵循网易出的crash拦截机制,自实现了一个crash拦截工具,现已上线运行数月,累计拦截闪退···总之很多啦···实现原理原理网上已有很多文章阐述,这里推荐几个链接。网易iOS App运行时Crash自动防护实践[黑魔法教你让iOS APP防住Crash](https://www.jianshu.com/p/021…优势:封装完善,使用方便,仅需将文件导入项目即可生效。具备debug期crash发生的UI层级提示。可和线上接口配合实现实时开关操作。可自定crashinfo上传地点(我司是直接上传到bugly搜集)经过实际测试,已在我司多个线上APP实测有效,暂未发现有什么奇怪的问题。项目要点其实从上述原理文章以及能够了解基本的实现逻辑,只是在实现过程中也遇到了不少的坑。下面就和大家分享一下一些实现过程的坑以及为了满足我司需求拓展的一些功能点。KVO这里划重点1、拦截KVO时,存在部分三方库的不能拦截,以及系统的相机相册无需拦截,否则会出现无效的crash提示,在我的项目已经进行了白名单过滤。如果用了一些特殊的三方,可能在使用此工具时,需要收录一下,避免无效的crashinfo被收集。//白名单主要针对观察者,因为被观察者很有可能是系统类,所以只能针对观察者处理,如果拦截到系统的观察者,则记录入白名单+ (NSArray *)kvoWhiteList{ static NSArray *whiteList = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ whiteList = @[@“WKKVOProxy”,//自己的 @“RACKVOProxy”,//RAC的 @“BLYSDKManager”,//bugly的 @"_YYTextKeyboardViewFrameObserver",//YYKit的 //相册相关 @“PLManagedAlbum”, @“AVCapturePhotoOutput”, @“AVCaptureStillImageOutput”, //3.2.9添加 拍照相关 @“AVCaptureSession”, @“PLPhotoStreamAlbum”, @“AVKVODispatcher”, @“PLCloudSharedAlbum”, @“AVPlayerPropertyCache”, ];//@“AVCaptureFigVideoDevice” }); return whiteList;}2、对KVO的拦截,需使用递归锁保证线程安全。 wk_pthread_mutex_init_recursive(&_lock,true); pthread_mutex_lock(&_lock); pthread_mutex_unlock(&lock);Zombie划重点在有僵尸对象造成崩溃时,实际是将其数据置为空,但是并不释放它,然后将其isa指向一个可接受任何方法的中转类中,以此来拦截掉崩溃。为了统一处理crash上报,在这里用了动态类创建传递类型信息的方式。并且.m文件需要使用MRC,在编译处添加-fno-objc-arc即可。 NSString *className = NSStringFromClass(selfClass); NSString *zombieClassName = [@“WKZombie” stringByAppendingString: className];//这一步很重要,动态生成类,如果被僵尸,则可以得知实际是哪个类产生了僵尸指针 导致崩溃 Class zombieClass = NSClassFromString(zombieClassName); if(!zombieClass) { zombieClass = objc_allocateClassPair([WKZombieStub class], [zombieClassName UTF8String], 0); } objc_destructInstance(self);//销毁实例 相关信息 内存不释放 object_setClass(self, zombieClass); instanceList.size(); if (instanceList.size() >= maxCount) { id object = instanceList.front(); instanceList.pop_front(); free(object); } instanceList.push_back(self);Container在拦截NSArray以及NSDictionary的系列方法时,需要注意一下它们的实现方式是类簇实现,需要找到它们真实的类来拦截才有效。swizzling_exchangeMethod(objc_getClass("__NSArray0"), @selector(objectAtIndex:), @selector(emptyArray_objectAtIndex:));swizzling_exchangeMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:), @selector(arrayI_objectAtIndex:));swizzling_exchangeMethod(objc_getClass("__NSSingleObjectArrayI"), @selector(objectAtIndex:), @selector(singleObjectArrayI_objectAtIndex:));划重点在对NSMutablArray拦截时,需要特别注意其objectAtIndex的方法,需得在遵守MRC的文件下拦截,否则会在iOS8上弹出键盘时,APP进入后台产生崩溃。是必现的。所以在工具中 这个方法是单独放到一个文件里面hook的,然后在编译处为此文件添加-fno-objc-arc。UI层级提示信息在Debug模式下,当拦截到crash时,会出现UI层级的提示,如下图:点击按钮可以查看具体的崩溃信息,如下图前面title表示为崩溃的类型,后面数字为拦截的次数。再次点击cell可定位崩溃的文件、对应方法名、最近一次崩溃发生的时间以及在本机上这个崩溃发生的次数。大家可能也注意到了Crash的按钮是可以随意拖动,以及根据你进入的大类型不同来变更提示信息的。一个可有可无的小优化CrashInfo上报CrashInfo的收集,我们只需要关注WKCrashReport类,去实现它的一个代理即可。@protocol WKCrashReportDelegate <NSObject>- (void)handleCrashInfo:(WKCrashModel *)model type:(NSString *)type;@end返回的两个参数:WKCrashModel 以及 NSString type其功用如下:WKCrashModel@interface WKCrashModel : NSObject@property (nonatomic, strong) NSString * clasName; //产生crash的类名@property (nonatomic, strong) NSString * msg; //could be 方法名,或者其他有效信息@property (nonatomic, strong) NSArray * threadStack;//crash时的堆栈信息@property (nonatomic, assign) NSTimeInterval time;//crash时间@property (nonatomic, strong, readonly) NSString * deviceType;//设备信息@property (nonatomic, strong, readonly) NSString * systemVersion;//系统版本@endNSString type其返回值可能有UnrecognizedSelector,KVO,Container,Timer,NotificationCenter,Null,String,Zombie分别代表八种拦截的crash类型PS:如有特殊需求可自行扩充使用方式Demo地址进入Demo地址找到WKCrashManagerDemo里面的WKCrashSDK文件夹,拖入项目即可。后续我会抽空将其加入cocoapods豪华午餐注:如从Demo中直接拖入,则默认开启除了Zomie拦截外的其他7种类型的crash拦截。如需自定义请查看WKCrashManager的实现文件。联系方式如有兴趣可通过邮箱357863248@qq.com一起交流进步。 ...

December 22, 2018 · 1 min · jiezi

WKViewManager iOS 弹窗架构

前言近来由于App中弹窗过多,再加上还有半透明的引导层,时常会出现多个弹窗重叠弹出,甚至会伴随引导层一起弹出,极大的影响了用户体验。上述问题,其实很简单,只需要添加一个弹窗队列即可。但是与此同时我们又添加了新的需求如下:广告弹窗不能出现在阅读页订阅更新弹窗只能出现在首页版本更新功能提示页不允许出现任何弹窗重新安装后出现的选择性别和分类的弹窗不允许出现任何弹窗广告观赏页面不能出现任何弹窗由于广告和公告页面的弹出顺序不定,当弹出广告时,广告视图可点击产生跳转,但是在跳转后公告会弹出(公告具备倒计时只显示多少秒,然后自动消失),则会出现公告无法被用户看到的情况,则需要在跳转离开页面时,暂停队列由于项目中我们采用了模块化的方式精简代码、降低工作耦合,所以我们的弹窗并不是基于ViewController弹出的,而是在需要弹出的时候获取当前显示的ViewController用于显示的。例如项目中任务模块,任务是可以在任何地方任何时候被完成,任务本身不关心它是位于何时何地被完成,只需要关心完成后,需要对应做出什么操作,例如写本地数据库,更新用户数据,弹出完成任务提示,和ViewController并无大的关联。简单的说,我们的弹窗是属于功能块的,而功能块不应该且不用去关心当前展示的ViewController是哪一个。功能介绍本文介绍的一个弹窗架构实际也可以归类为一个弹窗功能模块,当弹窗出现在不该出现的地方时,它来负责调度和处理,其过程对于其它功能模块透明,并不产生任何耦合度。其实现功能如下:弹窗视图按队列依次出现弹窗视图支持优先级模式队列基于ViewController,杜绝一个弹窗问题导致全局弹窗失效弹窗视图可指定出现于某ViewController出现后(白名单)ViewController可指定不接收弹窗视图出现于自身出现时(黑名单)实现ViewController didDisAppear后 队列暂停,willAppear队列继续优点WKViewManager 弹窗队列管理类,使用AOP切片基于self-manager模式封装,高内聚性,几乎不会和ViewController或功能模块产生耦合。劣势由于是弹窗视图的一个管理工具,所有需要被管理的弹窗均需属于WKBaseView的子类,项目中已书写对应所需要的弹窗基类如WKPopBaseView、WKStepMaskGuideView等等,有需要构建弹窗的时候可以去查看相关类。(关于WKBaseView,是一个封装的非常完善的弹窗基类,预计后续会写一篇文章详细介绍,它的整个结构是非常完善,针对弹窗功能几乎没有扩充瓶颈)实现原理白名单模式(功能点4)白名单模式只有一个关键内容白名单字典 - whiteList白名单字典位于WKViewManager类中。字典中key为从属于WKBaseView的子类的类名字符串,value是一个数组,里面放入对应的ViewController的类名字符串。例如 白名单内容为@{@“KMAnnouncementView”:@[@“HJTMainTabBarVC”]};则在广告公告模块中,KMAnnouncementView调用显示时,所获取的View不是HJTMainTabBarVC的View,则KMAnnouncementView会进入白名单待显示数组中,当HJTMainTabBarVC调用viewwillappear时才会实际被显示出来。一句话介绍白名单白名单中的key对应的view永远只会出现在其对应的value中的vc上,即便你在其他任何VC上调用显示显示KMAnnouncementView, KMAnnouncementView也只会出现在其对应的value的vc上。黑名单模式(功能点5)黑名单模式包括两个关键内容。 黑名单字典 - blackList 黑名单HomeVC - blackListHomeVCClassName黑名单字典位于WKViewManager类中。字典中key为ViewController的类名字符串,value是一个数组,里面应放入对应的从属于WKBaseView的子类的类名字符串。例如 黑名单内容为 @{@“KMBaseViewController”,@[@“WKPopBaseView”]};则表明,WKPopBaseView类(不包含其子类),如果由于功能块解耦或者其它原因,导致WKPopBaseView被展示于KMBaseViewController(不包含其子类)的子视图上时,WKViewManager会拦截此行为,将WKPopBaseView实例存入黑名单待显示数组,当回到黑名单HomeVC时再显示出来。从而实现了WKPopBaseView一定不会出现在KMBaseViewController的视图上。一句话解释黑名单黑名单中key对应的vc显示时,其对应的value中的view必然不会出现,如果被强制出现时,其也不会出现,直到黑名单homeVC的viewwillappear被调用才会显示。功能点6由于队列基于viewcontroller的,所以很简单只需要hook viewcontroller的 willappear和diddisappear即可,在willappear里面找到队列设置suspended为NO,在diddisappear里面suspended为yes即可Demo地址使用方式前提:因为内部实现使用了Masonry布局,所以需要先Pod Masonry三方库。步骤一、下载Demo,找到BaseView文件夹拖入项目。步骤二、将文件中的WKViewManagerHeader.h文件加入全局头文件.pch中。步骤三、新建的View继承自WKPopBaseView,并重写setInterFace,需首先调用父类的实现- (void)setInterFace{ [super setInterFace]; //TODO contentview的高度必须设置 [self.contentView mas_makeConstraints:^(MASConstraintMaker *make) { make.centerX.equalTo(self); make.centerY.equalTo(self).offset(SCREEN_HEIGHT); make.width.height.equalTo(@200); }];}可自定义出现和消失动画——–实现updateContentViewConstraint:(BOOL)isShow方法- (void)updateContentViewConstraint:(BOOL)isShow{ [self.contentView mas_updateConstraints:^(MASConstraintMaker *make) { make.centerY.equalTo(self).offset(isShow ? 0 : SCREEN_HEIGHT); }];}还有更多可自定义的效果,可通过顶层基类WKbaseVIew封装的声明周期实现。详情请查看Demo步骤四、初始化后,在需要展示的地方调用showInView:isShow:方法即可。 WKTestView * v = [[WKTestView alloc] init]; v.title = @“This is Test View can not show in HomePage”; [v showInView:self.view isShow:YES];交流方式邮箱357863248@qq.com

December 21, 2018 · 1 min · jiezi

批量修改OC类名脚本 -- python

前言近日由于种种原因,需要把代码里面的类全部都修改一遍类名。在网上找了一圈相关的开源库,没有发现有合适满意的,始终都存在一些问题,要么出现误修改,要么把方法名或者属性名也给修改了等等情况。于是决定掏出生疏的Python来写一个。批量修改类名需要解决四个问题:如何取得需要修改的类名?如何修改文件中出现的类名并且不会出现误改等情况?如何修改文件名?修改文件名后,如何同步修改project.pbxproj里面的文件名信息?问题一、如何取得需要修改的类名?想法A:由于自己前不久写了一个获取target对应的编译以及资源文件的工具MacPbxprojHelper,利用其来获取到target对应的类名写入文件,然后用python来读取文件获取到需要修改的类名,实时上第一版我也是这么做的,但是不够优雅,明明一个脚本能解决的事,为什么需要那么多步操作呢?想法B:利用python直接遍历文件夹获取类名,没错就决定是你了。其实用python遍历文件实现起来特别简单快捷。但是其中也有可能存在的问题,例如我们的项目中包含有部分资源文件,而资源文件的后缀名是不定的,但是实际我们只需要.m以及.mm结尾的文件前缀作为我们的类名,当然其中包含有类目也需要过滤掉。并且考虑到我们具备一些特殊的需求,需要过滤某些文件甚至某些文件夹下的文件等等。我这边的脚本都有考虑到实现代码如下:def getClassNames(filepath): #读取文件名入数组 #过滤文件夹 filterDirs = [“ThirdKit/”,“小说/”] #过滤文件 filterFiles = [] classNames = [] #遍历filepath下所有文件,包括子目录 for root, dirs, files in os.walk(filepath): for name in files: path = os.path.join(root, name) #过滤文件夹 isFilterDir = 0 for filterDir in filterDirs: if filterDir in path: isFilterDir = 1 break if isFilterDir != 1: if “.m” in name: splitNames = name.split(".m") #只选择.m和.mm结尾的文件 if splitNames[1] == "" or splitNames[1] == “m”: cn = splitNames[0] #过滤文件 isFilterFile = 0 for ff in filterFiles: if ff == cn: isFilterFile = 1 if isFilterFile != 1: #过滤类目 if cn.find("+") == -1: print(cn) classNames.append(cn) return classNames问题二、如何修改文件中出现的类名并且不出现误改的情况?在我搜集到的开源的脚本中,有极大比例的一部分是直接通过类名替换,其实这特别容易出现误改。比如我有一个类名为ABC,一个类名为AB,那么ABC就可能出现被修改两次的情况。所以我这边就采用笨办法,先敲定类名出现的场景,根据其场景设定规则,只有出现类名出现在这些规则中时,才去替换,这样就可以保证100%的正确率。(这也是大坑,因为不同的程序员书写习惯的问题,导致规则的定义实际极度繁琐,而且会出现部分遗漏,每一次遗漏后,我都需要添加规则再重跑来验证··)下面就是这个脚本最大的贡献·· 自认为基本涵盖完了类名的出现场景(如果有没有涵盖的,请记得一定联系我!!),一共37种规则,运用这37种规则,做到了一次脚本,既完美修改,无需在动代码就可以直接运行并且不会有闪退问题··#一开始构思了 14个 后来…. rule_1 = ‘"’ + cn + ‘.’ #引用修改 rule_2 = ‘"’ + cn + ‘"’ #类名被直接使用 rule_3 = ‘:’ + cn + ’ ’ #继承 后续有空格 rule_4 = ‘:’ + cn + ‘\n’ #类名后面是换行 rule_5 = ’ ’ + cn + ‘<’ rule_6 = ’ ’ + cn + ‘\n’ #类名后面是换行 rule_7 = ’ ’ + cn + ’ ’ rule_8 = ’ ’ + cn + ‘(’ rule_9 = ‘[’ + cn + ’ ’ rule_10 = ‘[’ + cn + ‘’ rule_11 = ‘(’ + cn + ‘’ rule_12 = ‘)’ + cn + ‘’ rule_13 = ’ ’ + cn + ‘’ rule_14 = ’ ’ + cn + ‘\n’ rule_15 = ‘(’ + cn + ’ ’ rule_16 = ’ ’ + cn + ‘;’ rule_17 = ‘,’ + cn + ’ ’ rule_18 = ‘,’ + cn + ‘’ rule_19 = ‘)’ + cn + ’ ’ rule_20 = ‘,’ + cn + ‘;’ rule_21 = ’ ’ + cn + ‘,’ rule_22 = ‘<’ + cn + ‘’ #被当做协议 遵守类 rule_23 = ‘<’ + cn + ’ ’ rule_24 = ‘,’ + cn + ‘,’ rule_25 = ’ ’ + cn + ‘:’ rule_26 = ‘:’ + cn + ‘//’ #后面跟注释的、、我TM。。 rule_27 = ’ ’ + cn + ‘//’ rule_28 = ’ ’ + cn + ‘{’ #后面跟大括号的·· rule_29 = ‘)’ + cn + ‘<’ #为什么遵守协议 也用了 实际类名 没有用ID rule_30 = ‘(’ + cn + ‘.’ #为什么类要使用.语法 rule_31 = ’ ’ + cn + ‘.’ #类名调用.语法 rule_32 = ‘!’ + cn + ‘.’ rule_33 = ‘:’ + cn + ‘.’ #类名调用点语法 被当做参数传入 rule_34 = ‘[’ + cn + ‘\n’ #类名被换行 rule_35 = ‘:’ + cn + ‘<’ #继承类名后面 直接接入协议 默认实际是不存在这个问题的 rule_36 = ‘"’ + cn + ‘’ #类取名有下划线以及数字 以及通过工厂用数字来创建的情况 rule_37 = ‘"’ + cn + ‘%’ #类取名有数字 以及通过工厂用数字来创建的情况问题三、如何修改文件名?这个问题其实很好解决,利用python的os库就可以直接rename,只是需要拼接全路径。并且在给类文件重命名的时,需要注意前缀和后缀添加的位置,不要添加到.h和.m后面去了即可。问题四、如何同步修改project.pbxproj里面的文件信息?这里又和第二个问题一样需要定义一定规则,才能确保不会出现误替换,但是这个规则和上面比起来简直是小巫见大巫。规则如下:def pbRule(cn): rule_1 = ‘=’ + cn + “.” rule_2 = ’ ’ + cn + “.” rule_3 = ‘/’ + cn + “.” rule_4 = ‘"’ + cn + “.“以上,问题都解决了。使用方式———-脚本说明———-脚本用于批量修改类名脚本可传入四个参数1、待处理的文件路径. example–>’./KanManHua'2、待修改的pbxproj文件地址 example– >’./KanManHua.xcodeproj/project.pbxproj'3、类名前缀 example–>‘MHT‘4、类名后缀 example–>’MHT’其中前三个参数为必传参数 划重点,要考———-请开心使用———另脚本支持 -h –help 操作当参数错误时,也有对应提示以及上述帮助信息正常使用示例:python changeClassName.py ./KanManHua ./KanManHua.xcodeproj/project.pbxproj MHT _MHThelpinfo获取示例:python changeClassName.pypython changeClassName.py -hpython changeClassName.py –help脚本地址后记由于我司还有需求修改指定 target的类名,实际也有一套可以仅仅只修改target类名的方法,但是由于使用要复杂一点,首先要提取target对应的编译类,再使用脚本来修改,修改使用到我之前发布的一个工具类MacPbxprojHelper,如有需要的话请和我联系。联系方式:357863248@qq.comMacPbxprojHelper介绍链接 ...

December 21, 2018 · 3 min · jiezi

WKAnimatorManager - 转场管理类

前言现如今App的体验越来越重要,我们的App中也要求加入更多的动画效果,其中最广泛的是转场动画,由于需要批量的加入转场动画,如果于每一个VC中去添加修改是一件费时费力的事情,所以有了下面要介绍的一个开源组件–WKAnimatorManager功能介绍实现了对转场动画的封装以及左滑的集中处理。优点:集成方便,使用简单,拓展性强。简要介绍转场管理结构中只有四个类:UINavigationController+WKTransitions功能:hook了UINavigationController的部分方法,实现集成即可使用。其一:hook了push,pop系列方法,于其中设置动画转场子类,便于统一处理动画其二:重写了viewdidload 设置UINavigationController的转场代理为WKAnimatorManager的单例UIViewController+WKTransitions功能:hook了UIViewController的部分方法以及新增了两个属性,实现集成即可使用。其一:为UIViewController添加了两个转场动画属性,一个用于模态转场wk_modelAnimator,一个用于导航栏转场wk_navAnimator,均为WKBaseAnimator的子类其二:hook了dismiss以及present模态转场方法,便于统一处理动画其三:hook了init,讲其模态转场代理设置为WKAnimatorManager单例WKAnimatorManager功能:遵循模态以及导航栏转场协议,集中处理转场逻辑内部自定义了UIScreenEdgePanGestureRecognizer手势,实现左滑返回逻辑自处理。WKBaseAnimator功能:动画处理基类自定义动画只需要继承自此类,将动画效果写于present以及dismiss方法即可。如需左滑手势返回,系统只支持了view层级动画的左滑返回操作,所以左滑返回动画必须为view层级动画,其次需设置edgeType为UIRectEdgeLeft即可。使用方式步骤一、下载Demo,将Demo里的Transitions文件夹拖入项目,再将已整合好的头文件WKAnimatorManagerHeader.h加入全局头文件.pch中。步骤二、创建继承自WKBaseAnimator.h的自定义动画类,下面以Demo中已存在的的动画作为例子(后面有效果展示):#import “WKBaseAnimator.h”@interface MagicMoveTransition : WKBaseAnimator//自定义需要的属性@property (nonatomic, assign) CGRect endRect;//终点@property (nonatomic, strong) UIView * sourceView;@end于.m文件中实现从父类继承而来的present以及dismiss- (void)present{ [UIView animateWithDuration:[self transitionDuration] delay:0.0f usingSpringWithDamping:0.8f initialSpringVelocity:1.0f options:UIViewAnimationOptionCurveLinear animations:^{ ….. } completion:^(BOOL finished) { ….. //告诉系统动画结束 [self completeTransition]; }];}- (void)dismiss { ….. //发生动画 [UIView animateWithDuration:[self transitionDuration] delay:0.0f usingSpringWithDamping:0.8f initialSpringVelocity:1.0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ ….. } completion:^(BOOL finished) { ….. //告诉系统动画结束 [self completeTransition]; }];}可自定义动画执行时间- (NSTimeInterval)transitionDuration{ return 0.6f;}支持左滑返回可以这样,在初始化时将其边界type修改为left- (instancetype)init{ self = [super init]; if (self) { self.edgeType = UIRectEdgeLeft; } return self;}步骤三、在push 或者 present之前为vc设置上对应的动画- (void)gotoWindoeModelVC{ WindowModelVC * vc = [WindowModelVC new]; WKWindowedModelAnimator * animator = [WKWindowedModelAnimator new]; animator.toViewHeight = SCREEN_HEIGHT / 2.f;// vc.wk_modelAnimator = animator;// [self.navigationController presentViewController:vc animated:YES completion:nil];//都可以啊 vc.wk_navAnimator = animator; [self.navigationController pushViewController:vc animated:YES];}效果展示在Demo中提供了一些常用的转场动画的封装,可直接使用。更多自定义的效果,可参考WKAnimatorManager的Demo。后记既然各位看到这了,再给大家分享一个小知识。在我实现自定义侧滑(UIScreenEdgePanGestureRecognizer)时,发现设置了其edges为UIRectEdgeLeft时,手势被触发的时机并不是在我进行侧滑时,而是在全屏任何一个地方都会触发这个手势。这一度对我造成了困扰。那么从触发手势开始思考,手势一开始于屏幕上触发(硬件层面),实质在这个层面是不会也不应该去做任何判断其手势的合法性,而是直接传递到系统由软件来判断其合法性,那么为什么我设置了UIRectEdgeLeft这个值之后,却没有达到预想的效果呢。带着这个疑问,我去拦截了系统的侧滑手势,发现其也是全屏可以被触发,只是一开始是UIGestureRecognizerStatePossible状态,然后如果其起始点超过50则变为了UIGestureRecognizerStateFailed状态,而小于等于50时则进入UIGestureRecognizerStateBegan状态,开始执行手势。那么接下来就简单了,我们只需要实现下面一个代理方法即可实现完全和系统一模一样的侧滑手势了。//系统左滑返回x实现方式- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{ if (gestureRecognizer == self.edgePan) { UIScreenEdgePanGestureRecognizer * pan = (UIScreenEdgePanGestureRecognizer *)gestureRecognizer; CGPoint point = [pan locationInView:self.edgePanVC.view]; return (point.x < 50); } return NO;} ...

December 21, 2018 · 1 min · jiezi

iOS分段选择器、旅行App、标度尺、对对碰小游戏、自定义相册等源码

iOS精选源码企业级开源项目,模仿艺龙旅行App标签选择器–LeeTagViewCSSegmentedControl常用的分段选择器,简单易用!仿微信左滑删除IOS左滑返回输入框iOS 基于PhotoKit框架的自定义相册JHDraw - 画线,画虚线,画五角星,画矩形,画虚线矩形标度尺对对碰小游戏iOS优质博客iOS无痕埋点方案分享探究前言当前互联网行业的竞争已经是非常激烈了, “功能驱动”的时代已经过去了, 现在更加注重软件的细节, 以及用户的体验问题。 说到用户体验,就不得不提到用户的操作行为。 在我们的软件中,我们会到处进行埋点, 以便提取到我们想要的数据,进而分析用户的行为习惯。 通过这些数据,我们也可以更好的分析出用户的操作趋势,从而在用户… 阅读原文Python的iOS自动化打包前言这段时间刚刚学习了一段时间的Python,加上自己是做iOS开发的,就想着用Python来做一个自动化打包,可以自动完成打包,上传到蒲公英,并且发送邮箱给测试人员.一是可以减少打包功夫,二来可以练练手,结合自己的工作来输出一点东西.废话不多说,直接上代码…原理就是使用xcodebuild来控制Xcode进行一系… 阅读原文Swift 中的类型擦除你可能听过这个术语 :类型擦除。甚至你也用过标准库中的类型擦除(AnySequence)。但是具体什么是类型擦除, 我们怎么才能实现类型擦除呢?这篇文章就是介绍这件事情的。在日常的开发中, 总有想要把某个类或者是某些实现细节对其他模块隐藏起来, 不然总会感觉这些类在项目里到处都是。或者想要实现两个不同类之间的互相转换。… 阅读原文一道值得思考的iOS面试题前言最近在群里看到有人发的一道面试题,题目如下:@interface Spark : NSObject @property(nonatomic,copy) NSString *name; @end@implementation Spark-… 阅读原文浅谈iOS之weak底层实现原理前言在iOS开发过程中,会经常使用到一个修饰词“weak”,使用场景大家都比较清晰,用于一些对象相互引用的时候,避免出现强强引用,对象不能被释放,出现内存泄露的问题。weak 关键字的作用弱引用,所引用对象的计数器不会加一,并在引用对象被释放的时候自动被设置为 nil。weak底层原理1.weak编译解析首先需要看一下… 阅读原文更多源码更多博文

December 19, 2018 · 1 min · jiezi

iOS导航栏样式方案梳理

1.背景在iOS开发中每个页面都有可能被个性化设计,但如果页面是以push方式进行管理,那么多个视图控制器共享一个导航栏,导航栏的适配显示就是一个问题。因此需基于系统导航进一步调整和修改才能满足需求。本文参考下面两篇博客进行分析梳理。Kenshin Cui’s Blog美团技术团队2.关注点页面样式自定义(包括隐藏或显示导航栏)之后,关注点如下:导航栏内容Title和Item容易编码维护。页面过渡导航栏内容渐变动画(参见系统导航效果)。页面过渡导航栏背景颜色变化不突兀。支持滑动手势pop。3.导航配置导航栏透明self.navigationBar.isTranslucent = true //需要开启半透明self.navigationBar.setBackgroundImage(UIImage(), for: .default)self.navigationBar.shadowImage = UIImage()导航栏隐藏// 导航栏显示(含animated,否则页面有无导航切换可能会突变,在手势pop时最明显) self.navigationController?.setNavigationBarHidden(true, animated: true)导航栏颜色导航栏半透明开启:既然开启半透明一般是想用模糊效果的,因此明显应使用下列第①种:// ① 半透明开启,此种方式设置颜色有明显模糊效果,展开图层树UINavigatuionBar -> background视图 -> UIVisualEffectView -> UIVisualEffectBackdropView, 发现进行UIVisualEffectBackdropView颜色变化(箭头代表内部子视图),但是因为UIVisualEffectView是模糊控制视图,因此会有模糊效果显现出来self.navigationController?.navigationBar.backgroundColor = UIColor.kcRed// ② 半透明开启,此种方式设置颜色没有模糊效果,展开图层树UINavigationBar ->background视图 -> imageView, 发现imageView颜色变化(箭头代表内部子视图)self.navigationController?.navigationBar.setBackgroundImage(UIImage(color:UIColor.kcRed), for: .default)// ③ 半透明开启,此种方式设置颜色有轻微模糊感,但不如第一种那样明显,展开图层树UINavigatuionBar -> background视图 -> UIVisualEffectView -> _UIVisualEffectSubview,发现_UIVisualEffectSubview颜色变化(箭头代表内部子视图),因为UIVisualEffectView视模糊控制视图,因此会有模糊效果显现出来self.navigationController?.navigationBar.barTintColor = UIColor.kcRed导航栏半透明关闭:建议采用第②种// ① 半透明关闭,此种方式不能设置导航栏背景颜色,展开图层树发现设置backgroundcolor仅仅影响UINavigationBar的颜色,但是UINavigationBar有一个background子视图(默认白色)遮盖了设置的颜色self.navigationController?.navigationBar.backgroundColor = UIColor.kcRed// ② 半透明关闭,此种方式可以设置导航栏颜色,展开图层树UINavigationBar ->background视图 -> imageView,发现imageView颜色变化(箭头代表内部子视图)self.navigationController?.navigationBar.setBackgroundImage(UIImage(color:UIColor.kcRed), for: .default)// ③ 半透明关闭,此种方式可以设置导航栏颜色。展开图层树发现是设置UINavigationBar的子视图background的颜色,但根据API语义(barTintColor)明显不是设置背景专属,可能会影响内部子视图颜色,因此一般不建议采用此种方法来设置背景色self.navigationController?.navigationBar.barTintColor = UIColor.kcRed隐藏导航栏下线 // 展开图层树发现黑线是一个高度为0.33的imageView(iphoneX显示),图层树UINavigationBar ->background视图 -> imageView,颜色为透明度0.3的黑色, self.navigationBar.shadowImage = UIImage()3.方案讨论方案一方案说明:用系统导航栏,且导航栏颜色控制仅仅在每个视图控制器viewWillAppear中进行配置,透明导航栏也可以使用颜色控制。当然也可根据需要部分页面隐藏导航栏。存在问题:此方案过于简单,页面过渡和手势滑动时导航栏颜色效果变化突兀。样例:参见KenshinCui博客的名为原始方式的方案(见其博客代码示例的Demo1)。关注点:不满足关注点3,页面过渡导航栏背景颜色变化突兀。方案二方案说明:隐藏系统导航栏,切换不同颜色的导航条则只需要隐藏用这个方法隐藏导航条然后自定义一个UINavigationBar增加到导航条的位置(添加一个假的导航条)。不过这种方式的由于隐藏了导航条,那么侧滑返回手势也会消失。透明导航条直接隐藏导航条。存在问题:①需要自己添加UINavigationBar。②由于隐藏了系统的导航栏,造成侧滑手势丢失。解决方式是重新设置当前控制器的interactivePopGestureRecognizer.delegate=self,但是多次push、pop会出现界面错乱操作失效的问题(解决方式就是在适当的时候禁用侧滑或者禁止手势shouldReceiveTouch)。样例:参见KenshinCui博客的方案1(见其博客代码示例的Demo2)。关注点:由于需要添UINavigationBar所以不满足关注点1;此方案导航栏内容和背景随视图渐进平移,背景不突兀,不满足关注点2,但满足关注点3;对于关注点4需要控制好手势的响应。此方案实现起来复杂,并且导航栏原生的特殊效果没有(自适应调整滚动视图、 iOS 11的大标题特效等),但此方案并没有突兀点,不影响需求的话可以采用。方案三方案说明:系统导航栏透明,自定义导航栏背景视图,将系统原有导航栏的背景设置为透明色,同时在每个 ViewController上添加一个View或者 NavigationBar来充当我们实际看到的导航栏,每个ViewController同样只需要关心自身的样式即可。当然也可根据需要部分页面隐藏导航栏。存在问题:基本上满足需求,但和系统原生比较起来,需要自己实现半透明效果,另外可在转场过程中通过self.transitionCoordinator?.animateAlongsideTransition设置navigationBar透明度。样例:参见KenshinCui博客的方案2(见其博客代码示例的Demo3)。关注点:基本满足所列4个关注点。方案四方案说明:隐藏导航栏,每个页面包含一个NavigationController ,每个页面有2个ViewController和一个NavigationController,一个ViewController交给所属导航管理页面跳转,且其子视图为NavigationController(寄宿到另一个ViewController)。我们具体细节内容布局在导航内层那个ViewController。存在问题:视图结构复杂,过渡时导航内容的没动画,手势处理需谨慎(面临两个导航)。样例:网传网易云音乐是这样的关注点:看起来和方案二相似,更好的满足关注点1。不满足关注点2。满足关注点3,如果手势处理好可满足关注点4。相对每个自身页面而言,导航栏的原生特殊效果可以通过内层NavigationController达到。方案五方案说明:使用系统导航栏,页面过渡添加Fake Bar在转场的过程中隐藏原有的导航栏并添加假的 NavigationBar,当转场结束后删除假的 NavigationBar 并恢复原有的导航栏,这一过程可以通过 Swizzle 的方式完成,而每个 ViewController 只需要关心自身的样式即可。当然也可根据需要部分页面隐藏导航栏。存在问题:但在解决 Bug 的时候,Swizzle 这种方式无疑会增加解决问题的时间成本和学习成本。样例:美团关注点:不满足关注点2,其它满足。4.推荐方案优先推荐方案3,简单易用;方案3为避免出乱子,需要良好的团队代码规范和完善的技术文档来做辅助。如果旧项目并且历史问题较多采用方案5。方案2和方案4满足需求的情况下也可选用,但这两个方案较复杂。 ...

October 30, 2018 · 1 min · jiezi

iOS开发手册

1.工程文件结构所有的文件应放在工程中的项目目录下。项目文件和物理文件需保持一致。Xcode创建的任何组(group)都必须在文件系统中有映射。项目文件不仅可以按照业务类型分组,也可以根据功能分组。2.代码格式规范2.1 代码注释格式文件注释:采用Xcode自动生成的注释格式。//// AppDelegate.h// 项目名称//// Created by 开发者姓名 on 2018/6/8.// Copyright © 2018年 公司名称. All rights reserved.//import注释:如果有一个以上的import语句,对这些语句进行分组,每个分组的注释是可选的。// Framework#import <UIKit/UIKit.h>// Model#import “WTUser.h”// View#import “WTView.h"方法注释:Xcode8之后快捷键自动生成(option + command + /)。/**<#Description#>@param application <#application description#>@param launchOptions <#launchOptions description#>@return <#return value description#>*/- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions;代码块注释:单行的用 “// + 空格” 开头, 多行用“/ /”。2.2 代码结构与排版声明文件:方法顺序和实现文件的顺序保持一致,根据需要用”#pragma mark -“将方法分组。实现文件:必须用”#pragma mark -“将方法分组。分组前后优先级:Lifecycle方法 > Public方法 > UI方法 > Data方法 > Event方法 > Private方法(逻辑处理等) > Delegate方法 > 部分Override方法 > Setter方法 > Getter方法。#pragma mark - Lifecycle- (instancetype)init {}- (void)viewDidLoad {}- (void)viewWillAppear:(BOOL)animated {}- (void)viewDidAppear:(BOOL)animated {}- (void)viewWillDisappear:(BOOL)animated {}- (void)viewDidDisappear:(BOOL)animated {}- (void)didReceiveMemoryWarning {}- (void)dealloc {}#pragma mark - Public- (void)refreshData {}#pragma mark - UI- (void)initSubViews {}#pragma mark - Data- (void)initData {}- (void)constructData {}#pragma mark - Event- (void)clickButton:(UIButton *)button {}#pragma mark - Private- (CGFloat)calculateHeight {}#pragma mark - UIScrollViewDelegate- (void)scrollViewDidScroll:(UIScrollView *)scrollView {}#pragma mark - Override- (BOOL)needNavigationBar {}#pragma mark - Setter- (void)setWindow:(UIWindow *)window {}#pragma mark - Getter- (UIWindow *)window {}变量:优先使用属性声明而非变量声明,注意属性修饰符、变量类型、变量之间的间隔。@property (strong, nonatomic) UIWindow *window;点语法:应始终使用点语法来访问和修改属性。间距要求如下:一个缩进使用四个空格。在”-“或者”+“号之后应该有一个空格,方法的大括号和其它大括号始终和声明在同一行开始,在新的一行结束,另外方法之间应该空一行。- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary )launchOptions { if (door.isClosed) { // Do something } else { // Do something } return YES;}长度要求如下:每行代码的长度不应该超过100个字符。单个函数或方法的实现代码控制在50行以内。单个文件里的代码行数控制在500600行之内。3.代码命名规范3.1 代码命名基础最好是既清晰又简短,但不要为简短丧失清晰性,并使用驼峰命名法。名称通常不缩写,即使名称很长也要拼写完全(禁止拼音),然而可使用少数非常常见的缩写,部分举例如下:常用缩写词含义常用缩写词含义appapplicationmaxmaximumaltalternateminminimumcalccalculatemsgmessageallocallocterectrectangledeallocdealloctemsgmessageinitinitializetemptemporaryintintegerfuncfunction由于Cocoa(Objective-C)没有C++一样的命名空间机制,需添加前缀(公司名首字母)防止命名冲突,前缀使用2个字符(以下统称项目前缀)。常见的单词略写:ASCII,PDF,HTTP,XML,URL,JPG,GIF,PNG,RGB等3.2 类和协议命名类名应明确该类的功能,并且要有项目前缀防止命名冲突。协议组合一组方法作为一个类的部分接口使用, 用类名作为协议名,例如:NSObject。协议仅仅组合一组方法而不关联具体类,这种协议的命名应采用动名词形式(ing)。委托形式的协议命名为类名加上Delegate,例如:UIScrollViewDelegate。3.3 变量和属性命名变量名应前置下划线“”,属性名没有下划线。属性本质上是存取方法setter/getter,可进行重写(注意内存管理)。@property (strong, nonatomic) UIWindow *window;- (void)setWindow:(UIWindow *)window;- (UIWindow *)window;可以适当的对setter/getter进行别名设置。@property(nonatomic,getter=isUserInteractionEnabled) BOOL userInteractionEnabled;3.4 方法和函数命名方法名和函数名一般不需要前缀,但函数(C语言形式)作为全局作用域的时候最好加上项目前缀。 表示行为的方法名称以动词开头,但不要使用do/does等无实际意义的助动词。参数前面的单词要能够描述该参数,并且参数名最好能用描述该参数的单词命名。- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;方法中多个参数可以使用适当的介词进行连接。// 后续多个参数使用with- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;// 添加适当介词能够使方法的含义更明确- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;// 第一个参数用了with,后面的参数不使用with- (instancetype)initWithImage:(nullable UIImage *)image highlightedImage:(nullable UIImage *)highlightedImage;只有在方法返回多个值的时候使用get单词进行明确。- (void)getLineDash:(nullable CGFloat *)pattern count:(nullable NSInteger *)count phase:(nullable CGFloat *)phase;方法返回某个对象实例。// 类方法创建对象+ (instancetype)buttonWithType:(UIButtonType)buttonType;// 单例命名+ (UIApplication *)sharedApplication; 委托或代理方法命名第一个参数最好能相关某个对象。- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath;私有方法不要以下划线““开头,因为系统私有方法保留此方式。自定义方法和系统方法重名,建议在方法开头加前缀”xx_methodName“。3.5 常量和宏的命名const常量外部声明:在Objective-C文件中优先采用FOUNDATION_EXTERN和UIKIT_EXTERN,而非C语言中的extern。const常量采用驼峰命名原则。const常量根据作用域适当加上前缀(含项目前缀):可供外部使用需加上相应的类名或者模块前缀,仅文件内部使用需要加上小写字母“k”.宏定义每个字母采用大写,单词之间用下划线“”间隔。宏定义也可根据作用范围加上适当前缀,避免命名冲突。3.6 枚举的命名使用枚举来定义一组相关的整数常量,增强代码的可读性。枚举可根据作用域添加前缀(含项目前缀),格式:[相关类名或功能模块名] + [描述] + [状态]。建议优先采用Objective-C的声明NS_ENUM和NS_OPTIONS,少采用C语言形式的enum等枚举声明.枚举定义时需指定None状态,并且其rawValue一般为起始值0。// NS_ENUMtypedef NS_ENUM(NSInteger, UIStatusBarAnimation) { UIStatusBarAnimationNone = 0, UIStatusBarAnimationFade = 1, UIStatusBarAnimationSlide = 2,}typedef NS_OPTIONS(NSUInteger, UIRemoteNotificationType) { UIRemoteNotificationTypeNone = 0, UIRemoteNotificationTypeBadge = 1 << 0, UIRemoteNotificationTypeSound = 1 << 1, UIRemoteNotificationTypeAlert = 1 << 2, UIRemoteNotificationTypeNewsstandContentAvailability = 1 << 3,}3.7 通知命名外部声明:在Objective-C文件中优先采用FOUNDATION_EXTERN和UIKIT_EXTERN,而非C语言中的extern。通知的命名一般都是跨文件使用的,需添加项目前缀。// [相关联类名或者功能模块名] + [will/Did](可选) + [描述] + NotificationUIApplicationDidEnterBackgroundNotification UIApplicationWillEnterForegroundNotification 3.8 类型别名命名根据作用域添加前缀(含项目前缀),格式:[类名或功能模块名] + [描述]。4.文件资源命名规范资源文件命名也需加上项目前缀。资源文件名全小写,单词之间用下划线“”间隔。资源文件命名格式:[项目前缀] + [业务] + [文件名]图片文件命名格式:[项目前缀] + [业务] + [类型] + [状态]。// 常见类型:logol,icon,img// 常见状态:normal,selected,highlightUIImage *image = [UIImage imageNamed:@“wt_setting_icon_normal”];5.代码警告处理注意警告问题的隐蔽性,因此最好修复警告。警告类型的查看步骤:选中警告 -> 右键Reveal in Log(不编译Reveal in Log是灰色的,因此先编译) ->查看方括号的内容如果需要忽略警告,建议优先代码push或者pop处理。#pragma clang diagnostic push#pragma clang diagnostic ignored “-Warc-retain-cycles”// 造成警告的代码#pragma clang diagnostic pop如果警告数量过大,检查警告类型以及必要性,可xcode配置忽略此类型警告。步骤:选中工程 -> TARGETS -> Build Settings -> Other Warning Flags。忽略单个和全局配置稍有差别,如下举例:push/pop Other Warning Flags-Wformat —-> -Wno-format -Wunused-variable —-> -Wno-unused-variable -Wundeclared-selector —-> -Wno-undeclared-selector -Wint-conversion —-> -Wno-int-conversion也可以在pch等大范围作用域的头文件中添加代码来忽略后续警告:#pragma clang diagnostic ignored “警告名称” 。6.外部库文件引入库文件引入最好把警告处理掉。库文件引入优先采用CocoaPods引入,并且指定版本号。源文件方式需引入文件到工程目录下。源文件方式需注意有无版本说明信息(可能在README文件中,也可能在某个.h头文件中,又或者有Version文件)没有时需在库文件目录下新增版本说明文件,7.代码版本管理版本管理工具:svn 或 git。svn文件管理配置:目录/.subversion打开config文件配置global-ignore。git文件管理配置:.gitignore文件记录了被git忽略的文件,作用于本仓库,常见语法如下:井号(#)用来添加注释用的,比如 “#注释”。build/ : 星号()是通配符,build/则是要说明要忽略 build 文件夹下的所有内容。.pbxuser : 表示要忽略后缀名为.pbxuser的文件。!default.pbxuser : 感叹号(!)是取反的意思,.pbxuser 表示忽略所有后缀名为.pbxuser的文件,如果加上!default.pbxuser则表示,除了default.pbxuse忽略其它后缀名为pbxuse的文件。提交信息规范:BUG类型为“Fix + [BUG编号] + [BUG描述]”。任务类型为“Done + [任务编号] + [任务描述]”。任务中间态为“Doing + [任务编号] + [任务描述]”。引入类库为“import + [类库名]”。8.构建和分发手动构建:Xcode界面化构建注、xcodebuild终端命令构建。自动化构建:Jenkins+Fastlane、xcodebuild脚本执行 。内测分发渠道:fir.im、蒲公英等。线上分发渠道:AppStore。 ...

October 23, 2018 · 2 min · jiezi

ios短视频播放缓存之道

一套基于AVPLayer短视频播放缓存库ShortMediaCache GitHub地址。主要特点:1.为短视频量身设计,接入方便,不侵占业务2.边播变缓存,缓存后直接播放3.预加载功能,秒播下一条短视频4.自动缓存管理业务背景公司电商APP接入短视频模块也有半年多的时间了,之间一直在忙着完善业务功能,现在是时候沉淀下来总结这一路来的收获。视频播放对于ios开发来说其实并不是一个难事儿,简单几行代码就能实现,确实,最初的短视频播放也是基于此,给定视频url直接丢给系统播放器(AVPlayer)就可以播放了。但是随着短视频业务发力,短视频模块在APP业务中承担了更多更重要的角色,如何提升短视频的播放速度变得尤为重要,随之便提出了短视频边播变缓存,短视频预加载相关功能要求。业务分析,公司APP主业务是电商,短视频作为为电商引流业务,提高APP活跃度的业务模块,同时在APP其他业务功能中也存在视频播放,例如商品详情页面商品介绍,基于此设计之初并不打算将所有的播放业务耦合在一起,因为短视频的播放概率远远大于其他长视频,依次业务需求大致分为2类逻辑,短视频和在线播放,对于短视频统一按照短视频播放模块来执行边播变缓存,而其他的相对比较长一些的视频则直接在线播放也不缓存,此处也不做过多介绍。边播边缓存的实现短视频播放特点:1、全屏播放2、快速播放,争取每个短视频都能秒播3、内容高度浓缩,无需进度条与拖拽进度4、精彩的短视频可能会被重复观看几次5、其他(声音控制、流量)基于以上特点,可以大致将短视频播放划分为2个层级,第1层为播放器层,第2层为缓存层,播放器层是基于缓存层的,主要负责播放过程控制和UI展现,比如暂停,继续,声音控制,暂停播放显示控制以及其他的UI;播放器层对于每个APP可能会有不同的业务需求,实现的功能也大不相同,故将缓冲层与播放器层剥离,而缓存层则主要负责短视频内容的下载,预加载,缓存管理,这也是ShortMediaCache的主要功能。如何从缓存播放ShortMediaCache缓存播放逻辑大致的实现结构如下图:对于AVPlayer连接播放器层与缓存层的数据交互是通过自定义实现AVAssetResourceLoaderDelegate协议实现的,在播放器加载的过程中,播放器会通过AVPlayerItem向AVURLAsset的resourceLoader获取需要加载数据信息,比如加载的数据偏移,大小等,最终这些数据请求(AVAssetResourceLoadingRequest)会到达其代理(AVAssetResourceLoaderDelegate)对象,代理对象根据请求数据的位置和大小,去读取相关文件缓存数据,然后回填给请求,以此来响应播放器的数据缓冲请求,与此同时缓存层通过网络请求将下载下来的数据写入文件保存。对于AVAssetResourceLoaderDelegate协议主要需要实现以下方法:- (BOOL)resourceLoader:(AVAssetResourceLoader *)resourceLoader shouldWaitForLoadingOfRequestedResource:(AVAssetResourceLoadingRequest *)loadingRequest;播放器的数据加载请求会放到loadingRequest里面,通过其dataRequest对象的requestedOffset和requestedLength可以知道本次数据请求的区块,从缓存文件中按需读取数据填充后执行finishLoading方法即可完成本次数据请求- (void)resourceLoader:(AVAssetResourceLoader *)resourceLoader didCancelLoadingRequest:(AVAssetResourceLoadingRequest *)loadingRequest;请求取消回调下载对于下载应该放到子线程中去通过NSURLSession来实现,因为视频文件可能之前已经缓存了部分,需要从已缓存的位置大小处继续下载缓存,在每次开启下载前需要去读取已缓存文件的大小,并设置请求头部字段Range便可从此处继续下载后面未下载的部分。NSString *range = [NSString stringWithFormat:@“bytes=%ld-”, (long)cachedSize];[downloadRequest setValue:range forHTTPHeaderField:@“Range”];由于针对短视频的播放不存在进度拖拽或seek功能,所以每次下载到的数据可以直接通过缓存管理的相关方法直接append到缓存文件末尾。因为短视频的播放首要任务就是保证当前单个视频的流畅播放,所以在理论上只会存在一个下载任务来独享所有的下载带宽,当在空闲状态的情况下才适合去做其他的短视频资源的预加载。缓存管理缓存主要创建了三个目录管理,分别为temp、media和trash目录,缓存分为临时缓存和最终缓存,当短视频资源未下载完时是放在一个目录下的(temp目录)、而当视频资源缓存完时移动到另外一个目录(media),这样分别存放便能方便读取和管理两种状态的缓存,所有需要删除的缓存文件都是先移入trash目录,随后再删除以此来保证较高的删除效率。所有文件命名使用的是视频资源的url md5值保证唯一性。缓存应该具有自动管理功能,以防止其无限膨胀,默认配置下ShortMediaCache允许临时缓存最多保存1天,最大100Mb,而最终缓存则允许最多保存2天最大200Mb,如果业务需要可以自定义ShortMediaCacheConfig配置实现。预加载要实现下一个视频或者几个视频能快速的播放起来,预加载的下载任务应该和正常的边下边播任务区分开,因为首先应该保证正在播放的短视频能顺畅的播放,所以边下边播任务优先级应该高于预加载任务,在没有边下边播任务时才能执行预加载任务,并且当有新的边下边播任务时应当停止当前的预加载任务,首要执行边下边播任务。ShortMediaCache提供了预加载功能实现,通过调用ShortMediaManager以下方法:- (void)resetPreloadingWithMediaUrls:(NSArray<NSURL *> *)mediaUrls;使用者可以多次调用此方法,来不断更新需要预加载的资源队列ShortMediaCache使用方式下载源码文件,将ShortMediaCache文件夹引入工程, 通过ShortMediaResourceLoader来创建AVPlayer需要播放视频的AVPlayerItem即可#import “ShortMediaResourceLoader.h"ShortMediaResourceLoader _resourceLoader = [ShortMediaResourceLoader new];AVPlayerItem _playerItem = [_resourceLoader playItemWithUrl:videoUrl]; AVPlayer _player = [AVPlayer playerWithPlayerItem:_playerItem];正常情况下应该持有_resourceLoader对象持续更新ShortMediaCache大致类调用逻辑图如下:更多功能细节和使用方式请前往ShortMediaCacheGitHub地址,下载源码运行Demo体验,后续会持续完善此库和其Demo,如果喜欢欢迎Star,使用问题请Issue.参考:https://mp.weixin.qq.com/s/v1…http://msching.github.io/blog…

September 1, 2018 · 1 min · jiezi