你会如何存储用户的一些敏感信息,如登录的token

使用keychain来存储,也就是钥匙串,使用keychain需要导入Security框架iOS的keychain服务提供了一种安全的保存私密信息(密码,序列号,证书等)的方式,每个ios程序都有一个独立的keychain存储。相对于 NSUserDefaults、文件保存等一般方式,keychain保存更为安全,而且keychain里保存的信息不会因App被删除而丢失,所以在 重装App后,keychain里的数据还能使用。从ios 3。0开始,跨程序分享keychain变得可行。如何需要在应用里使 用使用keyChain,我们需要导入Security.framework ,keychain的操作接口声明在头文件SecItem.h里。直接使用SecItem.h里方法操作keychain,需要写的代码较为复杂,为减轻 咱们程序员的开发,我们可以使用一些已经封装好了的工具类,下面我会简单介绍下我用过的两个工具类:KeychainItemWrapper和 SFHFKeychainUtils。自定义一个keychain的类CSKeyChain.h@interface CSKeyChain : NSObject+ (NSMutableDictionary *)getKeychainQuery:(NSString *)service;+ (void)save:(NSString *)service data:(id)data;+ (id)load:(NSString *)service;+ (void)delete:(NSString *)service;@endCSKeyChain.m#import “CSKeyChain.h”#import<Security/Security.h>@implementation CSKeyChain+ (NSMutableDictionary *)getKeychainQuery:(NSString *)service { return [NSMutableDictionary dictionaryWithObjectsAndKeys: (__bridge_transfer id)kSecClassGenericPassword,(__bridge_transfer id)kSecClass, service, (__bridge_transfer id)kSecAttrService, service, (__bridge_transfer id)kSecAttrAccount, (__bridge_transfer id)kSecAttrAccessibleAfterFirstUnlock,(__bridge_transfer id)kSecAttrAccessible, nil];}+ (void)save:(NSString *)service data:(id)data { // 获得搜索字典 NSMutableDictionary *keychainQuery = [self getKeychainQuery:service]; // 添加新的删除旧的 SecItemDelete((__bridge_retained CFDictionaryRef)keychainQuery); // 添加新的对象到字符串 [keychainQuery setObject:[NSKeyedArchiver archivedDataWithRootObject:data] forKey:(__bridge_transfer id)kSecValueData]; // 查询钥匙串 SecItemAdd((__bridge_retained CFDictionaryRef)keychainQuery, NULL);}+ (id)load:(NSString *)service { id ret = nil; NSMutableDictionary *keychainQuery = [self getKeychainQuery:service]; // 配置搜索设置 [keychainQuery setObject:(id)kCFBooleanTrue forKey:(__bridge_transfer id)kSecReturnData]; [keychainQuery setObject:(__bridge_transfer id)kSecMatchLimitOne forKey:(__bridge_transfer id)kSecMatchLimit]; CFDataRef keyData = NULL; if (SecItemCopyMatching((__bridge_retained CFDictionaryRef)keychainQuery, (CFTypeRef *)&keyData) == noErr) { @try { ret = [NSKeyedUnarchiver unarchiveObjectWithData:(__bridge_transfer NSData *)keyData]; } @catch (NSException *e) { NSLog(@“Unarchive of %@ failed: %@”, service, e); } @finally { } } return ret;}+ (void)delete:(NSString *)service { NSMutableDictionary *keychainQuery = [self getKeychainQuery:service]; SecItemDelete((__bridge_retained CFDictionaryRef)keychainQuery);}@end在别的类实现存储,加载,删除敏感信息方法// 用来标识这个钥匙串static NSString * const KEY_IN_KEYCHAIN = @“com.cs.app.allinfo”;// 用来标识密码static NSString * const KEY_PASSWORD = @“com.cs.app.password”;+ (void)savePassWord:(NSString *)password { NSMutableDictionary *passwordDict = [NSMutableDictionary dictionary]; [passwordDict setObject:password forKey:KEY_PASSWORD]; [CSKeyChain save:KEY_IN_KEYCHAIN data:passwordDict];}+ (id)readPassWord { NSMutableDictionary *passwordDict = (NSMutableDictionary *)[CSKeyChain load:KEY_IN_KEYCHAIN]; return [passwordDict objectForKey:KEY_PASSWORD];}+ (void)deletePassWord { [CSKeyChain delete:KEY_IN_KEYCHAIN];}原文更多:iOS面试题大全 ...

April 1, 2019 · 1 min · jiezi

多线程、锁和线程同步方案

多线程多线程技术大家都很了解,而且在项目中也比较常用。比如开启一个子线程来处理一些耗时的计算,然后返回主线程刷新UI等。首先我们先简单的梳理一下常用到的多线程方案。具体的用法这里我就不说了,每一种方案大家可以去查一下,网上教程很多。常见的多线程方案我们比较常用的是GCD和NSOperation,当然还有NSThread,pthread。他们的具体区别我们不详细说,给出下面这一个表格,大家自行对比一下。容易混淆的术语提到多线程,有一个术语是经常能听到的,同步,异步,串行,并发。同步和异步的区别,就是是否有开启新的线程的能力。异步具备开启线程的能力,同步不具备开启线程的能力。注意,异步只是具备开始新线程的能力,具体开启与否还要跟队列的属性有关系。串行和并发,是指的任务的执行方式。并发是任务可以多个同时执行,串行之能是一个执行完成后在执行下一个。在面试的过程中可能被问到什么网情况下会出现死锁的问题,总结一下就是使用sync函数(同步)往当前的串行对列中添加任务时,会出现死锁。锁多线程的安全隐患多线程和安全问题是分不开的,因为在使用多个线程访问同一块数据的时候,如果同时有读写操作,就可能产生数据安全问题。所以这时候我们就用到了锁这个东西。其实使用锁也是为了在使用多线程的过程中保障数据安全,除了锁,然后一些其他的实现线程同步来保证数据安全的方案,我们一起来了解一下。线程同步方案下面这些是我们常用来实现线程同步方案的。OSSpinLockos_unfair_lockpthread_mutexNSLockNSRecursiveLockNSConditionNSConditinLockdispatch_semaphoredispatch_queue(DISPATCH_QUEUE_SERIAL)@synchronized可以看出来,实现线程同步的方案包括各种锁,还有信号量,串行队列。我们只挑其中不常用的来说一下使用方法。下面是我们模拟了存钱取钱的场景,下面是加锁之前的代码,运行之后肯定是有数据问题的。/** 存钱、取钱演示 /- (void)moneyTest { self.money = 100; dispatch_queue_t queue = dispatch_get_global_queue(0, 0); dispatch_async(queue, ^{ for (int i = 0; i < 10; i++) { [self __saveMoney]; } }); dispatch_async(queue, ^{ for (int i = 0; i < 10; i++) { [self __drawMoney]; } });}/* 存钱 /- (void)__saveMoney { int oldMoney = self.money; sleep(.2); oldMoney += 50; self.money = oldMoney; NSLog(@“存50,还剩%d元 - %@”, oldMoney, [NSThread currentThread]); }/* 取钱 /- (void)__drawMoney { int oldMoney = self.money; sleep(.2); oldMoney -= 20; self.money = oldMoney; NSLog(@“取20,还剩%d元 - %@”, oldMoney, [NSThread currentThread]); }加锁的代码,涉及到锁的初始化、加锁、解锁这么三部分。我们从OSSpinLock开始说。OSSpinLock自旋锁OSSpinLock叫做自旋锁。那什么叫自旋锁呢?其实我们可以从大类上面把锁分为两类,一类是自旋锁,一类是互斥锁。我们通过一个例子来区分这两类锁。如果线程A率先到达加锁的部分,并成功加锁,线程B到达的时候会因为已经被A加锁而等待。如果是自旋锁,线程B会通过执行一个循环来实现等待,我们不用管它循环执行了什么,只要知道他在那"转圈圈"等着就行。如果是互斥锁,那线程B在等待的时候会休眠。使用OSSpinLock需要导入头文件#import <libkern/OSAtomic.h>//声明一个锁@property (nonatomic, assign) OSSpinLock lock;// 锁的初始化self.lock = OS_SPINLOCK_INIT;在我们这个例子中,存钱取钱都是访问了money,所以我们要在存和取的操作中使用同一个锁。/* 存钱 /- (void)__saveMoney { OSSpinLockLock(&_lock); //….省去中间的逻辑代码 OSSpinLockUnlock(&_lock);}/* 取钱 */- (void)__drawMoney { OSSpinLockLock(&_lock); //….省去中间的逻辑代码 OSSpinLockUnlock(&_lock);}这就是简单的自旋锁的使用,我们发现在使用的过程中,Xcode一直提醒我们这个OSSpinLock被废弃了,让我们使用os_unfair_lock代替。OSSpinLock之所以会被废弃是因为它可能会产生一个优先级反转的问题。具体来说,如果一个低优先级的线程获得了锁并访问共享资源,那高优先级的线程只能忙等,从而占用大量的CPU。低优先级的线程无法和高优先级的线程竞争(CPU会给高优先级的线程分配更多的时间片),所以会导致低优先级的线程的任务一直完不成,从而无法释放锁。os_unfair_lock的用法跟OSSpinLock很像,就不单独说了。pthread_mutexDefault一看到这个pthread我们应该就能知道这是一种跨平台的方案了。首先还是来看用法。//声明一个锁@property (nonatomic, assign) pthread_mutex_t lock;//初始化pthread_mutex_init(pthread_mutex_t *restrict _Nonnull, const pthread_mutexattr_t *restrict _Nullable)我们可以看到在初始化锁的时候,第一个参数是锁的地址,第二个参数是一个pthread_mutexattr_t类型的地址,如果我们不传pthread_mutexattr_t,直接传一个NULL,相当于创建一个默认的互斥锁。//方式一pthread_mutex_init(mutex, NULL);//方式二// - 创建attrpthread_mutexattr_t attr;// - 初始化attrpthread_mutexattr_init(&attr);// - 设置attr类型pthread_mutexattr_settype(&attr,PTHREAD_MUTEX_DEFAULT);// - 使用attr初始化锁pthread_mutex_init(&_lock, &attr);// - 销毁attrpthread_mutexattr_destroy(&attr);上面两个方式是一个效果,那为什么使用attr,那就说明除了default类型的还有其他类型,我们后面再说。在使用的时候用pthread_mutex_lock(&_lock); 和 pthread_mutex_unlock(&_lock);加锁解锁。NSLock就是对这种普通互斥锁的OC层面的封装。RECURSIVE 递归锁调用pthread_mutexattr_settype的时候如果类型传入PTHREAD_MUTEX_RECURSIVE,会创建一个递归锁。举个例子吧。// 伪代码-(void)test { lock; [self test]; unlock;}如果是普通的锁,当我们在test方法中,递归调用test,应该会出现死锁,因为被lock,在递归调用时无法调用,一直等待。但是如果锁是递归锁,他会允许同一个线程多次加锁和解锁,就可以解决这个问题了。NSRecursiveLock是对递归锁的封装。Condition 条件锁我们直接上这种锁的使用方法,- (void)otherTest{ [[[NSThread alloc] initWithTarget:self selector:@selector(__remove) object:nil] start]; [[[NSThread alloc] initWithTarget:self selector:@selector(__add) object:nil] start];}// 线程1// 删除数组中的元素- (void)__remove { pthread_mutex_lock(&_mutex); NSLog(@"__remove - begin"); if (self.data.count == 0) { // 等待 pthread_cond_wait(&_cond, &_mutex); } [self.data removeLastObject]; NSLog(@“删除了元素”); pthread_mutex_unlock(&_mutex);}// 线程2// 往数组中添加元素- (void)__add { pthread_mutex_lock(&_mutex); sleep(1); [self.data addObject:@“Test”]; NSLog(@“添加了元素”); // 信号 pthread_cond_signal(&_cond); // 广播// pthread_cond_broadcast(&_cond); pthread_mutex_unlock(&_mutex);}我们创建了两个线程,一个往数组中添加数据,一个删除数据,我们通过这个条件锁实现的效果就是在数组中还没有数据的时候等待,数组中添加了一个数据之后在进行删除。条件锁就是互斥锁+条件。我们声明一个条件并初始化。@property (assign, nonatomic) pthread_cond_t cond;//使用完后也要pthread_cond_destroy(&_cond);pthread_cond_init(&_cond, NULL);在__remove方法中if (self.data.count == 0) { // 等待 pthread_cond_wait(&_cond, &_mutex);}如果线程1率先拿到所并加锁,执行到上面代码这里发现数组中还没有数据,就执行pthread_cond_wait,此时线程1会暂时放开_mutex这个锁,并在这休眠等待。线程2在__add方法中最开始因为拿不到锁,所以等待,在线程1休眠放开锁之后拿到锁,加锁,并执行为数组添加数据的代码。添加完了之后会发个信号通知等待条件的线程,并解锁。 pthread_cond_signal(&_cond); pthread_mutex_unlock(&_mutex);线程2执行了pthread_cond_signal之后,线程1就收到了通知,退出休眠状态,继续执行下面的代码。这个地方可能有人会有疑问,是不是线程2应该先unlock再cond_dingnal,其实这个地方顺序没有太大差别,因为线程2执行了pthread_cond_signal之后,会继续执行unlock代码,线程1收到signal通知后会推出休眠状态,同时线程1需要再一次持有这个锁,就算此时线程2还没有unlock,线程1等到线程2 unlock 的时间间隔很短,等到线程2 unlock 后线程1会再去持有这个锁,并加锁。NSCondition就是OC层面的条件锁,内部把mutex互斥锁和条件封装到了一起。NSConditionLock其实也差不多,NSConditionLock可以指定具体的条件,这两个OC层面的类的用法大家可以自行上网搜索。dispatch_semaphore 信号量@property (strong, nonatomic) dispatch_semaphore_t semaphore;//初始化self.semaphore = dispatch_semaphore_create(5);在初始化一个信号的的过程中传入dispatch_semaphore_create的值,其实就代表了允许几个线程同时访问。再回到之前我们存钱取钱这个例子。self.moneySemaphore = dispatch_semaphore_create(1);我们一次只允许一个线程访问,所以在初始化的时候传1。下面就是使用方法。- (void)__drawMoney{ dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER); // … 省略代码 dispatch_semaphore_signal(self.moneySemaphore);}- (void)__saveMoney{ dispatch_semaphore_wait(self.moneySemaphore, DISPATCH_TIME_FOREVER); // … 省略代码 dispatch_semaphore_signal(self.moneySemaphore);}dispatch_semaphore_wait是怎么上锁的呢?如果信号量>0的时候,让信号量-1,并继续往下执行。如果信号量<=0的时候,休眠等待。就这么简单。dispatch_semaphore_signal让信号量+1。小提示在我们平时使用这种方法的时候,可以把信号量的代码提取出来定义一个宏。#define SemaphoreBegin \static dispatch_semaphore_t semaphore; \static dispatch_once_t onceToken; \dispatch_once(&onceToken, ^{ \ semaphore = dispatch_semaphore_create(1); }); \dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);#define SemaphoreEnd \dispatch_semaphore_signal(semaphore);读写安全方案上面我们讲到的线程同步方案都是每次只允许一个线程访问,在实际的情况中,读写的同步方案应该下面这样:每次只能有一个线程写可以有多个线程同时读读和写不能同时进行这就是多读单写,用于文件读写的操作。在我们的iOS中可以用下面这两种解决方案。pthread_rwlock 读写锁这个读写锁的用法很简单,跟之前的普通互斥锁都差不多,大家随便搜一下应该就能搜到,我就不拿出来写了,这里主要是提一下这种锁,大家以后有需要的时候可以用。dispatch_barrier_async 异步栅栏首先在使用这个函数的时候,我们要用自己创建的并发队列。如果传入的是一个串行队列或者全局的并发队列,那dispatch_barrier_async等同于dispatch_async的效果。self.queue = dispatch_queue_create(“rw_queue”, DISPATCH_QUEUE_CONCURRENT);dispatch_async(self.queue, ^{ [self read];}); dispatch_barrier_async(self.queue, ^{ [self write];});在读取数据的时候,使用dispatch_async往对列中添加任务,在写数据时,用dispatch_barrier_async添加任务。dispatch_barrier_async添加的任务会等前面所有的任务都执行完,他再执行,而且他执行的时候,不允许有别的任务同时执行。atomic我们都知道这个atomic是原子性的意思。他保证了属性setter和getter的原子性操作,相当于在set和get方法内部加锁。atomic修饰的属性是读/写安全的,但不是线程安全。假设有一个 atomic 的属性 “name”,如果线程 A 调用 [self setName:@“A”],线程 B 调用 [self setName:@“B”],线程 C 调用 [self name],那么所有这些不同线程上的操作都将依次顺序执行——也就是说,如果一个线程正在执行 getter/setter,其他线程就得等待。因此,属性 name 是读/写安全的。但是,如果有另一个线程 D 同时在调[name release],那可能就会crash,因为 release 不受 getter/setter 操作的限制。也就是说,这个属性只能说是读/写安全的,但并不是线程安全的,因为别的线程还能进行读写之外的其他操作。线程安全需要开发者自己来保证。 ...

April 1, 2019 · 2 min · jiezi

跨平台技术演进

前言大家好,我是simbawu,关于这篇文章,有问题欢迎来这里讨论。随着移动互联网的普及和快速发展,手机成了互联网行业最大的流量分发入口。以及随着5G的快速发展,未来越来越多的“端”也会如雨后春笋般快速兴起。而“快”作为互联网的生存之道,为了占领市场,企业也会积极跟进,快速布局。同一个应用,各个“端”独立开发,不仅开发周期长,而且人员成本高。同时,作为技术人员,也不应该满足于这种重复、低能的工作状态。在这样的形势下,跨平台的技术方案也受到越来越多人和企业的关注。接下来,我将从原理、优缺点等方面为大家分享《跨平台技术演进》。H5说到跨平台,没人不知道H5。不管是在Mac、Windows、Linux、iOS、Android还是其他平台,只要给一个浏览器,连“月球”上它都能跑。浏览器架构下面,我们来看看让H5如此横行霸道的浏览器的架构:User Interface 用户界面:提供用户与浏览器交互Browser Engine 浏览器引擎:控制渲染引擎与JS解释器Rendering Engine 渲染引擎:负责页面渲染JavaScript Interpreter JS解释器:执行JS代码,输出结果给渲染引擎Networking 网络工作组:处理网络请求UI Backend UI后端:绘制窗口小部件Data Storage 数据存储:管理用户数据浏览器由以上7个部分组成,而“渲染引擎”是性能优化的重中之重,一起了解其中的渲染原理。渲染引擎原理不同的浏览器内核不同,渲染过程会不太一样,但主要流程还是一致的。分为下面6步骤:HTML解析出DOM TreeCSS解析出CSSOMDOM Tree与CSSOM关联生成Render TreeLayout 根据Render Tree计算每个节点的尺寸、位置Painting 根据计算好的信息绘制整个页面的像素信息Composite 将多个复合图层发送给GPU,GPU会将各层合成,然后显示在屏幕上。从以上6步,我们可以总结渲染优化的要点:Layout在浏览器渲染过程中比较耗时,应尽可能避免重排的产生复合图层占用内存比重非常高,可采用减小复合图层进行优化以上就是浏览器端的内容。但H5作为跨平台技术的载体,是如何与不同平台的App进行交互的呢?这时候JSBridge就该出场了。JSBridge原理JSBridge,顾名思义,是JS和Native之间的桥梁,用来进行JS和Native之间的通信。通信分为以下两个维度:JavaScript 调用 Native,有两种方式:拦截URL Scheme:URL Scheme是一种类似于url的链接(boohee://goods/876898),当web前端发送URL Scheme请求之后,Native 拦截到请求并根据URL Scheme进行相关操作。注入API:通过 WebView 提供的接口,向 JavaScript 的 Context(window)中注入对象或者方法,让 JavaScript 调用时,直接执行相应的 Native 代码逻辑,达到 JavaScript 调用 Native 的目的。Native 调用 JavaScript:JavaScript暴露一个对象如JSBridge给window,让Native能直接访问。那么App内加载H5的过程是什么样的呢?App打开H5过程打开H5分为4个阶段:交互无反馈打开页面 白屏请求API,处于loading状态出现数据,正常展现这四步,对应的过程如上图所以,我们可以针对性的做性能优化。优缺点分析下面,我们进行H5的优缺点分析:优点跨平台:只要有浏览器,任何平台都可以访问开发成本低:生态成熟,学习成本低,调试方便迭代速度快:无需审核,及时响应,用户可毫无感知使用最新版缺点性能问题:在反应速度、流畅度、动画方面远不及原生功能问题:对摄像头、陀螺仪、麦克风等硬件支持较差虽然H5目前还存在不足,但随着PWA、WebAssembly等技术的进步,相信H5在未来能够得到越来也好的发展。小程序2018年是微信小程序飞速发展的一年,19年,各大厂商快速跟进,已经有了很大的影响力。下面,我们以微信小程序为例,分析小程序的技术架构。小程序跟H5一样,也是基于Webview实现。但它包含View视图层、App Service逻辑层两部分,分别独立运行在各自的WebView线程中。View可以理解为h5的页面,提供UI渲染。由WAWebview.js来提供底层的功能,具体如下:消息通信封装为WeixinJSBridge日志组件Reporter封装wx api(UI相关)小程序组件实现和注册VirtualDOM,Diff和Render UI实现页面事件触发每个窗口都有一个独立的WebView进程,因此微信限制不能打开超过5个层级的页面来保障用户体验。App Service提供逻辑处理、数据请求、接口调用。由WAService.js来提供底层的功能,具体如下:日志组件Reporter封装wx apiApp,Page,getApp,getCurrentPages等全局方法AMD模块规范的实现运行环境:iOS:JavaScriptCoreAndriod:X5内核,基于Mobile Chrome 53/57DevTool:nwjs Chrome 内核仅有一个WebView进程View & App Service通信视图层和逻辑层通过系统层的JSBridage进行通信,逻辑层把数据变化通知到视图层,触发视图层页面更新,视图层将触发的事件通知到逻辑层进行业务处理。优缺点分析优点预加载WebView,准备新页面渲染View层和逻辑层分离,通过数据驱动,不直接操作DOM使用Virtual DOM,进行局部更新组件化开发缺点仍使用WebView渲染,并非原生渲染,体验不佳不能运行在非微信环境内没有window、document对象,不能使用基于浏览器的JS库不能灵活操作 DOM,无法实现较为复杂的效果页面大小、打开页面数量都受到限制既然WebView性能不佳,那有没有更好的方案呢?下面我们看看React Native。React NativeRN的理念是在不同平台上编写基于React的代码,实现Learn once, write anywhere。Virtual DOM在内存中,可以通过不同的渲染引擎生成不同平台下的UI,JS和Native之间通过Bridge通信React Native 工作原理在 React 框架中,JSX 源码通过 React 框架最终渲染到了浏览器的真实 DOM 中,而在 React Native 框架中,JSX 源码通过 React Native 框架编译后,与Native原生的UI组件进行映射,用原生代替DOM元素来渲染,在UI渲染上非常接近Native App。### React Native 与Native平台通信React Native用JavaScriptCore作为JS的解析引擎,在Android上,需要应用自己附带JavaScriptCore,iOS上JavaScriptCore属于系统的一部分,不需要应用附带。用Bridge将JS和原生Native Code连接起来。Native和 JavaScript 两端都保存了一份配置表,里面标记了所有Native暴露给 JavaScript 的模块和方法。交互通过传递 ModuleId、MethodId 和 Arguments 进行。优缺点分析优点垮平台开发:相比原生的ios 和 android app各自维护一套业务逻辑大同小异的代码,React Native 只需要同一套javascript 代码就可以运行于ios 和 android 两个平台,在开发、测试和维护的成本上要低很多。快速编译:相比Xcode中原生代码需要较长时间的编译,React Native 采用热加载的即时编译方式,使得App UI的开发体验得到改善,几乎做到了和网页开发一样随时更改,随时可见的效果。快速发布:React Native 可以通过 JSBundle 即时更新 App。相比原来冗长的审核和上传过程,发布和测试新功能的效率大幅提高。渲染和布局更高效:React Native摆脱了WebView的交互和性能问题,同时可以直接套用网页开发中的css布局机制。脱了 autolayout 和 frame 布局中繁琐的数学计算,更加直接简便。缺点动画性能差:React Native 在动画效率和性能的支持还存在一些问题,性能上不如原生Api。不能完全屏蔽原生平台:就目前的React Native 官方文档中可以发现仍有部分组件和API都区分了Android 和 IOS 版本,即便是共享组件,也会有平台独享的函数。也就是说仍不能真正实现严格意义上的“一套代码,多平台使用”。另外,因为仍对ios 和android的原生细节有所依赖,所以需要开发者若不了解原生平台,可能会遇到一些坑。生态不完善:缺乏很多基本控件,第三方开源质量良莠不齐展望未来虽然RN还存在不足,但RN新版本已经做了如下改进,并且RN团队也在积极准备大版本重构,能否成为开发者们所信赖的跨平台方案,让我们拭目以待。改变线程模式。UI 更新不再同时需要在三个不同的线程上触发执行,而是可以在任意线程上同步调用 JavaScript 进行优先更新,同时将低优先级工作推出主线程,以便保持对 UI 的响应。引入异步渲染能力。允许多个渲染并简化异步数据处理。简化 JSBridge,让它更快、更轻量。既然React Native在渲染方面还摆脱不了原生,那有没有一种方案是直接操控GPU,自制引擎渲染呢,我们终于迎来了Flutter!FlutterFlutter是Google开发的一套全新的跨平台、开源UI框架,支持iOS、Android系统开发,并且是未来新操作系统Fuchsia的默认开发套件。渲染引擎依靠跨平台的Skia图形库来实现,依赖系统的只有图形绘制相关的接口,可以在最大程度上保证不同平台、不同设备的体验一致性,逻辑处理使用支持AOT的Dart语言,执行效率也比JavaScript高得多。Flutter架构原理Framework:由Dart实现,包括Material Design风格的Widget,Cupertino(针对iOS)风格的Widgets,文本/图片/按钮等基础Widgets,渲染,动画,手势等。此部分的核心代码是:flutter仓库下的flutter package,以及sky_engine仓库下的io,async,ui(dart:ui库提供了Flutter框架和引擎之间的接口)等package。Engine:由C++实现,主要包括:Skia,Dart和Text。Skia是开源的二维图形库,提供了适用于多种软硬件平台的通用API。其已作为Google Chrome,Chrome OS,Android, Mozilla Firefox, Firefox OS等其他众多产品的图形引擎,支持平台还包括Windows7+,macOS 10.10.5+,iOS8+,Android4.1+,Ubuntu14.04+等。Skia作为渲染/GPU后端,在Android和Fuchsia上使用FreeType渲染,在iOS上使用CoreGraphics来渲染字体。Dart部分主要包括:Dart Runtime,Garbage Collection(GC),如果是Debug模式的话,还包括JIT(Just In Time)支持。Release和Profile模式下,是AOT(Ahead Of Time)编译成了原生的arm代码,并不存在JIT部分。Text即文本渲染,其渲染层次如下:衍生自minikin的libtxt库(用于字体选择,分隔行)。HartBuzz用于字形选择和成型。Embedder:是一个嵌入层,即把Flutter嵌入到各个平台上去,这里做的主要工作包括渲染Surface设置,线程设置,以及插件等。从这里可以看出,Flutter的平台相关层很低,平台(如iOS)只是提供一个画布,剩余的所有渲染相关的逻辑都在Flutter内部,这就使得它具有了很好的跨端一致性。Dart优势很多人会好奇,为什么Flutter要用Dart,而不是用JavaScript开发,这里列下Dart的优势Dart 的性能更好。Dart在 JIT模式下,速度与 JavaScript基本持平。但是 Dart支持 AOT,当以 AOT模式运行时,JavaScript便远远追不上了。速度的提升对高帧率下的视图数据计算很有帮助。Native Binding。在 Android上,v8的 Native Binding可以很好地实现,但是 iOS上的 JavaScriptCore不可以,所以如果使用 JavaScript,Flutter 基础框架的代码模式就很难统一了。而 Dart的 Native Binding可以很好地通过 Dart Lib实现。Fuchsia OS。Fuchsia OS内置的应用浏览器就是使用 Dart语言作为 App的开发语言。优缺点分析优点性能强大:在两个平台上重写了各自的UIKit,对接到平台底层,减少UI层的多层转换,UI性能可以比肩原生优秀的语言特性:参考上面Dart优势分析路由设计优秀:Flutter的路由传值非常方便,push一个路由,会返回一个Future对象(也就是Promise对象),使用await或者.then就可以在目标路由pop,回到当前页面时收到返回值。缺点优点即缺点,Dart 语言的生态小,精通成本比较高UI控件API设计不佳与原生融合障碍很多,不利于渐进式升级总结移动互联网的普及和快速发展,跨平台技术风起云涌,这也是技术发展过程中的必经之路,等浪潮退去,才知道谁在裸泳。我个人更看好H5或类H5方案,给它一个浏览器,连“月球”都能跑,这才是真正的跨平台,其他都是浮云。广而告之本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。欢迎讨论,点个赞再走吧 。◕‿◕。 ~ ...

April 1, 2019 · 1 min · jiezi

【2019-03-29】记录过去一周看过觉得很好的文章

跨平台抓包工具,亲自测试并使用,总的来说就是配置非常简单好用,值得拥有Mobile Debug官方网站(代理抓包/移动端H5调试/请求劫持/HTTPS支持/Hosts管理/WebSocket数据捕获/跨平一个可以更好排版微信公众号工具微信公众号工具一个专门收集web的面试的栏目,很多也很全,无聊的时候刷一刷看一看自己还有那些还不会的工作日每天一道前端大厂面试题,祝大家天天进步,一年后会看到不一样的自己。本文主要是将flutter在iOS和安卓中的实践以及遇到的问题,flutter是Google的又一个跨平台框架,主要是解决安卓和iOS不同系统带来的多成本和开发效率问题,目前国内已经又不少团队入坑,有兴趣可以玩玩。flutter实战Xcode 10.2发布,是和swift 5一起发的,主要是支持swift 5的开发 ,同时也新增了不少特性,解决了大量已存在问题。xcode 10.2这是网站有很多Mac系统应用,都是免费的,如果你在使用Mac,但是有些应用的付费的你又不想出钱,不妨来这里找找看有没有替代品或者是免费的。mac 免费下载ifunmac作者认为:良好的架构和优秀的实现。就像一个大的项目会拆分成很多模块一样,想要提高自己的编程能力也要拆分成很多小模块去达成。比如你的觉得你的命名不好,代码可读性差,你就去找这方面相关的资料去针对性的学习。可以看看《编写可读代码的艺术》《Clean code》。如果你觉得自己模块抽象能力不好,学习一下面向对象、设计模式之类的。如果本身这些具体模块的好坏自己不了解,直接学习一个优质项目也是囫囵吞枣。一个优质的项目应该具有什么特点swift5版本 正式更新,这是一个大版本更新。主要有:不再包含swift标准库的动态链接”瘦身app“;新增语言特性“#”解决字符串分隔符来带的转译混乱问题;在标准库中新增了simd类型和基本操作符以及set、dictionary等等;Swift5 更新预览Dio 是一个强大的Dart Http请求库,支持Restful API、FormData、拦截器、请求取消、Cookie管理、文件上传/下载、超时、自定义适配器等。目前Dio在pub上综合得分100分,排名已上榜pub首页(All Tab下) !同时Dio也是Github上最受欢迎的Flutter第三方库,项目地址:Dio-Github。Flutter Http库Dio 2.1正式发布flutter是一个由Google开发的跨平台框架,主要是解决iOS和安卓开发时由于使用不同不的语言导致开发成本和开发效率低的问题,当然也是顺应潮流,因为大前端是web端目前的大势,如果你先学习flutter不妨到这里的flutter光网看看,写个 hello worldflutter中文官网

March 29, 2019 · 1 min · jiezi

浅谈移动端 View 的显示过程

作者:个推安卓开发工程师 一七随着科技的发展,各种移动端早已成为人们日常生活中不可或缺的部分,人们使用移动端产品工作、社交、娱乐……移动端界面的流畅性已经成为影响用户体验的重要因素之一。那么你是否思考过移动端所展现的流畅画面是如何实现的呢?本文通过对移动端View显示过程的简略分析,帮助开发者了解View渲染的逻辑,更好地优化自己的APP。上图展示的是一个完整的页面渲染过程。通过上图,我们可以初步了解每一帧页面从代码布局的编写到展示给使用者,其背后的逻辑是如何一步一步执行的。屏幕如何呈像像素点在电子屏幕中显示的图片,其实都是由一个个“小点”所组成的,这些“小点”被称为“像素点”。每一个像素点都有自己的颜色,每一张完整的图片都是由它们相连拼接形成的。每个像素点一般都有 3 个子像素:红、绿、蓝,根据这三种原色,我们能够调制出各种各样的颜色。大电视机与现在的平板电视不同的是,以前的黑白电视机或者大背投彩电,总是带着大大的“后背”。“大后背”电视其实就是阴极射线管电视机,俗称显像管电视。其成像原理是电子枪发射出的电子束(阴极射线)通过聚焦系统和偏转系统,射向屏幕上涂有荧光层的指定位置。被电子束轰击的每个位置,荧光层都会产生一个小亮点,最终小亮点们将会组成一幅幅影像,显示在电视屏幕上。这也是以前大电视机的屏幕都呈圆弧形的原因。因为越接近圆形,边长到中心的距离越相近,呈像越均匀。那为什么当磁铁贴近电视机时,会让电视机的成像出现问题呢?那是因为磁铁会干扰电子束的正常轨迹,并且在贴近屏幕的时候,也可能使得屏幕的荧光层磁化,出现一个个不正常的光斑。下图展示的是摄像机慢放后,电子束的绘制过程。LCD 和 OLED随着科技的不断进步,电视、手机、电脑的体积越来越薄,射线管显像方式也逐渐被淘汰。目前在手机市场上占据主流地位的是 LCD 和 OLED 两种屏幕。LCD 全称为 Liquid Crystal Display ,即液晶显示器。OLED 全称为 Organic Light-Emitting Diode ,即有机发光二极管。这两者之间存在显著的差别:1. 两者成像原理不同LCD 是靠白色的背光穿透彩色薄膜显色的,而 OLED 则是靠每个像素点自行发光。2. 在耗电量方面LCD的耗电量较高,即使只显示一个亮点,LCD 的背光源也需要一直发光,而且容易出现漏光现象。而OLED的每个像素都能独立工作,而且 可以自行发光,因此采用OLED的设备可以制作得更薄,甚至可以弯曲。3.在制作方面 LCD使用的是无机材料, OLED 则需要使用有机材料,因此 OLED的制作费用更高,并且使用寿命不如 LCD 。图形显示核心 GPU与CPU相对比,GPU的计算单元更多,更擅长大规模并发计算,例如密码破解、图像处理等。CPU 则是遵循冯诺依曼架构存储程序顺序执行,在大规模并行计算能力上,受到的限制更大,因此更擅长逻辑控制。应用程序编程接口 API (OpenGL)在没有统一的 API 之前,开发者需要在各式各样的图形硬件上编写各种自定义接口和驱动程序,工作量极大。1990 年 SGI(硅谷图形公司)成为了工作站 3D 图形领域的领导者,并将其 API 转变为一项开放标准,即 OpenGL。后来,SGI还促成了 OpenGL 架构审查委员会(OpenGL ARB)的创建。垂直同步 Vertical Synchronization当我们在使用手机 APP 的过程中,发现页面出现卡顿现象,那么极有可能是页面没有在 16ms 内更新导致的。实际上,人眼与大脑之间的协作无法感知超过 60fps 的画面更新。60fps 相当于是每秒 60 帧,那么每个页面需要在 1000/60 = 16ms 内更新为其他页面,才不会让我们感受到页面的卡顿。而在没有 VSync 的情况下可能会出现以下情况:如上图所示,在没有 VSync 的情况下,会出现需要显示第二帧时,其尚未处理完成的情况,因此Display 中显示的仍是第一帧。这会造成该帧显示时长超过16ms,从而导致页面卡顿的现象。为了使 CPU、GPU 生成帧的速度与 Display 保持一致,Android 系统每 16ms 就会发出一次 VSYNC 信号,触发 UI 渲染更新。从上图中我们可以看出,每隔 16ms ,安卓会发出一个 VSync 信号,收到信号后 CPU 开始处理下一帧的的内容,GPU 在 CPU 处理结束之后,将会进行光栅化,此时屏幕上显示的是上一帧已经处理完成的页面。如此反复,就可以在页面中展示一幅幅的指定画面。而确保画面流畅的前提是CPU 和 GPU 处理一帧所花费的时间不能超过 16 ms,否则就会出现以下情况:当CPU 和 GPU 处理一帧的时间超过了16 ms时,在第一个 Display 中,由于 GPU 处理 B 画面的时间过长,导致系统发出 VSync 信号时, Display不能及时地显示出 B 画面,而重复显示A页面,造成卡顿。此外,在第二个 Display 中,由于 A Buffer 还在被 Display 所使用,不能在收到 VSync 信号后开始处理下一帧的页面,导致该时间段内 CPU 的闲置。为了避免这种时间的浪费,三缓存机制由此出现:如上图所示,在三缓存机制中,当 A 缓存被 Display 使用、B 缓存被 GPU 处理时,系统会发出 Vsync 信号,并加入新的缓存 C ,用来缓存下一帧的内容。这种方式虽然不能完全避免 A页面的重复显示,但是能够让后面页面的显示更加平滑。View 的绘制流程View 的绘制是从 ViewRootImpl 的 performTraversals() 方法开始的,其整体流程大致分为三步,如下图所示:measure控件测量过程从 performMeasure() 方法开始。在该方法中childWidthMeasureSpec 和 childHeightMeasureSpec,分别是用来确定宽度和高度的。MeasureSpec 是一个 int 值,它存储着两个信息:低 30 位是 View 的 specSize,高 2 位是 View 的 specMode。specMode 有三种类型:1.UNSPECIFIED父视图对子视图没有任何限制,可以将视图按照开发者的意愿设置成任意的大小,在一般开发过程中不会用到。2.EXACTLY父视图为子视图指定一个确切的尺寸,该尺寸由 specSize 的值来决定。3.AT_MOST父视图为子视图指定一个最大的尺寸,该尺寸的最大值是 specSize。观察 View 的 measure() 方法,可以发现该方法是被 final 修饰的,因此 View 的子类只能够通过重载 onMeasure() 方法来完成自己的测量逻辑。在 onMeasure() 方法中:调用 getDefaultSize() 方法来获取视图的大小:该方法中的第二个参数 measureSpec 是从 measure() 方法中传递过来的:通过 getMode() 和 getSize() 解析获取其中对应的值,再根据 specMode 给最终的 size 赋值。不过以上只是一个简单控件的一次 measure 过程,在真正测量的过程中,由于一个页面往往包含多个子 View ,所以需要循环遍历测量,在 ViewGroup 中有一个 measureChildren() 方法,就是用来测量子视图的:measure 整体流程的方法调用链如下:layout在performTraversals() 方法的测量过程结束后,进入 layout 布局过程:performLayout(lp,desiredWindowWidth,desiredWindowHeight);该过程的主要作用即根据子视图的大小以及布局参数,将相应的 View 放到合适的位置上。host.layout(0,0,host.getMeasuredWidth(),host.getMeasuredHeight());如上,layout() 方法接收了四个参数,按照顺时针,分别是左上右下。该坐标针对的是父视图,以左上为起始点,传入了之前测量出的宽度和高度。之后,让我们进入到 layout() 方法中观察:我们通过 setFrame() 方法给四个变量赋值,判断 View 的位置是否变化以及是否需要重新进行 layout,而且其中还调用了 onLayout() 方法。在进入该方法后,我们可以发现里面是空的,这是因为子视图的具体位置是相对于父视图而言的,所以 View 的 onLayout 为空实现。再进入 ViewGroup 类中查看,我们可以发现,这其实是一个抽象的方法,在这样的情况下, ViewGroup 的子类便需要重写该方法:draw绘制的流程主要如下图所示,该流程也是存在遍历子 View 绘制的过程:需要注意的是,View 的 onDraw() 方法是空的,这是因为每个视图的内容都不相同,这个部分交由子类根据自身的需要来处理,才更加合理:安卓渲染机制的整体流程1.APP 在 UI 线程构建 OpenGL 渲染需要的命令及数据;2.CPU 将数据上传(共享或者拷贝)给 GPU 。(PC 上一般有显存,但是 ARM 这种嵌入式设备内存一般是 GPU 、 CPU 共享内存);3.通知 GPU 渲染。一般而言,真机不会阻塞等待 GPU 渲染结束,通知结束后就返回执行其他任务;4.通知 SurfaceFlinger 图层合成;5.SurfaceFlinger 开始合成图层。总结移动端技术发展很快,而画面显示优化是一个持续发展的实践课题,贯穿于每个开发者的日常工作中。未来,个推技术团队将继续关注移动端的性能优化,为大家分享相关的技术干货。 ...

March 29, 2019 · 2 min · jiezi

一文看完苹果2019春季发布会

苹果于北京时间2019年3月26日在史蒂夫·乔布斯剧院(Steve Jobs Theater)举行春季特别活动,和以往的春季发布会不同,此处的发布会上,苹果并没有发布什么重磅硬件产品,取而代之的则是新闻订阅服务和视频流媒体服务两个重要的新业务。总的来说呢,这场发布会和中国用户关系不大,因为除了游戏之外,其他此次发布的服务估计都不会在大陆推出。大体来说,此次发布的新闻订阅服务和视频流媒体服务主要涉及以下几个方面:1.苹果推出新闻服务Apple News+,支持杂志的订阅,服务费用为9.99美元/月。2.苹果推出信用卡服务Apple Card,支持每日提现,今年夏天美国上线。3.苹果推出游戏订阅服务Apple Arcade,支持超过100款独占游戏,国区上线有望。4.苹果更新Apple TV应用服务,推出Apple TV+视频流媒体,今年秋天上线。1.Apple News+Apple News 本来就是免费的,但 News+ 在 News 的基础上增加了很多精美的杂志,增加了一些本来就要付费的报纸,增加了离线阅读功能等,要获得这些高级功能需要支付 $9.99/月。Apple News +拥有超过 300 种不同的杂志,并且更加注重用户的沉浸式体验。在谈到用户隐私的时候,苹果承诺不会让广告商知道用户的阅读习惯。用户在开通了 News+ 后,用户可以阅读 News+ 上的所有付费内容,不需要给每一个出版商单独付费。Apple News+ 订阅服务已经通过 iOS 12.2 的发布而正式推出,但由于中国用户无法使用 Apple News,所以和我们无关。2.Apple Card2019年Apple Pay的交易量已经超过了100亿次,今年年底超过40个国家和地区都将支持Apple Pay。对广州地区用户还有一个好消息,那就是Apple Pay即将在年内支持羊城通交通储值卡。Apple Card 感觉很不错,苹果自称 Apple Card 完全重新定义了“信用卡”这种东西。通过Apple Card,用户注册信用卡将更加的便捷,并且在全球随时随地都可以进行使用,所有的苹果设备都可以使用Apple Card。Apple Card可以让用户更加直观的看到每天消费状况。Apple Card还支持每日提现功能,用户使用Apple Pay消费时就可以获得每日提现的额度,这个额度将会是2%。为了帮助用户养成良好的消费习惯,Apple Card可以帮助用户存下利息,并且实时显示利息和支付决策。Apple Card另一个诱人的地方在于,这项服务中没有任何的年费和其他费用。苹果将与高盛和万事达进行合作,并且在全球范围内都可以使用。安全隐私无疑是信用卡服务最让用户关心的部分。Apple Card用户可以拥有自己的卡号,只有Apple Pay才能知道你的信息,每次支付的时候都需要通过指纹或者Face ID进行识别。之所以说 Apple Card 重新定义了信用卡,是因为它有这些特点:充分保护你的隐私,不会收集你的消费数据并用于营销;实体卡上无卡号、无安全码、无签名、无过期日期,需要卡号的时候怎么办呢?可以打开钱包应用查看;没有年费,没有超额费等多种费用;消费有返现:Apple 相关消费返现 3%,用 Apple Pay 消费返现 2%,其余消费返现 1%,并且返现金额每日结算,第二天就可以用;每一笔消费都可通过地图视图展示消费地点,挺有趣的,希望微信、支付宝也支持这种功能,最好在世界地图上标志每一个城市的消费金额,年终总结的时候肯定能刷爆朋友圈…3.Apple ArcadeApple Arcade是苹果推出的一项全平台游戏订阅服务,覆盖iPhone、iPad及MacBook等多品类硬件。苹果在其中所扮演的角色类似发行商,他们会与游戏开发者/开发商合作,同时对玩家推出按时付费的订阅服务。订阅Apple Arcade,用户可以享受超过100款独占游戏。Apple Arcade没有广告和二次付费,并支持离线游玩。Apple Arcade即将于今年秋天上线,支持超过150个国家,不过价格还在商议之中。Apple Arcade公布之后,苹果中国官网就立即更新了相关内容,也意味着这项服务很有可能被引进国内。4.Apple TV+全场发布会的第四项服务是电视服务。众所周知,苹果已经很长时间没有更新过Apple TV产品线,人们也在期待着苹果的后续动作。今天的发布会上,苹果带来了Apple TV channels应用程序,而不是机顶盒。Apple TV channels应用程序将支持市面上主流的网络电视服务,用户可以选择性支付想要看的节目,定制化的使用内容,还可以免去广告的困扰。在Apple TV中也进行了更新,用户可以随时定义菜单,并且用Siri进行交互。Apple TV channels每天都可以根据用户进行推荐内容,找到用户喜欢看的电视剧或者频道。全新的Apple TV channels应用可以实时保存用户的观看记录,如果想看全新的内容也可以进行实时搜索。用户只需要轻轻一点就可以随时观看,无需任何操作。与Apple TV应用相同,Apple TV+订阅服务将没有广告,支持苹果众多的自制内容,今年秋天向全球超过100个国家进行推送。发布会结束之后,苹果立即推送了iOS 12.2版本。此次版本更新中加入了Apple News、新的Animoji、HomeKit 和 AirPlay 2 支持电视、适配AirPods 2以及Safari 更改等内容。不过,此次春季发布会没有发布什么硬件产品,相信在9月的秋季发布会会进行最近硬件产品的发布。 ...

March 27, 2019 · 1 min · jiezi

ViewController生命周期

ARC环境单个viewController的生命周期initWithCoder:(NSCoder *)aDecoder:(如果使用storyboard或者xib)loadView:加载viewviewDidLoad:view加载完毕viewWillAppear:控制器的view将要显示viewWillLayoutSubviews:控制器的view将要布局子控件viewDidLayoutSubviews:控制器的view布局子控件完成这期间系统可能会多次调用viewWillLayoutSubviews、viewDidLayoutSubviews俩个方法viewDidAppear:控制器的view完全显示viewWillDisappear:控制器的view即将消失的时候这期间系统也会调用viewWillLayoutSubviews 、viewDidLayoutSubviews 两个方法viewDidDisappear:控制器的view完全消失的时候多个viewControllers跳转当我们点击push的时候首先会加载下一个界面然后才会调用界面的消失方法initWithCoder:(NSCoder *)aDecoder:ViewController2 (如果用xib创建的情况下)loadView:ViewController2viewDidLoad:ViewController2viewWillDisappear:ViewController1 将要消失viewWillAppear:ViewController2 将要出现viewWillLayoutSubviews ViewController2viewDidLayoutSubviews ViewController2viewWillLayoutSubviews:ViewController1viewDidLayoutSubviews:ViewController1viewDidDisappear:ViewController1 完全消失viewDidAppear:ViewController2 完全出现小结整个控制器声明周期:viewDidLoadviewWillAppearviewWillLayoutSubviewsviewDidLayoutSubviewsviewDidAppearviewWillDisappearviewDidDisappear二、非ARC环境下didReceiveMemoryWarning:当app收到内存警告的时候会发消息给视图控制器。app从来不会直接调用这个方法,而是当系统确定可用内存不足的时候采取调用。如果你想覆写这个方法来释放一些控制器使用的额外内存,你应该在你的实现方法中调用父类的实现方法。

March 25, 2019 · 1 min · jiezi

Objective-C 中的消息与消息转发

大家都知道OC是一门动态语言,其动态性由底层的runtime库来支撑实现。OC所有的方法都是通过runtime来发送消息,当我们探讨消息发送,其实也就是在探讨OC方法的调用过程。[receiver message];这是我们很熟悉的一个OC方法的调用,大家都知道这个方法最终会被编译器转换为消息发送函数objc_msgSend(receiver, @selector(message));首先声明咱们这篇文章不去讲解具体的class的数据结构一类的细节问题,我们主要关注的是这个过程。很遗憾,objc_msgSend的实现是用汇编写的,我并不能看懂。但是从runtime的源码中我发现了一个关键的方法://查找方法的实现extern IMP lookUpImpOrForward(Class, SEL, id obj, bool initialize, bool cache, bool resolver);我们要执行一个方法,其实最重要的就是找到这个方法的实现。下面我们来看一下这个lookUpImpOrForward函数的源码。lookUpImpOrForward通过源码中的注释可以看出来,主要的流程分为以下几个阶段:无锁状态下从缓存中查找方法的实现runtimeLock.assertUnlocked();// Optimistic cache lookupif (cache) { imp = cache_getImp(cls, sel); if (imp) return imp;}类的实现和初始化// 判断类是否已经实现if (!cls->isRealized()) { realizeClass(cls);} // 是否初始化if (initialize && !cls->isInitialized()) { runtimeLock.unlock(); _class_initialize (_class_getNonMetaClass(cls, inst)); runtimeLock.lock();在执行这两段代码之前还有两行代码,一个是runtimeLock.lock();,注释里面解释是通过加锁在遇到并发实现类的时候保护实现的过程。另一个是checkIsKnownClass(cls);。看名字是检测这个类是不是我们已知的类,什么情况下可能出现我们未知的类呢?我猜测是通过NSClassFromString()来获取类的时候,如果class的string写错了,就会生成一个未知的类,这时候肯定找不到方法的实现,直接就crash掉行了。我们的Class在底层其实是一个名为objc_class的结构体,大家可以去源码中看一下,这里我们不细说。执行realizeClass方法其实等于对类的第一次初始化,包括配置类的读写空间(class_rw_t)并且返回类的正确的结构体,就相当于搭好了这个类的框架。_class_initialize则是让类执行我们熟悉的+initialize方法。加锁,查找加锁下面就要开始真正的IMP查找了,查找之前有一个runtimeLock.assertLocked();加锁。注释中给出了解释,runtimeLock在查找方法的时候加锁,是为了保持method-lookup(查找方法)和 cache-fill(缓存填充)这两种方法的原子性,// Otherwise, a category could be added but ignored indefinitely because// the cache was re-filled with the old value after the cache flush on// behalf of the category.这几行注释我没读明白啥意思,感觉是说如果不加锁,有添加category的情况时,会导致缓存被冲洗掉。在类中查找通过代码可以看出,首先依然是从缓存中查找,然后在当前的类中查找,最后通过一个循环来从各级父类中查找。这部分代码我们就不展示出来了,大家自己可以看源码,如果找到了这个方法的实现,我们看到会调用log_and_fill_cache这样一个方法,其实就是把这次查找缓存起来,方便下次使用。如果没有找到就进入到下一个环节。动态决议这个名称大家应该是耳熟的,我们都知道消息转发机制,也就是说在我们调用了一个未实现的方法时,并不会直接crash掉,然后报unrecognized selector sent to instance错误。我们的系统会给我们两次机会,第一次就是动态决议。if (resolver && !triedResolver) { runtimeLock.unlock(); _class_resolveMethod(cls, sel, inst); runtimeLock.lock(); // Don’t cache the result; we don’t hold the lock so it may have // changed already. Re-do the search from scratch instead. triedResolver = YES; goto retry;}在OC层面最直接的表现形式就是看我们是否实现了+ (BOOL)resolveClassMethod:(SEL)sel或者+ (BOOL)resolveInstanceMethod:(SEL)sel方法(对应类方法和实例方法)。void _class_resolveMethod(Class cls, SEL sel, id inst){ if (! cls->isMetaClass()) {//不是元类 // try [cls resolveInstanceMethod:sel] _class_resolveInstanceMethod(cls, sel, inst); } else { // try [nonMetaClass resolveClassMethod:sel] // and [cls resolveInstanceMethod:sel] _class_resolveClassMethod(cls, sel, inst); if (!lookUpImpOrNil(cls, sel, inst, NO/initialize/, YES/cache/, NO/resolver/)) { _class_resolveInstanceMethod(cls, sel, inst); } }}根据cls是否为元类来调用_class_resolveInstanceMethod或_class_resolveClassMethod。/************************************************************************ lookUpImpOrNil.* Like lookUpImpOrForward, but returns nil instead of _objc_msgForward_impcache**********************************************************************/IMP lookUpImpOrNil(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver){ IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver); // 这个imp或者是一个正经的IMP指针,或者是一个汇编的入口 //_objc_msgForward_impcache 是一个汇编的入口 (看名字应该是消息转发相关的) // 本方法是不进行消息转发的 ~ 所以如果获取到的IMP是这个入口,就直接return nil if (imp == _objc_msgForward_impcache) return nil; else return imp;}通过lookUpImpOrNil的源码我们知道,其实它封装了lookUpImpOrForward,不进行lookUpImpOrForward中最后的消息转发这一步,而且在这个if中,参数resolver传入了NO,也就是动态决议这一步也不进行,只进行了方法在本类和各级父类中的查找,如果找不到,则跟非元类一样执行_class_resolveInstanceMethod。else中为什么要再执行这一段if代码,我不是很理解。具体的动态决议实现的代码,我们看其中一个就行来看,用_class_resolveInstanceMethod当例子吧static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst){ if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, NO/initialize/, YES/cache/, NO/resolver/)) { // Resolver not implemented. // 没有找到SEL_resolveInstanceMethod(resolveInstanceMethod)方法 return; } // 下面应该是类型转换的代码而已吧? BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend; // 执行实现的+resolveInstanceMethod方法 bool resolved = msg(cls, SEL_resolveInstanceMethod, sel); // Cache the result (good or bad) so the resolver doesn’t fire next time. // +resolveInstanceMethod adds to self a.k.a. cls //通过+resolveInstanceMethod动态添加了方法,在进行一次查找 IMP imp = lookUpImpOrNil(cls, sel, inst, NO/initialize/, YES/cache/, NO/resolver/); if (resolved && PrintResolving) { if (imp) { _objc_inform(“RESOLVE: method %c[%s %s] " “dynamically resolved to %p”, cls->isMetaClass() ? ‘+’ : ‘-’, cls->nameForLogging(), sel_getName(sel), imp); } else { // Method resolver didn’t add anything? _objc_inform(“RESOLVE: +[%s resolveInstanceMethod:%s] returned YES” “, but no new implementation of %c[%s %s] was found”, cls->nameForLogging(), sel_getName(sel), cls->isMetaClass() ? ‘+’ : ‘-’, cls->nameForLogging(), sel_getName(sel)); } }}一开始先通过调用lookUpImpOrNil来查找是否已经实现了+resolveInstanceMethod,没有实现就直接返回了。如果实现了,通过objc_msgSend来执行实现的+resolveInstanceMethod方法。我们在+resolveInstanceMethod方法中都是动态的添加方法,所以在执行完之后在进行一次查找。消息转发如果在动态决议之后,依然没有找到方法,那我们还有最后一次机会,那就是消息转发。imp = (IMP)_objc_msgForward_impcache;cache_fill(cls, sel, imp, inst);很遗憾_objc_msgForward_impcache汇编实现看不懂。不过我们还是可以从另一个方向来研究消息转发到底做了什么。先补充一个骚操作,这也是我这两天刚学到的,就是打印runtime的代码执行日志,这么说可能不太贴切,反正就是能看到我们执行一个方法的过程中调用了哪些方法。想要开始打印的地方加上下面代码extern void instrumentObjcMessageSends(BOOL);instrumentObjcMessageSends(YES);想要关闭的地方加上下面代码extern void instrumentObjcMessageSends(BOOL);instrumentObjcMessageSends(NO);当然还有别的方式,大家可以去查一下。首先我们新建一个工程,类型就选择macOS->Common Line Tool。因为我用iOS的工程不管用。main函数中这样写#import <Foundation/Foundation.h>#import <objc/runtime.h>#import “Sark.h"int main(int argc, const char * argv[]) { @autoreleasepool { extern void instrumentObjcMessageSends(BOOL); instrumentObjcMessageSends(YES); Sark * test = [[Sark alloc] init]; [test performSelector:@selector(xxx)]; instrumentObjcMessageSends(NO); } return 0;}创建SEL选择子的时候我们故意穿进去一个xxx,就是为了让类找不到方法,然后走消息转发的的这个过程。执行一下代码,运行时发送的所有消息都会打印到/private/tmp/msgSend-xxxx文件里了。(这是电脑系统的路径)如果找起来不方便可以直接使用下面的命令行打开该文件。open /private/tmp/从该路径的msgSend-xxx文件中我们找到了这么一部分代码+ Sark NSObject initialize+ Sark NSObject alloc- Sark NSObject init- Sark NSObject performSelector:+ Sark NSObject resolveInstanceMethod:+ Sark NSObject resolveInstanceMethod:- Sark NSObject forwardingTargetForSelector:- Sark NSObject forwardingTargetForSelector:- Sark NSObject methodSignatureForSelector:- Sark NSObject methodSignatureForSelector:- Sark NSObject class- Sark NSObject doesNotRecognizeSelector:- Sark NSObject doesNotRecognizeSelector:- Sark NSObject class我们可以看到在执行完了resolveInstanceMethod之后又执行forwardingTargetForSelector:和methodSignatureForSelector:,最后才因为找不到方法执行doesNotRecognizeSelector:。现在我们可以大概了解在消息转发的过程中执行了哪些方法了,经过查阅资料我们得出:全部的消息转发过程做了如下几件事:(包括动态决议+消息转发)调用resolveInstanceMethod:方法,允许用户在此时为该Class动态添加实现。如果有实现了,则调用并返回。如果仍没实现,继续下面的动作。调用forwardingTargetForSelector:方法,尝试找到一个能响应该消息的对象。如果获取到,则直接转发给它。如果返回了nil,继续下面的动作。调用methodSignatureForSelector:方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。调用forwardInvocation:方法,将地3步获取到的方法签名包装成Invocation传入,如何处理就在这里面了。上面这4个方法均是模板方法,开发者可以override,由runtime来调用。具体如何重写使用这几个方法,大家可以自己查一下。参考资料https://blog.ibireme.com/2013…https://github.com/draveness/… ...

March 20, 2019 · 2 min · jiezi

重磅推出TabLayout高级窗口组件

TabLayout是在APICloud现有窗口系统基础上升级而来的高级窗口组件,符合Material Design规范,可通过简单的配置为窗口实现原生的导航栏和TabBar,它将帮助您节省30%以上的重复编码工作量,同时为APP节省50%以上的系统资源开销,带来APP页面打开速度、应用性能上的整体提升,助您更快速的开发精美APP。使用tabLayout主要优点1、减少代码,提升开发效率使用tabLayout只需要简单配置参数即可实现首页tabBar+frameGroup的整体布局,不用在window页面中书写header、footer标签和css样式来实现导航栏、标签栏,同时也不用考虑适配状态栏和虚拟home键。因此可以将更多时间花在具体业务的实现上面,从而提高开发效率。2、加快打开页面速度,提升应用性能使用tabLayout来实现导航栏时,由于导航栏是原生实现的,那么只需要打开一个window页面来实现内容页,相较于之前window+frame的结构,减少了一个webView的开销,因此大大提高了页面打开速度,并且减少了应用的内存占用。tabLayout相关的方法请参考API文档,下面介绍tabLayout的基本使用:◆◆实现导航栏navigationBar效果◆◆tabLayout封装了原生的导航栏,可以方便地实现头部效果,导航栏会自动适配屏幕状态栏和沉浸式。实现的代码只需要简单的几行:function openNavWin(){var param = { name: ’nav’, url: ‘./main_content.html’, bgColor: ‘#fff’, title: ’navigationBar’, navigationBar: { rightButtons: [{ iconPath: “widget://image/more.png” }] }}api.openTabLayout(param);}对于导航栏上面按钮的点击事件,则可以在打开的页面中通过监听事件进行处理:function apiready(){api.addEventListener({ name: ’navbackbtn’}, function(ret, err) { alert(‘点击了返回按钮’); api.closeWin();});api.addEventListener({ name: ’navitembtn’}, function(ret, err) { if (ret.type == ‘right’) { alert(‘点击了右边按钮’); }});}◆◆实现tabBar效果◆◆tabLayout将tabBar控件和frameGroup结合到了一起,tabLayout会自动管理tabBar项和对应的页面,同时tabBar会自动适配底部虚拟home键。实现的代码如下:function openNavTabWin(){var param = { name: ’nav-tab’, title:’nav-tab’, bgColor:’#fff’, slidBackEnabled: false, navigationBar: { hideBackButton: true }, tabBar: { animated: true, list: [ { text: “微信”, iconPath: “widget://image/nav_tab_1.png”, selectedIconPath: “widget://image/nav_tab_1_on.png” }, { text: “通讯录”, iconPath: “widget://image/nav_tab_2.png”, selectedIconPath: “widget://image/nav_tab_2_on.png” }, { text: “发现”, iconPath: “widget://image/nav_tab_3.png”, selectedIconPath: “widget://image/nav_tab_3_on.png” }, { text: “我”, iconPath: “widget://image/nav_tab_4.png”, selectedIconPath: “widget://image/nav_tab_4_on.png” } ], frames: [ { title: “微信”, name: “tab_frm_1”, url: “widget://html/tab_content_1.html” }, { title: “通讯录”, name: “tab_frm_2”, url: “widget://html/tab_content_2.html” }, { title: “发现”, name: “tab_frm_3”, url: “widget://html/tab_content_3.html” }, { title: “我”, name: “tab_frm_4”, url: “widget://html/tab_content_4.html” } ] }}api.openTabLayout(param);}如果需要在点击tabBar项后做其它的处理,可以监听tabitembtn事件进行处理,监听点击事件后tabBar将不会自动切换页面,需要调用setTabBarAttr方法进行切换。function apiready(){api.addEventListener({ name:’tabitembtn’}, function(ret) { console.log(‘点击了第’+(ret.index+1)+‘项’); api.setTabBarAttr({ index: ret.index });});}打开tabBar后,可以为tabBar上面的各项设置角标,如:function setTabBarItemDot(){api.setTabBarItemAttr({ index: 2, badge: { text: ‘’, radius: 5, x: 8 }});} ...

March 19, 2019 · 1 min · jiezi

项目研发流程

一般的项目研发流程

March 16, 2019 · 1 min · jiezi

原生骨架库模版功能上线,零耦合。

前言前文章地址首先,原有的骨架库实现的大概思路:如果你开启了动画,框架会根据view内的所有subViews的位置,映射出一组一模一样的CALayer动画,并进行管理。目录技术瓶颈模版功能 - 展示模版功能 - 使用方式模版功能 - 其他细节技术瓶颈如果使用约束进行布局,例如知名的第三方库Masonry布局,大部分只需要2个约束就可以很好地布局。但是, 2个约束就可以很好地布局是在数据已经填充的前提下,如果没有数据,则frame信息是完全不对。因此,映射不出合理的动画。本框架采用的是AOP编程,最初地思想是开发者尽量不需要动自己原有的代码,就可以完成动画的设置。但是,当你使用后会发现,会于原代码产生一定耦合,不会利于他人阅读和维护。我们将骨架展示给用户时,大部分情况是这样的:可能并不需要很复杂的view,子view并不需要完全展示给用户可能是很个性化的view(因为映射出的动画,并不能保证好看,又需要调试)可能是通用的view,很多地方共用一个就行了那么模版功能特别适合你。模版功能 - 展示交流群TABAniamted交流群:304543771提出你的意见模版功能 - 使用方式模版功能是库的一个新功能,并不是一个新的库。模版功能只针对常用的表格组件。开启和结束动画的方式不变唯一的改变就是在表格初始化的时候,注册模版,如下- (UICollectionView *)collectionView { if (!_collectionView) { UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; _collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, kNavigationHeight, kScreenWidth, kScreenHeight-kNavigationHeight) collectionViewLayout:layout]; _collectionView.backgroundColor = [UIColor whiteColor]; _collectionView.dataSource = self; _collectionView.delegate = self; _collectionView.animatedDelegate = self; _collectionView.showsHorizontalScrollIndicator = NO; _collectionView.showsVerticalScrollIndicator = NO; // 注册模版 [_collectionView registerTemplateClassArray:@[[TemplateCollectionViewCell class], [TemplateSecondCollectionViewCell class]]]; } return _collectionView;}模版功能 - 其他细节cell模版需要自己写,布局写死,想什么样就什么样但需要继承自TABBaseCollectionViewCell或TABBaseTableViewCell以table举例,TABBaseTableViewCell中的cellHeight方法,需要你在子类重写,并指定数值,这个返回值就是改模版在动画是展示的高度+ (NSNumber *)cellHeight { return [NSNumber numberWithFloat:10+headImgWidth+5+80+10+imgWidth];}模版功能依旧根据animationType设置动画类型使用isUseTemplate属性切换为模版模式,可以在动画开启前随意切换。模版中的组件,使用经典类型的动画,依旧需要指定动画类型提供两种方式注册模版,一个section和多section,多个section是以一个class数组形式储存。言外之意,数组中的模版类和section一一对应。- (void)registerTemplateClassArray:(NSArray <Class> *)classArray;最后如有问题,加入交流群:304543771github地址:https://github.com/tigerAndBu… ...

March 15, 2019 · 1 min · jiezi

iOS 使用CoreImage进行人脸识别

2018-09-04更新: 很久没有更新文章了,工作之余花时间看了之前写的这篇文章并运行了之前写的配套Demo,通过打印人脸特征CIFaceFeature的属性,发现识别的效果并不是很好,具体说明见文章最底部的更新标题,后续我将分别用OpenCV(跨平台计算机视觉库) 和 Vision (iOS 11新API)两种库实现人脸面部识别,敬请期待~~OC版下载地址, swift版下载地址CoreImage是Cocoa Touch中一个强大的API,也是iOS SDK中的关键部分,不过它经常被忽视。在本篇教程中,我会带大家一起验证CoreImage的人脸识别特性。在开始之前,我们先要简单了解下CoreImage framework 组成CoreImage framework组成Apple 已经帮我们把image的处理分类好,来看看它的结构:主要分为三个部分:1.定义部分:CoreImage 和CoreImageDefines。见名思义,代表了CoreImage 这个框架和它的定义。2.操作部分:滤镜(CIFliter):CIFilter 产生一个CIImage。典型的,接受一到多的图片作为输入,经过一些过滤操作,产生指定输出的图片。检测(CIDetector):CIDetector 检测处理图片的特性,如使用来检测图片中人脸的眼睛、嘴巴、等等。特征(CIFeature):CIFeature 代表由 detector处理后产生的特征。3.图像部分:画布(CIContext):画布类可被用与处理Quartz 2D 或者 OpenGL。可以用它来关联CoreImage类。如滤镜、颜色等渲染处理。颜色(CIColor): 图片的关联与画布、图片像素颜色的处理。向量(CIVector): 图片的坐标向量等几何方法处理。图片(CIImage): 代表一个图像,可代表关联后输出的图像。在了解上述基本知识后,我们开始通过创建一个工程来带大家一步步验证Core Image的人脸识别特性。将要构建的应用iOS的人脸识别从iOS 5(2011)就有了,不过一直没怎么被关注过。人脸识别API允许开发者不仅可以检测人脸,也可以检测到面部的一些特殊属性,比如说微笑或眨眼。首先,为了了解Core Image的人脸识别技术我们会创建一个app来识别照片中的人脸并用一个方框来标记它。在第二个demo中,让用户拍摄一张照片,检测其中的人脸并检索人脸位置。这样一来,就充分掌握了iOS中的人脸识别,并且学会如何利用这个强大却总被忽略的API。话不多说,开搞!建立工程(我用的是Xcode8.0)这里提供了初始工程,当然你也可以自己创建(主要是为了方便大家)点我下载 用Xcode打开下载后的工程,可以看到里面只有一个关联了IBOutlet和imageView的StoryBoard。使用CoreImage识别人脸在开始工程中,故事板中的imageView组件与代码中的IBOutlet已关联,接下来要编写实现人脸识别的代码部分。在ViewController.swift文件中写下如下代码:import UIKitimport CoreImage // 引入CoreImageclass ViewController: UIViewController { @IBOutlet weak var personPic: UIImageView! override func viewDidLoad() { super.viewDidLoad() personPic.image = UIImage(named: “face-1”) // 调用detect detect() } //MARK: - 识别面部 func detect() { // 创建personciImage变量保存从故事板中的UIImageView提取图像并将其转换为CIImage,使用Core Image时需要用CIImage guard let personciImage = CIImage(image: personPic.image!) else { return } // 创建accuracy变量并设为CIDetectorAccuracyHigh,可以在CIDetectorAccuracyHigh(较强的处理能力)与CIDetectorAccuracyLow(较弱的处理能力)中选择,因为想让准确度高一些在这里选择CIDetectorAccuracyHigh let accuracy = [CIDetectorAccuracy: CIDetectorAccuracyHigh] // 这里定义了一个属于CIDetector类的faceDetector变量,并输入之前创建的accuracy变量 let faceDetector = CIDetector(ofType: CIDetectorTypeFace, context: nil, options: accuracy) // 调用faceDetector的featuresInImage方法,识别器会找到所给图像中的人脸,最后返回一个人脸数组 let faces = faceDetector?.features(in: personciImage) // 循环faces数组里的所有face,并将识别到的人脸强转为CIFaceFeature类型 for face in faces as! [CIFaceFeature] { print(“Found bounds are (face.bounds)”) // 创建名为faceBox的UIView,frame设为返回的faces.first的frame,绘制一个矩形框来标识识别到的人脸 let faceBox = UIView(frame: face.bounds) // 设置faceBox的边框宽度为3 faceBox.layer.borderWidth = 3 // 设置边框颜色为红色 faceBox.layer.borderColor = UIColor.red.cgColor // 将背景色设为clear,意味着这个视图没有可见的背景 faceBox.backgroundColor = UIColor.clear // 最后,把这个视图添加到personPic imageView上 personPic.addSubview(faceBox) // API不仅可以帮助你识别人脸,也可识别脸上的左右眼,我们不在图像中标识出眼睛,只是给你展示一下CIFaceFeature的相关属性 if face.hasLeftEyePosition { print(“Left eye bounds are (face.leftEyePosition)”) } if face.hasRightEyePosition { print(“Right eye bounds are (face.rightEyePosition)”) } } }}编译并运行app,结果应如下图所示:2.png根据控制台的输出来看,貌似识别器识别到了人脸:Found bounds are (314.0, 243.0, 196.0, 196.0)当前的实现中没有解决的问题:人脸识别是在原始图像上进行的,由于原始图像的分辨率比image view要高,因此需要设置image view的content mode为aspect fit(保持纵横比的情况下缩放图片)。为了合适的绘制矩形框,需要计算image view中人脸的实际位置与尺寸还要注意的是,CoreImage与UIView使用两种不同的坐标系统(看下图),因此要实现一个CoreImage坐标到UIView坐标的转换。UIView坐标系:CoreImage坐标系:现在使用下面的代码替换detect()方法:func detect1() { guard let personciImage = CIImage(image: personPic.image!) else { return } let accuracy = [CIDetectorAccuracy: CIDetectorAccuracyHigh] let faceDetector = CIDetector(ofType: CIDetectorTypeFace, context: nil, options: accuracy) let faces = faceDetector?.features(in: personciImage) // 转换坐标系 let ciImageSize = personciImage.extent.size var transform = CGAffineTransform(scaleX: 1, y: -1) transform = transform.translatedBy(x: 0, y: -ciImageSize.height) for face in faces as! [CIFaceFeature] { print(“Found bounds are (face.bounds)”) // 应用变换转换坐标 var faceViewBounds = face.bounds.applying(transform) // 在图像视图中计算矩形的实际位置和大小 let viewSize = personPic.bounds.size let scale = min(viewSize.width / ciImageSize.width, viewSize.height / ciImageSize.height) let offsetX = (viewSize.width - ciImageSize.width * scale) / 2 let offsetY = (viewSize.height - ciImageSize.height * scale) / 2 faceViewBounds = faceViewBounds.applying(CGAffineTransform(scaleX: scale, y: scale)) faceViewBounds.origin.x += offsetX faceViewBounds.origin.y += offsetY let faceBox = UIView(frame: faceViewBounds) faceBox.layer.borderWidth = 3 faceBox.layer.borderColor = UIColor.red.cgColor faceBox.backgroundColor = UIColor.clear personPic.addSubview(faceBox) if face.hasLeftEyePosition { print(“Left eye bounds are (face.leftEyePosition)”) } if face.hasRightEyePosition { print(“Right eye bounds are (face.rightEyePosition)”) } }}上述代码中,首先使用仿射变换(AffineTransform)将Core Image坐标转换为UIKit坐标,然后编写了计算实际位置与矩形视图尺寸的代码。再次运行app,应该会看到人的面部周围会有一个框。OK,你已经成功使用Core Image识别出了人脸。但是有的童鞋在使用了上面的代码运行后可能会出现方框不存在(即没有识别人脸)这种情况,这是由于忘记关闭Auto Layout以及Size Classes了。 选中storyBoard中的ViewController,选中view下的imageView。然后在右边的面板中的第一个选项卡中找到use Auto Layout ,将前面的✔️去掉就可以了经过上面的设置后我们再次运行App,就会看到图三出现的效果了。构建一个人脸识别的相机应用想象一下你有一个用来照相的相机app,照完相后你想运行一下人脸识别来检测一下是否存在人脸。若存在一些人脸,你也许想用一些标签来对这些照片进行分类。我们不会构建一个保存照片后再处理的app,而是一个实时的相机app,因此需要整合一下UIImagePicker类,在照完相时立刻进行人脸识别。在开始工程中已经创建好了CameraViewController类,使用如下代码实现相机的功能:class CameraViewController: UIViewController, UIImagePickerControllerDelegate, UINavigationControllerDelegate { @IBOutlet var imageView: UIImageView! let imagePicker = UIImagePickerController() override func viewDidLoad() { super.viewDidLoad() imagePicker.delegate = self } @IBAction func takePhoto(_ sender: AnyObject) { if !UIImagePickerController.isSourceTypeAvailable(.camera) { return } imagePicker.allowsEditing = false imagePicker.sourceType = .camera present(imagePicker, animated: true, completion: nil) } func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) { if let pickedImage = info[UIImagePickerControllerOriginalImage] as? UIImage { imageView.contentMode = .scaleAspectFit imageView.image = pickedImage } dismiss(animated: true, completion: nil) self.detect() } func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { dismiss(animated: true, completion: nil) }}前面几行设置UIImagePicker委托为当前视图类,在didFinishPickingMediaWithInfo方法(UIImagePicker的委托方法)中设置imageView为在方法中所选择的图像,接着返回上一视图调用detect函数。还没有实现detect函数,插入下面代码并分析一下func detect() { let imageOptions = NSDictionary(object: NSNumber(value: 5) as NSNumber, forKey: CIDetectorImageOrientation as NSString) let personciImage = CIImage(cgImage: imageView.image!.cgImage!) let accuracy = [CIDetectorAccuracy: CIDetectorAccuracyHigh] let faceDetector = CIDetector(ofType: CIDetectorTypeFace, context: nil, options: accuracy) let faces = faceDetector?.features(in: personciImage, options: imageOptions as? [String : AnyObject]) if let face = faces?.first as? CIFaceFeature { print(“found bounds are (face.bounds)”) let alert = UIAlertController(title: “提示”, message: “检测到了人脸”, preferredStyle: UIAlertControllerStyle.alert) alert.addAction(UIAlertAction(title: “确定”, style: UIAlertActionStyle.default, handler: nil)) self.present(alert, animated: true, completion: nil) if face.hasSmile { print(“face is smiling”); } if face.hasLeftEyePosition { print(“左眼的位置: (face.leftEyePosition)”) } if face.hasRightEyePosition { print(“右眼的位置: (face.rightEyePosition)”) } } else { let alert = UIAlertController(title: “提示”, message: “未检测到人脸”, preferredStyle: UIAlertControllerStyle.alert) alert.addAction(UIAlertAction(title: “确定”, style: UIAlertActionStyle.default, handler: nil)) self.present(alert, animated: true, completion: nil) }}这个detect()函数与之前实现的detect函数非常像,不过这次只用它来获取图像不做变换。当识别到人脸后显示一个警告信息“检测到了人脸!”,否则显示“未检测到人脸”。运行app测试一下:我们已经使用到了一些CIFaceFeature的属性与方法,比如,若想检测人物是否微笑,可以调用.hasSmile,它会返回一个布尔值。可以分别使用.hasLeftEyePosition与.hasRightEyePosition检测是否存在左右眼。同样,可以调用hasMouthPosition来检测是否存在嘴,若存在则可以使用mouthPosition属性,如下所示:if (face.hasMouthPosition) { print(“mouth detected”)}如你所见,使用Core Image来检测面部特征是非常简单的。除了检测嘴、笑容、眼睛外,也可以调用leftEyeClosed与rightEyeClosed检测左右眼是否睁开,这里就不在贴出代码了。总结在这篇教程中尝试了CoreImage的人脸识别API与如何在一个相机app中应用它,构建了一个简单的UIImagePicker来选取照片并检测图像中是否存在人物。如你所见,Core Image的人脸识别是个强大的API!希望这篇教程能给你提供一些关于这个鲜为人知的iOS API有用的信息。点击swift版地址,OC版地址下载最终工程, 如果觉得对您有帮助的话,请帮我点个星星哦,您的星星是对我最大的支持。(__) 嘻嘻……更新:很久没有更新文章了,工作之余花时间回顾了之前写的这篇文章并运行了之前写的配套Demo,通过打印人脸特征CIFaceFeature的属性(如下),发现识别的效果并不是很好,如下图:人脸特征CIFaceFeature的属性/ CIDetector发现的脸部特征。 所有的位置都是相对于原始图像. /NS_CLASS_AVAILABLE(10_7, 5_0)@interface CIFaceFeature : CIFeature{ CGRect bounds; BOOL hasLeftEyePosition; CGPoint leftEyePosition; BOOL hasRightEyePosition; CGPoint rightEyePosition; BOOL hasMouthPosition; CGPoint mouthPosition; BOOL hasTrackingID; int trackingID; BOOL hasTrackingFrameCount; int trackingFrameCount; BOOL hasFaceAngle; float faceAngle; BOOL hasSmile; BOOL leftEyeClosed; BOOL rightEyeClosed;}/* coordinates of various cardinal points within a face. 脸部各个基点的坐标。 Note that the left eye is the eye on the left side of the face from the observer’s perspective. It is not the left eye from the subject’s perspective.请注意,左眼是脸左侧的眼睛从观察者的角度来看。 这不是左眼主体的视角. /@property (readonly, assign) CGRect bounds; // 指示图像坐标中的人脸位置和尺寸的矩形。@property (readonly, assign) BOOL hasLeftEyePosition; // 指示检测器是否找到了人脸的左眼。@property (readonly, assign) CGPoint leftEyePosition; // 左眼的坐标@property (readonly, assign) BOOL hasRightEyePosition; // 指示检测器是否找到了人脸的右眼。@property (readonly, assign) CGPoint rightEyePosition; // 右眼的坐标@property (readonly, assign) BOOL hasMouthPosition; // 指示检测器是否找到了人脸的嘴部@property (readonly, assign) CGPoint mouthPosition; // 嘴部的坐标@property (readonly, assign) BOOL hasTrackingID; // 指示面部对象是否具有跟踪ID。/* * 关于trackingID: * coreImage提供了在视频流中检测到的脸部的跟踪标识符,您可以使用该标识符来识别在一个视频帧中检测到的CIFaceFeature对象是在先前视频帧中检测到的同一个脸部。 * 只有在框架中存在人脸并且不与特定人脸相关联时,该标识符才会一直存在。如果脸部移出视频帧并在稍后返回到帧中,则分配另一个ID。 (核心图像检测面部,但不识别特定的面部。) * 这个有点抽象 */@property (readonly, assign) int trackingID;@property (readonly, assign) BOOL hasTrackingFrameCount; // 指示面部对象的布尔值具有跟踪帧计数。@property (readonly, assign) int trackingFrameCount; // 跟踪帧计数@property (readonly, assign) BOOL hasFaceAngle; // 指示是否有关于脸部旋转的信息可用。@property (readonly, assign) float faceAngle; // 旋转是以度数逆时针测量的,其中零指示在眼睛之间画出的线相对于图像方向是水平的。@property (readonly, assign) BOOL hasSmile; // 是否有笑脸@property (readonly, assign) BOOL leftEyeClosed; // 左眼是否闭上@property (readonly, assign) BOOL rightEyeClosed; // 右眼是否闭上问题:那么如何让人脸识别的效果更好呢? 如何让面部识别点更加精确呢?有没有别的方法呢? 答案是肯定的。现在市面上有很多成熟的面部识别产品:Face++, 收费Video++,收费ArcFace 虹软人脸认知引擎, 收费百度云人脸识别, 收费阿里云识别, 收费等等, 我们看到都是收费的。 当然这些sdk是可以试用的。如果你有折腾精神,想自己尝试人脸识别的实现,我们可以一起交流。 毕竟市面上的这些sdk也不是一开始就有的, 也是通过人们不断研究开发出来的。 而且自己折腾过程中,通过不断地遇坑爬坑,对知识的理解更加深透,自己的技术也会有增进,不是吗? 不好意思,有点扯远了。Core Image只是简单的图像识别, 并不能对流中的人脸进行识别。 它只适合对图片的处理。比较有名的OpenCV(跨平台计算机视觉库)就可以用来进行面部识别,识别精度自然很高。还有就是iOS 11.0+ 推出的Vision框架(让我们轻松访问苹果的模型,用于面部检测、面部特征点、文字、矩形、条形码和物体)也可以进行面部识别。后面我将会用这两个框架讲解如何进行面部识别。敬请期待!!! ...

March 14, 2019 · 4 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

iOS证书申请流程备忘

1、现在本地电脑生成 CertificateSigningRequest.certSigningRequest证书注意常用名称必填,否则会在上传证书的时候提示无效证书2、为应用添加AppIDs比较简单:选择生成Explicit App ID即可,这个是为单独应用生成AppIDs,方便管理,App Services根据需要选择即可,必须选择Access WiFi Information和Push Notifications即可3、创建推送证书入口在这里,按照指引完成即可,完成后可下载一个aps.cer文件,双击导入到本地系统中,并在证书上右键导出p12格式证书(个推只需要这个p12格式,信鸽需要转换为pem格式),转换命令openssl pkcs12 -in CertificateName.p12 -out CertificateName.pem -nodes

March 12, 2019 · 1 min · jiezi

RunLoop(一):源码与逻辑

简述什么是RunLoop?顾名思义RunLoop是一个运行循环,它的作用是使得程序在运行之后不会马上退出,保持运行状态,来处理一些触摸事件、定时器时间等。RunLoop可以使得线程在有任务的时候处理任务,没有任务的时候休眠,以此来节省CPU资源,提高程序性能。那RunLoop是怎样保持程序的运行状态,到底处理了哪些事件?下面我们就从源码的层面来了解一下RunLoop。RunLoop获取runloop对象NSRunLoop和CFRunLoopRef都代表RunLoop对象,NSRunLoop是对CFRunLoopRef的封装。Foundation[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象Core FoundationCFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象CFRunLoopGetMain(); // 获得主线程的RunLoop对象RunLoop相关类从源码的代码结构中我们可以找出来一下5个跟RunLoop相关的结构CFRunLoopRefCFRunLoopModeRefCFRunLoopSourceRefCFRunLoopObserverRefCFRunLoopTimerRef下面是CFRunLoopRef的结构代码struct __CFRunLoop { CFRuntimeBase _base; pthread_mutex_t _lock; /* locked for accessing mode list */ __CFPort _wakeUpPort; // used for CFRunLoopWakeUp Boolean _unused; volatile _per_run_data *_perRunData; // reset for runs of the run loop pthread_t _pthread; uint32_t _winthread; CFMutableSetRef _commonModes; CFMutableSetRef _commonModeItems; CFRunLoopModeRef _currentMode; CFMutableSetRef _modes; struct _block_item *_blocks_head; struct _block_item *_blocks_tail; CFAbsoluteTime _runTime; CFAbsoluteTime _sleepTime; CFTypeRef _counterpart;};变量很多,我们不需要全部看,只需要注意这两个CFRunLoopModeRef _currentMode;CFMutableSetRef _modes;每一个runloop里面有很多mode(存在一个set集合里面),然后之后后一个mode叫做currentMode,也就是说runloop一次只能处理一种mode。然后我们再看CFRunLoopModeRef的结构,我已经给大家省略了里面那些我们不需要关注的变量typedef struct __CFRunLoopMode CFRunLoopModeRef;struct __CFRunLoopMode { CFStringRef _name; CFMutableSetRef _sources0; CFMutableSetRef _sources1; CFMutableArrayRef _observers; CFMutableArrayRef _timers;};根据上面这些我们大概的可以概括出来RunLoop这些相关类的关系。CFRunLoopModeRef由上面的源码我们可以稍微总结一下这个CFRunLoopModeRef:CFRunLoopModeRef代表RunLoop的运行模式一个RunLoop包含多个CFRunLoopModeRef,每个CFRunLoopModeRef又包含多个_sources0,_sources1,_observers,_timers。RunLoop每次只能运行一种mode,切换mode的时候,要先退出之前的mode。如果mode中没有_sources0、_sources1、_observers、_timers,程序会立刻退出。常用的两种ModekCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,通常主线程是在这个Mode下运行UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。CFRunLoopObserverRef源码中给出了可以监听的RunLoop状态/ Run Loop Observer Activities /typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { // 进入RunLoop kCFRunLoopEntry = (1UL << 0), // 即将处理timers kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理Sources kCFRunLoopBeforeSources = (1UL << 2), // 即将休眠 kCFRunLoopBeforeWaiting = (1UL << 5), // 被唤醒 kCFRunLoopAfterWaiting = (1UL << 6), // 退出循环 kCFRunLoopExit = (1UL << 7), // 所有状态 kCFRunLoopAllActivities = 0x0FFFFFFFU};具体的怎么样添加observer来监听RunLoop状态我就不贴代码了,网上一搜有很多的。RunLoop的运行逻辑前面我们已经了解了RunLoop相关的结构的源码,知道了RunLoop大概的数据结构,那RunLoop到底是如何工作的呢?它的运行逻辑是什么?我们了解过了每个mode中会存放不同的_sources0、_sources1、_observers、_timers,这些我们可以全部统称是RunLoop要处理的东西,那每一种具体对应我们了解的哪写事件呢?Source0触摸事件处理performSelector:onThread:Source1基于系统Port(端口)的线程间通信系统事件捕捉TimersNSTimer定时器performSelector:withObject:afterDelay:Observers用于监听RunLoop的状态UI刷新(BeforeWating)Autorelease Pool (BeforWaiting)注: UI的刷新并不是即时生效,比如说我们改变了view的backgroundColor,当执行到这行代码是并不是立刻生效,而是先记录下有这么一个任务,然后在RunLoop处理完所有的时间,进入休眠之前UI刷新。这是大神总结的RunLoop的运行逻辑图,我直接拿过来用了。我们主要是看左边这部分,右边的这些标注是在源码中对应的主要方法名称。这个图很容易理解,只有从06跳转到08这一步,单从图上看的话不是很清晰,这一块结合源码就比较明了了。第06步,如果存在Source1就直接跳转到08,在代码中使用了goto这个关键字,其实就是跳过了runloop休眠和唤醒这一部分的代码,直接跳转到了处理各种事件的这一部分。下面我把源码做了一些删减,方便大家可以更清楚的梳理整个过程// 这个是runloop入口函数SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { / DOES CALLOUT */ // 通知Observers 即将进入RunLoop if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry); // 核心方法 result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode); // 通知Observers 即将退出RunLoop if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); return result;}下面是核心方法static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) { int32_t retVal = 0; do { //通知Observers 即将处理Timers if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers); //通知Observers 即将处理Sources if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources); //处理Blocks __CFRunLoopDoBlocks(rl, rlm); //处理source0,根据返回值决定在处理一次blocks Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle); if (sourceHandledThisLoop) { __CFRunLoopDoBlocks(rl, rlm); } Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR); // source1相关 if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) { msg = (mach_msg_header_t *)msg_buffer; // 是否有Source1 有的话跳转到handle_msg if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) { goto handle_msg; } } didDispatchPortLastTime = false; // 通知Observers: 即将休眠 if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting); //休眠 __CFRunLoopSetSleeping(rl); //等待别的消息来唤醒当前线程 __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy); __CFRunLoopUnsetSleeping(rl); // 通知Observers: 即将醒来 if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting); // 标识标识 !!!!! handle_msg:; __CFRunLoopSetIgnoreWakeUps(rl); //下面根据是什么唤醒的runloop来分别处理 if (MACH_PORT_NULL == livePort) { CFRUNLOOP_WAKEUP_FOR_NOTHING(); // handle nothing } else if (livePort == rl->_wakeUpPort) { CFRUNLOOP_WAKEUP_FOR_WAKEUP(); // do nothing on Mac OS } // 被Timer唤醒 else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) { CFRUNLOOP_WAKEUP_FOR_TIMER(); // 处理Timers if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) { // Re-arm the next timer, because we apparently fired early __CFArmNextTimerInMode(rlm, rl); } } // 被Timer唤醒 else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) { CFRUNLOOP_WAKEUP_FOR_TIMER(); // 处理Timers if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) { // Re-arm the next timer __CFArmNextTimerInMode(rlm, rl); } } // 被GCD唤醒 else if (livePort == dispatchPort) { // 处理GCD相关 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE(msg); } else { //被Source1唤醒 //处理Source1 sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop; } //在处理一遍BLocks __CFRunLoopDoBlocks(rl, rlm); // 设置返回值 决定是否继续循环 if (sourceHandledThisLoop && stopAfterHandle) { retVal = kCFRunLoopRunHandledSource; } else if (timeout_context->termTSR < mach_absolute_time()) { retVal = kCFRunLoopRunTimedOut; } else if (__CFRunLoopIsStopped(rl)) { __CFRunLoopUnsetStopped(rl); retVal = kCFRunLoopRunStopped; } else if (rlm->_stopped) { rlm->_stopped = false; retVal = kCFRunLoopRunStopped; } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) { retVal = kCFRunLoopRunFinished; } } while (0 == retVal); return retVal;}图和源码结合来看,整个流程就清晰了很多。流程里面的有些东西不需要我们太过深入的研究,我们把这个流程掌握一下就OK了。细节补充第一点我们都知道RunLoop有一个优势,那就是可以使线程在有工作的时候工作,没有工作的时候休眠,来减少占用CPU资源,提高程序性能。这说明代码在执行到//等待别的消息来唤醒当前线程__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);的时候,会阻塞当前的线程。但这种阻塞跟我们之前所用到过的阻塞线程不是一回事。举个例子,我们可以使用while(1){};这句代码来阻塞线程,这句代码在底层会转换为汇编的代码,我们的线程一直在重读执行这几句代码,所以他仅仅是阻塞线程,并没有使线程休眠,我们的线程一直在工作。但是runloop,通过mach_msg使用了一些内核层的API,真的是实现了线程的休眠,让线程不再占用CPU资源。第二点RunLoop与线程的关系?一个线程对应一个RunLoop对象。RunLoop默认不创建,在第一次获取的时候创建,主线程中的默认存在RunLoop也是因为在底层代码中,提前获取过一次。RunLoop储存在一个全局的字典中,线程是key,RunLoop是value。(源码中有所体现)RunLoop会在线程结束时销毁。 ...

March 12, 2019 · 3 min · jiezi

RunLoop(二):实际应用

前不久我们我们对RunLoop的底层有了简单的了解,那我们现在就要把我们学到的这些东西,实际应用到我们的项目中。Timer定时器问题我们在vc中创建一个定时器,然后在view上面添加一个滚动视图,比如说scrollView,可以发现在scrollView滚动的时候,timer定时器会卡住,停止滚动之后才重新生效。这个问题比较简单,也是我们经常遇到的。因为定时器默认是添加在了RunLoop的NSDefaultRunLoopMode模式下,scrollView在滚动的时候会进入UITrackingRunLoopMode,RunLoop在同一时间只能处理一种mode,所以在滚动的时候,自然定时器就没法处理,卡住。解决方法就是我们创建了timer之后,把他add到RunLoop的NSRunLoopCommonModes,NSRunLoopCommonModes其实并不是一种真实的模式,他只是一个标志,意味着timer在标记为common的模式下都能使用 (标记为common 也就是_commonModes数组)。这个地方多说一句,这个标记为common是啥意思。我们得看回RunLoop结构体的源码struct __CFRunLoop { CFRuntimeBase _base; pthread_mutex_t _lock; /* locked for accessing mode list */ __CFPort _wakeUpPort; // used for CFRunLoopWakeUp Boolean _unused; volatile _per_run_data *_perRunData; // reset for runs of the run loop pthread_t _pthread; uint32_t _winthread; CFMutableSetRef _commonModes; CFMutableSetRef _commonModeItems; CFRunLoopModeRef _currentMode; CFMutableSetRef _modes; struct _block_item *_blocks_head; struct _block_item *_blocks_tail; CFAbsoluteTime _runTime; CFAbsoluteTime _sleepTime; CFTypeRef _counterpart;};可以看到里面有一个set类型的变量,CFMutableSetRef _commonModes;,被放到这个set中的mode就等于是被标记为了common。NSDefaultRunLoopMode和UITrackingRunLoopMode都在里面。下面是我们创建timer的正确姿势 ~ //我们平时可能都是用scheduledTimerWithTimeInterval这个方法创建,这个会默认把timer添加到runloop的defalut模式下,所以我们使用timerWithTimeInterval创建 NSTimer * timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) { NSLog(@"%d",++ count); }]; //NSRunLoopCommonModes 并不是一个真的模式 他只是一个标记,意味着timer在标记为common的模式下都能使用 (标记为common 也就是_commonModes数组) [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];线程保活线程保活并不是所有的项目都用的到,他适应于那种一直有任务需要处理的场景,而且注意,一定要是串行的任务。这种情况下保活一条线程,就可以免去线程创建和销毁的开销,提高性能。具体怎么保活线程,我下面先直接把我的代码贴出来,然后针对一些点在做一系列的说明。(模拟的项目场景是进入到一个vc中,开一条线程,然后用这条线程来执行任务,当然vc销毁时,线程也要销毁。)下面是全部代码,大家可以先跳过代码看下面的一些解析。#import “SecondViewController.h”@interface MyThread : NSThread@end@implementation MyThread- (void)dealloc { NSLog(@"%s",func);}@end@interface SecondViewController ()@property (nonatomic, strong) MyThread * thread;@property (nonatomic, assign, getter=isStopped) BOOL stopped;@end@implementation SecondViewController- (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor whiteColor]; self.stopped = NO; UIButton * btn = [UIButton buttonWithType:UIButtonTypeCustom]; btn.frame = CGRectMake(40, 100, 100, 40); btn.backgroundColor = [UIColor blackColor]; [btn setTitle:@“停止” forState:UIControlStateNormal]; [btn addTarget:self action:@selector(stopThread) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:btn]; __weak typeof(self) weakSelf = self; // 初始化thread self.thread = [[MyThread alloc] initWithBlock:^{ NSLog(@"–begin–"); [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode]; while (weakSelf && !weakSelf.isStopped) { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; } NSLog(@"–end–"); }]; [self.thread start]; }- (void)stopThread { if (!self.thread) return; // waitUntilDone YES [self performSelector:@selector(__stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];}// 执行这个方法必须要在我们自己创建的这个线程中- (void)__stopThread { // 标识 self.stopped = YES; // 停止runloop CFRunLoopStop(CFRunLoopGetCurrent()); // self.thread = nil;}#pragma mark - 添加touch事件 (每点击一次 让线程处理一次事件)- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { if (!self.thread) return; [self performSelector:@selector(threadDoSomething) onThread:self.thread withObject:nil waitUntilDone:NO];}- (void)threadDoSomething { NSLog(@“work–%@”,[NSThread currentThread]);}#pragma mark - dealloc- (void)dealloc { NSLog(@"%s",func); [self stopThread];}@end最顶部新建了一个继承自NSThread的MyThread类,目的就是为了重写-dealloc方法,在内部有打印内容,方便我调试线程是否被销毁。在我们真是的项目中,可以不需要这部分。初始化线程,开启RunLoop __weak typeof(self) weakSelf = self; // 初始化thread self.thread = [[MyThread alloc] initWithBlock:^{ NSLog(@"–begin–"); //往runloop里面添加source/timer/observer [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode]; while (weakSelf && !weakSelf.isStopped) { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; } NSLog(@"–end–"); }]; [self.thread start];这部分是初始化我们的线程,线程的初始化我们一般用的多的是self.thread = [[MyThread alloc] initWithTarget:self selector:@selector(runThread) object:nil];这样的方法,我是觉得这样把self传进线程内部,可能造成一些循环引用问题,最后影响vc和thread的销毁,所以我是用了block的形式。initWithBlock的意思也就是线程初始化完毕会执行block内的代码。一个子线程默认是没有RunLoop的,RunLoop会在第一次获取的时候创建,所以我们先[NSRunLoop currentRunLoop]获取RunLoop,也就是创建了我们当前线程的RunLoop。在了解RunLoop底层的时候我们了解到,如果一个RunLoop没有timer、observer、source,就会退出。我们新创建的RunLoop这些都是没有的,如果我们不手动的添加,那我们的RunLoop一跑起来就这就会退出的。所以就等于说我们必须手动给RunLoop添加点事情做。在代码中我们使用了addPort:forMode这个方法,向当前RunLoop添加一个端口让RunLoop监听。RunLoop有工作做了,自然就不会退出的。我们在开启线程的时候,用了一个while循环,通过一个属性stopped来控制是否跳出循环,然后循环内部使用了- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;这个方法开启RunLoop。有人有可能会问了,这里的开启RunLoop为什么不直接使用- (void)run; 这个方法。这里我稍微解释一下:查阅一下苹果的文档可以了解到,这个run方法,内部其实也是循环的调用了runMode这个方法的,但是这个循环是永远不会停止的,也就是说我们使用run方法开启的RunLoop是永远都不会停下来的,我们调用了stop之后,也只会停止当前的这一次循环,他还是会继续run起来的。所以文档中也提到,如果我们要创建一个可以停下来的RunLoop,用runMode这个方法。所以我们用这个while循环模拟run的运行原理,但是呢,我们通过stopped这个属性可以控制循环的停止。while里面的条件weakSelf && !weakSelf.isStopped为什么不仅仅使用stopped判断,而是还要判断weakSelf是否有值?我们下面会提到的。两个stopThread方法- (void)stopThread { if (!self.thread) return; // waitUntilDone YES [self performSelector:@selector(__stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];}// 执行这个方法必须要在我们自己创建的这个线程中- (void)__stopThread { // 标识置为YES,跳出while循环 self.stopped = YES; // 停止runloop的方法 CFRunLoopStop(CFRunLoopGetCurrent()); // RunLoop退出之后,把线程置空释放,因为RunLoop退出之后就没法重新开启了 self.thread = nil;}stopThread是给我们的停止button调用的,但是实际的停止RunLoop操作在__stopThread里面。在stopThread中调用__stopThread一定要使用performSelector:onThread:这一类的方法,这样就可以保证在我们指定的线程中执行这个方法。如果我们直接调用__stopThread,就说明是在主线程调用的,那就代表我们把主线程的RunLoop停掉了,那我们的程序就完了。touch模拟事件处理我们在touchBegin方法中,让我们self.thread执行-threadDoSomething这个方法,代表每点击一次,我们的线程就要处理一次-threadDoSomething中的打印事件。做这个操作是为了检测看我们每次工作的线程是不是都是我们最开始创建的这一个线程,没有重新开新线程。其他细节那我们仔细观察的话会发现一个问题,-threadDoSomething和stopThread这两个方法中都是用下面这个方法来处理线程间通信- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait但是两次调用传入的wait参数是不一样的。我们要先知道这个waitUntilDone:(BOOL)wait代表什么意思。如果wait传的是YES,就代表我们在主线程用self调用这个performSelector的时候,主线程会等待我们的self.thread这个线程执行他需要执行的方法,等着self.thread执行完方法之后,主线程再继续往下走。那如果是NO,肯定就是主线程不会等了,主线程继续往下走,然后我们的self.thread去调用自己该调用的方法。那为什么在stop方法中是用的YES?有这么一个情形,如果我们push进这个vc,线程初始化,然后RunLoop开启,但是我们不想通过点击停止button来停止,当我们点击导航的back的时候,我也需要销毁线程。所以我们在vc的-dealloc方法中也调用了stopThread方法。那如果stopThread中使用- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait的时候wait不用YES,而是NO,会出现什么情况,那肯定是crash了。如果wait是NO,代表我们的主线程不会等待self.thread执行__stopThread方法。#pragma mark - dealloc- (void)dealloc { NSLog(@"%s",func); [self stopThread];}但是dealloc中主线程调用完stopThread,之后整个dealloc方法就结束了,也就是我们的控制器已经销毁了。但是呢这个时候self.thread还在执行__stopThread方法呢。__stopThread中还要self变量,但是他其实已经销毁了,所以这个地方就会crash了。所以在stopThread中的wait一定要设置为YES。在当时写代码的时候,这样确实处理了crash的问题,但是我直接返回值后,RunLoop并没有结束,线程没有销毁。这就要讲到上面说的while判断条件是weakSelf && !weakSelf.isStopped的原因了。vc执行了dealloc之后,self被置为nil了,weakSelf.isStopped也是nil,取非之后条件又成立了,while循环还要继续的走,RunLoop又run起来了。所以这里我们加上weakSelf这个判断,也就是self必须不为空。总结上面就是我实现的线程保活这一功能的代码和细节分析,当然我们在实际的项目中可能有多个位置需要线程保活这一功能,所以我们应该把这一部分做一下简单的封装,来方便我们在不同的地方调用。大家有兴趣的可以自己封装一下,我在写RunLoop相关的代码时,大多用的是OC层的代码,有兴趣的小伙伴可以尝试一下C语言的API。RunLoop的应用当前不止这么一点,还可以监控应用卡顿,做性能优化,这些以后研究明白了再继续更博客吧,一起加油。 ...

March 12, 2019 · 2 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

iOS新手用swift写一个macos打包工具 一键打包到指定位置

使用dmg安装macos app打包出的app运行如下图,使用磁盘压缩成dmg,直接打开package.dmg即可配置完毕后点击start运行打包脚本,生成ipa到指定目录该项目用swift开发,项目和dmg保存在https://github.com/gwh111/tes…流程解析概述整个流程就是,通过recoverAndSet()函数恢复之前保存数据,start()检查路径后会替换内部package.sh的动态路径,然后起一个线程创建Process(),通过Pipe()监控脚本执行输出,捕获异常1.recoverAndSet()通过UserDefaults简单地记住上次打包的路径,下次写了新代码后即可点击start立即打包恢复时把值传给控件func recoverAndSet() { let objs:[Any]=[projectPath,projectName,exportOptionsPath,ipaPath] let names:[NSString]=[“projectPath”,“projectName”,“exportOptionsPath”,“ipaPath”] for i in 0…3{ print(i) let key=names[i] let obj=objs[i] as! NSTextField let v=UserDefaults.standard.value(forKey: key as String) if (v == nil){ continue } obj.stringValue=(v as? String)! } let ps=UserDefaults.standard.value(forKey: “projectName” as String) if (ps==nil){ }else{ projectName.stringValue=(ps as? String)!; } let dr=UserDefaults.standard.value(forKey: “debugRelease”) if (dr==nil){ }else{ debugRelease.selectedSegment=dr as! Int; } debugRelease.action = #selector(segmentControlChanged(segmentControl:)) }2.selectPath()通过NSOpenPanel()创建打开文档面板对象,选择文件目录,而不是手动输入通常项目路径名和项目名称是一致的,这里使用了path.components(separatedBy:"/")将路径分割自动取工程名@IBAction func selectPath(_ sender: NSButton) { let tag=sender.tag print(tag) // 1. 创建打开文档面板对象 let openPanel = NSOpenPanel() // 2. 设置确认按钮文字 openPanel.prompt = “Select” // 3. 设置禁止选择文件 openPanel.canChooseFiles = true if tag==0||tag==2 { openPanel.canChooseFiles = false } // 4. 设置可以选择目录 openPanel.canChooseDirectories = true if tag==1 { openPanel.canChooseDirectories = false openPanel.allowedFileTypes=[“plist”] } // 5. 弹出面板框 openPanel.beginSheetModal(for: self.view.window!) { (result) in // 6. 选择确认按钮 if result == NSApplication.ModalResponse.OK { // 7. 获取选择的路径 let path=openPanel.urls[0].absoluteString.removingPercentEncoding! if tag==0 { self.projectPath.stringValue=path let array=path.components(separatedBy:"/") if array.count>1{ let name=array[array.count-2] print(array) print(name as Any) self.projectName.stringValue=name } }else if tag==1 { self.exportOptionsPath.stringValue=path }else{ self.ipaPath.stringValue=path } let names:[NSString]=[“projectPath”,“exportOptionsPath”,“ipaPath”] UserDefaults.standard.setValue(openPanel.url?.path, forKey: names[tag] as String) UserDefaults.standard.setValue(self.projectName.stringValue, forKey: “projectName”) UserDefaults.standard.synchronize() // self.savePath.stringValue = (openPanel.directoryURL?.path)!// // 8. 保存用户选择路径(为了可以在其他地方有权限访问这个路径,需要对用户选择的路径进行保存)// UserDefaults.standard.setValue(openPanel.url?.path, forKey: kSelectedFilePath)// UserDefaults.standard.synchronize() } // 9. 恢复按钮状态// sender.state = NSOffState } }3.start()通过str.replacingOccurrences(of: “file://”, with: “")将路径和sh里的路径替换通过DispatchQueue.global(qos: .default).async获取Concurrent Dispatch Queue并开启Process()在处理完的terminationHandler里回到主线程更新UI@IBAction func start(_ sender: Any) { guard projectPath.stringValue != "” else { self.logTextField.stringValue=“工程目录不能为空”; return } guard projectName.stringValue != "" else { self.logTextField.stringValue=“工程名不能为空”; return } guard exportOptionsPath.stringValue != "" else { self.logTextField.stringValue=“exportOptions不能为空 xcode生成ipa文件夹中包含”; return } guard ipaPath.stringValue != "" else { self.logTextField.stringValue=“输出ipa目录不能为空”; return } var str1=“abc” let str2=“abc” if str1==str2{ print(“same”) } //save let objs:[Any]=[projectPath,exportOptionsPath,ipaPath] let names:[NSString]=[“projectPath”,“exportOptionsPath”,“ipaPath”] for i in 0…2{ let obj=objs[i] as! NSTextField UserDefaults.standard.setValue(obj.stringValue, forKey: names[i] as String) } UserDefaults.standard.setValue(self.projectName.stringValue, forKey: “projectName”) UserDefaults.standard.setValue(self.debugRelease.selectedSegment, forKey: “debugRelease”) UserDefaults.standard.synchronize() // self.showInfoTextView.string=“abc”; if isLoadingRepo { self.logTextField.stringValue=“正在执行上一个任务”; return }// 如果正在执行,则返回 isLoadingRepo = true // 设置正在执行标记 let projectStr=self.projectPath.stringValue let nameStr=self.projectName.stringValue let plistStr=self.exportOptionsPath.stringValue let ipaStr=self.ipaPath.stringValue let returnData = Bundle.main.path(forResource: “package”, ofType: “sh”) let data = NSData.init(contentsOfFile: returnData!) var str = NSString(data:data! as Data, encoding: String.Encoding.utf8.rawValue)! as String if debugRelease.selectedSegment==0 { str = str.replacingOccurrences(of: “DEBUG_RELEASE”, with: “debug”) }else{ str = str.replacingOccurrences(of: “DEBUG_RELEASE”, with: “release”) } str = str.replacingOccurrences(of: “NAME_PROJECT”, with: nameStr) str = str.replacingOccurrences(of: “PATH_PROJECT”, with: projectStr) str = str.replacingOccurrences(of: “PATH_PLIST”, with: plistStr) str = str.replacingOccurrences(of: “PATH_IPA”, with: ipaStr) str = str.replacingOccurrences(of: “file://”, with: “”) print(“返回的数据:(str)”); self.logTextField.stringValue=“执行中。。。”; DispatchQueue.global(qos: .default).async { // str=“aaaabc”// str = str.replacingOccurrences(of: “ab”, with: “dd”) // print(self.projectPath.stringValue)// print(self.exportOptionsPath.stringValue)// print(self.ipaPath.stringValue) let task = Process() // 创建NSTask对象 // 设置task task.launchPath = “/bin/bash” // 执行路径(这里是需要执行命令的绝对路径) // 设置执行的具体命令 task.arguments = ["-c",str] task.terminationHandler = { proce in // 执行结束的闭包(回调) self.isLoadingRepo = false // 恢复执行标记 //5. 在主线程处理UI DispatchQueue.main.async(execute: { self.logTextField.stringValue=“执行完毕”; }) } self.captureStandardOutputAndRouteToTextView(task) task.launch() // 开启执行 task.waitUntilExit() // 阻塞直到执行完毕 } }4.captureStandardOutputAndRouteToTextView()对执行脚本的日志监控为了看到脚本报错或执行成功提示,使用Pipe()监控 NSPipe一般是两个线程之间进行通信使用的在osx 系统中 ,沙盒有个规则:在App运行期间通过NSOpenPanel用户手动打开的任意位置的文件,把这个这个路径保存下来,后面都是可以直接用这个路径继续访问文件,但当App退出后再次运行,这个路径默认是不可以访问的fileprivate func captureStandardOutputAndRouteToTextView(_ task:Process) { //1. 设置标准输出管道 outputPipe = Pipe() task.standardOutput = outputPipe //2. 在后台线程等待数据和通知 outputPipe.fileHandleForReading.waitForDataInBackgroundAndNotify() //3. 接受到通知消息 observe=NotificationCenter.default.addObserver(forName: NSNotification.Name.NSFileHandleDataAvailable, object: outputPipe.fileHandleForReading , queue: nil) { notification in //4. 获取管道数据 转为字符串 let output = self.outputPipe.fileHandleForReading.availableData let outputString = String(data: output, encoding: String.Encoding.utf8) ?? "" if outputString != “”{ //5. 在主线程处理UI DispatchQueue.main.async { if self.isLoadingRepo == false { let previousOutput = self.showInfoTextView.string let nextOutput = previousOutput + “\n” + outputString self.showInfoTextView.string = nextOutput // 滚动到可视位置 let range = NSRange(location:nextOutput.utf8CString.count,length:0) self.showInfoTextView.scrollRangeToVisible(range) if self.observe==nil { return } NotificationCenter.default.removeObserver(self.observe!) return }else{ let previousOutput = self.showInfoTextView.string var nextOutput = previousOutput + “\n” + outputString as String if nextOutput.count>5000 { nextOutput=String(nextOutput.suffix(1000)); } // 滚动到可视位置 let range = NSRange(location:nextOutput.utf8CString.count,length:0) self.showInfoTextView.scrollRangeToVisible(range) self.showInfoTextView.string = nextOutput } } } if self.isLoadingRepo == false { return } //6. 继续等待新数据和通知 self.outputPipe.fileHandleForReading.waitForDataInBackgroundAndNotify() } }-exportOptions.Plist 常用文件内容格式compileBitcodeFor non-App Store exports, should Xcode re-compile the app from bitcode? Defaults to YESembedOnDemandResourcesAssetPacksInBundleFor non-App Store exports, if the app uses On Demand Resources and this is YES, asset packs are embedded in the app bundle so that the app can be tested without a server to host asset packs. Defaults to YES unless onDemandResourcesAssetPacksBaseURL is specifiedmethodDescribes how Xcode should export the archive. Available options: app-store, ad-hoc, package, enterprise, development, and developer-id. The list of options varies based on the type of archive. Defaults to developmentteamIDThe Developer Portal team to use for this export. Defaults to the team used to build the archivethinningFor non-App Store exports, should Xcode thin the package for one or more device variants? Available options: <none> (Xcode produces a non-thinned universal app), <thin-for-all-variants> (Xcode produces a universal app and all available thinned variants), or a model identifier for a specific device (e.g. “iPhone7,1”). Defaults to <none>uploadBitcodeFor App Store exports, should the package include bitcode? Defaults to YESuploadSymbolsFor App Store exports, should the package include symbols? Defaults to YES ...

March 11, 2019 · 4 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

程序员—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开发者使用方便的几个工具

移动应用世界发生了巨大的变化,无论是在风格上还是在市场竞争上,消费者意识都推动了移动应用开发公司的崛起。 新的应用以及新的功能的出现Apple IOS是为用户提供最新工具和升级的平台之一,它为iPhone、iPad、AppleTV和iPad等不同产品开发应用程序支持。关于苹果和iOS:-苹果并不是一个新名字。它的产品涵盖有普通电脑和个人电脑。它基于iOS,iphone操作系统,自他出现以来,全世界都对它表示赞赏。作为iOS应用程序开发人员,苹果每年两次更新其操作系统是一项重要信息。如果你希望从事iOS开发者的工作,以下是一些工具希望可以帮助到你。开发工具以下是开发iOS应用程序时非常有用的几个应用程序工具。为了完整地理解,类似的功能工具是分组在一个主要的功能之下。让我们 看一看,Designing在应用程序的设计阶段有帮助的工具 a)Free iOS PSD 是PSD格式的免费模板的集合 b) Live View它有助于检查应用程序在实时场景中的外观。 c) Glimpshop是一种新工具替代Photoshop。Image Extractors此工具用于创建和提取文件的图像。a)Appcrush Creating an .app file创建一个新文件,即应用程序的图像。Design Implementation此工具将有助于应用程序的开发a) iICNS有许多苹果图标可供使用 b) Cocca Controls Collection 用于IOS应用程序的代码/组件集 c) Dribble Best可用于应用程序开发的最佳应用程序设计这些基本工具中很少有能够证明对开发IOS应用程序有用的工具。除了上面提到的工具之外,还有很多有用的工具往后会继续介绍。

March 8, 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

Autoreleasepool自动释放池-源码

Autoreleasepool相关的内容是在面试中比较容易被问到的。之前呢,谈到Autoreleasepool只能粗浅的了解到自动释放池与内存的管理有关,具体是怎么样来管理和释放对象,并没有深入的学习,本文是笔者在深入学习Autoreleasepool之后的总结和心得,希望对大家有帮助。main函数首先我们从main函数开始,main函数是我们应用的入口。int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); }}可以看出,我们整个iOS应用都是包含在一个自动释放池中。而且在现在的ARC环境中,自动释放池的用法就是这样子 @autoreleasepool {}。@autoreleasepool这个@autoreleasepool{}大家都会用,我们的代码直接写在这个大括号内即可。我们代码中的对象是怎样加到自动释放池中的,最后又是怎么样被释放的呢 ?我们要先知道这个@autoreleasepool到底是什么。从网上的一些博客中可以学到的,在命令行使用clang -rewrite-objc main.m 让编译器重新改写main函数所在的这个文件。当然了这一步我并没有操作,直接“盗用”了大家的结果。从上图中可以看出,@autoreleasepool被转换为了一个__AtAutoreleasePool结构体。然后通过在main.cpp中查找找到这个结构体的定义。struct __AtAutoreleasePool { __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();} ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);} void * atautoreleasepoolobj;};这个结构体在初始化是调用objc_autoreleasePoolPush(),在析构时调用objc_autoreleasePoolPop()。经过整理,可以把main函数中实际的代码应该是类似这样的。int main(int argc, const char * argv[]) { { void * atautoreleasepoolobj = objc_autoreleasePoolPush(); // do whatever you want objc_autoreleasePoolPop(atautoreleasepoolobj); } return 0;}下面就是正式的通过源码来学习Autoreleasepool了Autoreleasepool源码上面我们提到了mian函数中的@autoreleasepool其实最终转成了objc_autoreleasePoolPush()和objc_autoreleasePoolPop()这两个方法的调用,我们去源码中搜一下这两个函数。void *objc_autoreleasePoolPush(void){ return AutoreleasePoolPage::push();}voidobjc_autoreleasePoolPop(void *ctxt){ AutoreleasePoolPage::pop(ctxt);}有源码可以看出,这两个函数就是对AutoreleasePoolPage类的pish和pop方法的封装,所以我们来着重看AutoreleasePoolPage类。AutoreleasePoolPage简化一下代码,先看类的部分属性class AutoreleasePoolPage { static size_t const SIZE = #if PROTECT_AUTORELEASEPOOL PAGE_MAX_SIZE; // must be multiple of vm page size#else PAGE_MAX_SIZE; // size and alignment, power of 2#endif magic_t const magic; id *next; pthread_t const thread; AutoreleasePoolPage * const parent; AutoreleasePoolPage *child; uint32_t const depth; uint32_t hiwat;};熟悉链表的朋友,看到这个parent和child就差不多能猜出来了,每一个自动释放池其实是一个双向链表,链表的每一个结点就是这个AutoreleasePoolPage,每个AutoreleasePoolPage的大小为4096字节#define I386_PGBYTES 4096#define PAGE_SIZE I386_PGBYTES自动释放池中的栈(转载)如果我们的一个AutoreleasePoolPage 被初始化在内存的 0x100816000 ~ 0x100817000 中,它在内存中的结构如下:其中有 56 bit 用于存储 AutoreleasePoolPage 的成员变量,剩下的 0x100816038 ~ 0x100817000 都是用来存储加入到自动释放池中的对象。begin() 和 end() 这两个实例方法帮助我们快速获取 0x100816038 ~ 0x100817000 这一范围的边界地址。next 指向了下一个为空的内存地址,如果next指向的地址加入一个 object,它就会如下图所示移动到下一个为空的内存地址中:从图片中我们可以看到在AutoreleasePoolPage的栈中出现了一个POOL_SENTINEL,我们称之为哨兵对象。#define POOL_SENTINEL nil其实哨兵对象只是nil的别名,他有啥作用呢 ?每个自动释放池初始化在调用objc_autoreleasePoolPush的时候,都会把一个POOL_SENTINELpush到自动释放池的栈顶,并且返回这个POOL_SENTINEL的地址。int main(int argc, const char * argv[]) { { void * atautoreleasepoolobj = objc_autoreleasePoolPush(); // do whatever you want objc_autoreleasePoolPop(atautoreleasepoolobj); } return 0;}上面这个atautoreleasepoolobj就是一个POOL_SENTINEL。可以看到在调用objc_autoreleasePoolPop时,会传进去这个地址:根据传入的哨兵对象地址找到哨兵对象所处的page在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次- release消息,并向回移动next指针到正确位置补充2:从最新加入的对象一直向前清理,可以向前跨越若干个page,直到哨兵所在的page因为自动释放池是一个双向链表,而且每一个page的空间有限,所以会存在当前page已满的情况,也就出现了一个自动释放池跨越几个page的情况,所以在release的时候,也要顺着链表全部清理掉。objc_autoreleasePoolPush static inline void *push() { id *dest; if (DebugPoolAllocation) { // Each autorelease pool starts on a new pool page. dest = autoreleaseNewPage(POOL_BOUNDARY); } else { dest = autoreleaseFast(POOL_BOUNDARY); } assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY); return dest; }经查阅,DebugPoolAllocation是来区别调试模式的,我们主要看autoreleaseFast这个函数。 static inline id *autoreleaseFast(id obj) { AutoreleasePoolPage *page = hotPage(); if (page && !page->full()) { return page->add(obj); } else if (page) { return autoreleaseFullPage(obj, page); } else { return autoreleaseNoPage(obj); } }hotPage( )我们可以理解为获取当前的AutoreleasePoolPage,获取到当前page之后又根据page是否已满来区别处理。有 hotPage 并且当前 page 不满调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中有 hotPage 并且当前 page 已满调用 autoreleaseFullPage 初始化一个新的页调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中无 hotPage调用 autoreleaseNoPage 创建一个 hotPage调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中 static attribute((noinline)) id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) { // The hot page is full. // Step to the next non-full page, adding a new page if necessary. // Then add the object to that page. assert(page == hotPage()); assert(page->full() || DebugPoolAllocation); do { if (page->child) page = page->child; else page = new AutoreleasePoolPage(page); } while (page->full()); setHotPage(page); return page->add(obj); }上面代码中的函数是在page已满的时候调用,从源码中可以看出通过传入的page遍历链表,直到找到一个未满的page,如果遍历到最后一个结点也没有未满的,就新建一个new AutoreleasePoolPage(page);。并且要把找到的满足条件的这个page设置为hotPage。objc_autoreleasePoolPop我们看一下pop的源码,里面内容很多,我们精简了一下。static inline void pop(void *token) { AutoreleasePoolPage *page = pageForPointer(token); id *stop = (id *)token; page->releaseUntil(stop); if (page->child) { // 不清楚为什么要用下面这个if分类 if (page->lessThanHalfFull()) { page->child->kill(); } else if (page->child->child) { page->child->child->kill(); } }}通过token调用pageForPointer()方法获取到当前的AutoreleasePoolPage,然后调用releaseUntil()释放page中的对象,直到stop,child节点调用kill()方法。void kill() { AutoreleasePoolPage *page = this; //通过循环先找到最后一个节点 while (page->child) page = page->child; AutoreleasePoolPage *deathptr; //通过do-while循环,依次从后往前置为nil do { deathptr = page; page = page->parent; if (page) { page->unprotect(); page->child = nil; page->protect(); } delete deathptr; } while (deathptr != this);}pageForPointer()主要是通过内存地址的操作,获取当前指针所在页的首地址, releaseUntil()也是通过一个循环来释放所有的对象,具体的源码大家可以自己看一下。Autorelease对象什么时候释放在没有手动加AutoreleasePool的情况下,Autorelease对象都是在当前的runloop迭代结束时释放的,因为系统在每个runloop迭代中都加入了自动释放池Push和Pop。这个问题又要跟runloop联系到一起了,等我们研究过runloop的源码,对这个问题应该就有更深刻的认识了。参考文章自动释放池的前世今生黑幕背后的Autorelease ...

March 7, 2019 · 2 min · jiezi

深入学习runtime

本文的切入点是2014年的一场线下分享会,也就是sunnyxx分享的objc runtime。很惭愧,这么多年了才完整的看了一下这个分享会视频。当时他出了一份试题,并戏称精神病院objc runtime入院考试。我们今天的这篇文章就是从这个试题中的题目入手,来深入的学习runtime。源码版本objc4-750第一题@implementation Son : Father- (id)init { self = [super init]; if (self) { NSLog(@"%@", NSStringFromClass([self class])); NSLog(@"%@", NSStringFromClass([super class])); } return self;}@end第一行的[self class]应该是没有疑问的,肯定是Son,问题就出在这个[super class]。大家都知道,我们OC的方法在底层会编译为一个objc_msgSend的方法(消息发送),[self class]符合这个情况,因为self是类的一个隐藏参数。但是super并不是一个参数,它是一个关键字,实际上是一个“编译器标示符”,所以这就有点不一样了,经查阅资料,在调用[super class]的时候,runtime调用的是objc_msgSendSuper方法,而不是objc_msgSend。首先要做的是验证一下是否是调用了objc_msgSendSuper。这里用到了clang这个工具,我们可以把OC的代码转成C/C++。@implementation Son- (void)test { [super class];}@end在终端运行clang -rewrite-objc Son.m生成一个Son.cpp文件。在这个.cpp文件的底部我们可以找到这么一部分代码// @implementation Sonstatic void _I_Son_test(Son * self, SEL _cmd) { ((Class (*)(__rw_objc_super *, SEL))(void )objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass(“Son”))}, sel_registerName(“class”));}// @end看起来乱七八糟,有很多强制类型转换的代码,不用理它,我们只要看到了我们想要的objc_msgSendSuper就好。去源码中看一下这个方法(具体实现好像是汇编,看不懂)OBJC_EXPORT voidobjc_msgSendSuper(void / struct objc_super *super, SEL op, … / ) OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);可以看出来这个方法第一个参数是一个objc_super类型的结构体,第二个是一个我们常见的SEL,后面的…代表还有扩展参数。再看一下这个objc_super结构体。/// Specifies the superclass of an instance. struct objc_super { /// Specifies an instance of a class. __unsafe_unretained _Nonnull id receiver; /// Specifies the particular superclass of the instance to message. #if !defined(__cplusplus) && !OBJC2 / For compatibility with old objc-runtime.h header 为了兼容老的 / __unsafe_unretained _Nonnull Class class;#else __unsafe_unretained _Nonnull Class super_class;#endif / super_class is the first class to search */};第一个参数是接收消息的receiver,第二个是super_class(见名知意~ ????)。我们和上面提到的.cpp中的代码对应一下就会发现重点了,receiver是self。所以,这个[super class]的工作原理是,从objc_super结构体的super_class指向类的方法列表开始查找class方法,找到这个方法之后使用receiver来调用。所以,调用class方法的其实还是self,结果也就是打印Son。第二题下面代码的结果?BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]];BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]];对于这个问题我们就要从OC类的结构开始说起了。我们都应该有所了解,每一个Objective-c的对象底层都是一个C语言的结构体,在之前老的源码中体现出,所有对象都包含一个isa类型的指针,在新的源码中已经不是这样了,用一个结构体isa_t代替了isa。这个isa_t结构体包含了当前对象指向的类的信息。我们来看看当前的类的结构,首先从我们的祖宗类NSObject开始吧。@interface NSObject <NSObject> { Class isa OBJC_ISA_AVAILABILITY;}我们的NSObject类有一个Class类型的变量isa,通过源码我们可以了解到这个Class到底是什么typedef struct objc_class *Class;typedef struct objc_object *id;struct objc_object {private: isa_t isa;}struct objc_class : objc_object { // Class ISA; Class superclass; cache_t cache; // formerly cache pointer and vtable class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags}上面的代码是我从源码中复制拼到一起来的。可以看出来,Class就是是一个objc_class结构体,objc_class中有四个成员变量Class superclass,cache_t cache,class_data_bits_t bits,和从objc_object中继承过来的isa_t isa。当Objc为一个对象分配内存,初始化实例变量后,在这些实例变量的结构体中第一个就是isa。而且从上面的objc_class的结构可以看出来,不仅仅是实例会包含一个isa结构体,所有的类也会有这个isa。所以说,我们可以得出这样一个结论:Objective-c中的类也是一个对象。那现在就有了一个新的问题,类的isa结构体中储存的是什么?这里就要引入一个元类的概念。知识补充:在Objective-c中,每个对象能执行的方法并没有存在这个对象中,因为如果每一个对象都单独储存可执行的方法,那对内存来说是一个很大的浪费,所以说每个对象可执行的方法,也就是我们说的一个类的实例方法,都储存在这个类的objc_class结构体中的class_data_bits_t结构体里面。在执行方法是,对象通过自己的isa找到对应的类,然后在class_data_bits_t中查找方法实现。关于方法的结构,可以看这篇博客来理解一些。(跳转链接)引入元类就是来保证了实例方法和类方法查找调用机制的一致性。所以让一个类的isa指向他的元类,这样的话,对象调用实例方法可以通过isa找到对应的类,然后查找方法的实现并调用,在调用类方法的时候,通过类的isa找到对应的元类,在元类里完成类方法的查找和调用。下面这种图也是在网上很常见的了,不需要过多解释,大家看一下记住就行了。看到这里我们就要回到我们的题目上了。首先呢,还是要去看一下这个源码中isKindOfClass:和isMemberOfClass:的实现了。isKindOfClass先看isKindOfClass吧,源码中提供了一个类方法一个实例方法。+ (BOOL)isKindOfClass:(Class)cls { for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) { if (tcls == cls) return YES; } return NO;}- (BOOL)isKindOfClass:(Class)cls { for (Class tcls = [self class]; tcls; tcls = tcls->superclass) { if (tcls == cls) return YES; } return NO;}总体的逻辑都是一样的,都是先声明一个Class类型的tcls,然后把这个tcls跟cls比较,看是否相等,如果不相等则循环tcls的各级superclass来进行比较,直到为tcls为nil停止循环。不同的地方就是类方法初始的tcls是object_getClass((id)self),实例方法的是[self class]。object_getClass((id)self)其实是返回了这个self的isa对应的结构,因为这个方法是在类方法中调用的,self则代表这个类,那object_getClass((id)self)返回的也应该是这个类的元类了。其实在-isKindOfClass这个实例方法中,调用方法的是一个对象,tcls初始等于[self class],也就是对相对应的类。我们可以看出来,在实例方法中这个tcls初始的值也是方法调用者的isa对应的结构,跟类方法中逻辑是一致的。回到我们的题目中,BOOL res1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];[NSObject class]也就是NSObject类调用这个isKindOfClass:方法(类方法),方法的参数也是NSObject的类。在第一次循环中,tcls对应的应该是NSObject的isa指向的,也就是NSObject的元类,它跟NSObject类不相等。第二次循环,tcls取自己的superclass继续比较,我们上面的那个图,大家可以看一下,NSObject的元类的父类就是NSObject这个类本身,在与NSObject比较结果是相等。所以res1为YES。BOOL res3 = [(id)[Sark class] isKindOfClass:[Sark class]];跟上面一样来分析,在第一次循环中,tcls对应的应该是Sark的isa指向的,也就是Sark的元类,跟Sark的类相比,肯定是不相等。第二次循环,tcls取superclass,从图中可以看出,Sark元类的父类是NSObject的元类,跟Sark的类相比,肯定也是不相等。第三次循环,NSObject元类的父类是NSObject类,也不相等。再取superclass,NSObject的superclass为nil,循环结束,返回NO,所以res3是NO。isMemberOfClass+ (BOOL)isMemberOfClass:(Class)cls { return object_getClass((id)self) == cls;}- (BOOL)isMemberOfClass:(Class)cls { return [self class] == cls;}有了上面isKindOfClass逻辑分析的基础,isMemberOfClass的逻辑我们应该很清楚,就是使用方法调用者的isa对应的结构和传入的cls参数比较。BOOL res2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];BOOL res4 = [(id)[Sark class] isMemberOfClass:[Sark class]];NSObject类的isa对应的是NSObject的元类,和NSObject类相比不相等,所以res2为NO。Sark类的isa对应的是Sark的元类,和Sark类相比也是不相等,所以,res4也是NO。第三题下面的代码会?Compile Error / Runtime Crash / NSLog…?@interface NSObject (Sark)+ (void)foo;@end@implementation NSObject (Sark)- (void)foo { NSLog(@“IMP: -[NSObject (Sark) foo]”);}@end// 测试代码[NSObject foo];[[NSObject new] foo];[[NSObject new] foo];这一个代码应该是毫无疑问会调用到-foo方法。问题就在这个[NSObject foo],因为在我们的认识中[NSObject foo]是调用的类方法,实现的是实例方法,应该不能调用到。其实这个题的考点跟第二个题差不多,我们已经知道了,一个类的实例方法储存在类中,类方法储存在这个类的元类。所以NSObject在调用foo这个方法是,会先去NSObject的元类中找这个方法,没有找到,那就要去父类中继续查找。上面图已经给出了,NSObject的元类的父类是NSObject类,所以在NSObject中查找方法,找到方法之后执行打印。第四题下面的代码会?Compile Error / Runtime Crash / NSLog…?@interface Sark : NSObject@property (nonatomic, copy) NSString *name;@end@implementation Sark- (void)speak { NSLog(@“my name’s %@”, self.name);}@end@implementation ViewController- (void)viewDidLoad { [super viewDidLoad]; id cls = [Sark class]; void *obj = &cls; [(__bridge id)obj speak];}@end这里我们先上结果:my name’s <ViewController: 0x7f9454c1c680>不管地址是多少,打印的总是ViewController。我们先想一下为什么可以成功的调用speak?id cls = [Sark class];创建了一个Sark的class。void *obj = &cls;创建一个obj指针指向了cls的地址。最后使用(__bridge id)obj把这个obj指针转成一个oc的对象,用对象来调用speak,所以可以调用成功。我们在方法中输出的是self.name,为什么会打印出来ViewController?经过查阅资料得知,在调用self.name的时候,本质上是self指针在内存向高位地址偏移一个指针。(这个还得以后深入研究)为了验证一下查到的这个结论,我改写了一下speak方法中的代码如下。- (void)speak { unsigned int count = 0; Ivar * ivars = class_copyIvarList([self class], &count); for (int i = 0; i < count; i ++) { Ivar ivar = ivars[i]; ptrdiff_t offSet = ivar_getOffset(ivar); const char * n = ivar_getName(ivar); NSLog(@"%@—–%ld",[NSString stringWithUTF8String:n],offSet); } NSLog(@“my name’s %@”, self.name);}取到类的各个变量,然后打印出他的偏移。输出结构如下:_name—–8偏移了一个指针。那为什么打印出来了ViewController的地址,我们就要研究各个变量的内存地址位置关系了。在iewDidLoad中变量的压栈顺序如下所示:第一个参数self和第二个参数_cmd是隐藏参数,第三和第四个参数是执行[super viewDidLoad]之后进栈的,之前第一题的时候我们有了解过,super调用的方法在底层编译之后会有一个objc_super类型的结构体。在结构体中有receiver和super_class两个变量,receiver就是self。我在网上查过很多的资料,都是super_class比receiver(self)先入栈,不太懂为什么是super_class先入。最后是生成的obj进栈。所以在打印self.name的时候,是obj的指针向高位偏移了一个指针,也就是self,所以打印出来的是ViewController的指针。参考https://github.com/draveness/…http://blog.sunnyxx.com/2014/...https://www.jianshu.com/p/743...https://github.com/ming1016/s… ...

March 7, 2019 · 2 min · jiezi

记前端hybrid学习总结

什么是hybridhybrid即“混合”,即前端和客户端的混合开发需前端开发人员和客户端开发人员配合完成某些环节可能涉及到server端hybrid存在价值可以快速迭代更新(无需app审核)体验流畅(和NA体验基本类似)减少开发和沟通成本,双端公用一套代码webview是app中的一个组件(app中可以有webview,也可以没有)用于加载h5页面,即一个小型的浏览器内核file协议浏览器打开本地文件,就是通过使用file协议file协议:本地文件,快http(s)协议:网络加载,慢使用场景使用NA:体验要求极致,变化不频繁(如头条首页)使用hybrid:体验要求高,变化频繁(如头条的新闻详情页)使用h5:体验无要求,不常用(如举报,反馈等页面)具体实现前端做好静态页面(html,js,css),将文件交给客户端客户端拿到前端静态页面,以文件形式储存在app内客户端在一个webview中使用file协议加载静态页面hybrid更新上线流程分版本,有版本号,如201803211015将静态文件压缩成zip包,上传到服务端客户端每次启动,都去服务端检查版本号如果服务端版本号大于客户端版本号,就去下载最新的zip包下载完之后解压,覆盖原有文件hybrid和h5的区别hybrid优点:体验更好,跟NA体验基本一致可快速迭代,无需app审核hybrid缺点:开发成本高:联调,测试,查bug都比较麻烦运维成本高适用场景产品都稳定功能,体验要求高,迭代频繁.产品型(hybrid)单次运营活动(如xx红包)或不常用功能.运营型(h5)schema协议 – 前端和客户端通讯的约定网上搜的微信部分的schema协议weixin://dl/scan 扫一扫<!–以下是演示,无法正常运行,微信有严格的权限验证,外部页面不能随意使用schema–> function invokeScan() { var iframe = document.createElement(‘iframe’); iframe.style.display = ’none’; iframe.src = ‘weixin://dl/scan’; // iframe 访问 schema var body = document.body || document.getElementByName(‘body’)[0]; body.appendChild(iframe); setTimeout(function(){ body.removeChild(iframe); // 销毁iframe iframe = null; });}document.getElementById(‘btn’).addEventListener(‘click’, function(){ invokeScan(); // html调用schema协议})// 如果要加上参数和callback,那么就要这么写window[’_weixin_scan_callback’] = function(result) { alert(result);}// …省略…iframe.src = ‘weixin://dl/scan?k1=v1&k2=v2&callback=_weixin_scan_callback’;// …省略…封装schemavoke.js(function (window, undefined) { // 封装schema function _invoke(action, data, callback){ // 拼装schema协议 var schema = ‘myapp://utils/’ + action; // 拼装参数 schema += ‘?a=a’; for(key in data){ if(data.hasOwnProperty(key)){ schema += ‘&’ + key + ‘=’ + data[key]; } } // 拼装callback callbackName = ‘’; if(typeof callback === ‘string’){ callbackName = callback; } else { callbackName = action + Date.now(); window[callbackName] = callback; } schema += ‘callback=callbackName’; // 触发schema var iframe = document.createElement(‘iframe’); iframe.style.display = ’none’; iframe.src = schema; var body = document.body; body.appendChild(iframe); setTimeout(() => { body.removeChild(iframe); iframe = null; }); } window.invoke = { share: function (data, callback) { _invoke(‘share’, data, callback); } }})(window);index.html<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <meta name=“viewport” content=“width=device-width, initial-scale=1.0”> <meta http-equiv=“X-UA-Compatible” content=“ie=edge”> <title>Document</title></head><body> <button id=“btn”>分享</button></body><script src=“voke.js”></script><script> document.getElementById(‘btn’).addEventListener(‘click’,function(){ window.invoke.share({ titile:‘111’, content:‘2222’ }, function(res){ if (res.status){ alert(‘分享成功!’); } else { alert(res.message); } }); });</script></html>内置上线将以上封装的代码打包,叫做invoke.js,内置到客户端客户端每次启动webview,都默认执行invoke.js本地加载,免去网络加载都时间,速度会更快本地加载,没有网络请求,黑客看不到schema协议,更安全 ...

March 6, 2019 · 1 min · jiezi

关于埋点

本文主要介绍 火球买手 项目上的埋点方案(基于神策),以及一些心得。事实上在项目早期,我们的埋点完全依赖于第三方的全埋点技术,客户端开发人员只需要做一些简单的工作就能满足 BI 部门对数据的需求。但随着业务增长,对数据的准确性和精细化的要求越来越高,之后不得不转向手动埋点,当然这个也是基于第三方的。目前 BI 部门对埋点数据要求可以总结为一句话:『从哪里来到哪里去』,比如在 Timeline 中点击一篇文章进入详情页,那么 Timeline 就是『从哪里来』,详情页就是『到哪里去』,当然实际项目中『从哪里来』不只需要一个维度定位,有时候需要两三个维度才能定位。下面具体来说下 火球买手 项目上是如何埋点的,首先『频道主页』是项目中比较常见的页面,它对应的 Model 是 Channel,然后当任何点击进入的频道主页的事件触发后都需要上报以下数据{ module_name page_name channel_name channel_id}page_name指的是当前的 ViewController 名称,module_name主要用于区分同一个页面内的不同入口,这样子就能确定『从哪里来』,channel_name 和 channel_id 数据来自于 Channel,至于『到哪里去』这里就用埋点的 key 来表明,比如是 ChannelClick。频道主页在 APP 中入口众多,即使在不考虑埋点的情况下,一个统一的入口也是必要的extension UIViewController { func pushToChannelDetailController(_ id: String?) { // … }}显然这样的方法根本无法满足埋点上的需求,改造一下:extension UIViewController { func pushToChannelDetailController(_ model: Channel?) { // … let value = [ “module_name” : model.module_name, “channel_id” : model.id, “channel_name”: model.name, “page_name”: self.pageName ] SensorsAnalyticsSDK.sharedInstance()?.track(key, withProperties: value) }}需要注意的是大多数情况下入口函数只接受一个具象参数是行不通的,因为随着项目的开发业务的迭代总有一些其他的模型被加入,它们同样带有能够跳转至频道主页的 id 属性。还有 pageName 映射:extension UIViewController { var pageName: String { switch self { case is ChannelDetailController: return “频道主页” } }}最后在具体的跳转处设置 module_name@objc func buttonAction( sender: Any) { model.module_name = “Header” pushToChannelDetailController(model)}整体看下来虽然可以应付点击进入频道主页的埋点,但是还是存在以下问题入口函数不够抽象在实际开发中接收不同的数据模型跳转到同一个页面的情况应该不少见,并且入口函数也是因为埋点把参数从相对抽象的 String 替换成了具象的 Channel,所以抽象 model 是首先要做的。无论接受什么类型参数,传给 ChannelDetail 还是 id,那么让一个只有带有 id 属性的 protocol 去约束模型再合适不过了。protocol CommonModelType { var id: String { get }}然后让 Channle 遵守这个协议,利用 extension 是为了看起来更解耦extension Channel: CommonModelType{}这时候入口函数就是这样func pushToChannellDetail(_ model: CommonModelType?)可以接受任何有 id 属性的模型。为了更抽象,甚至可以让 String 也遵守这个协议extension String: CommonModelType { var id: String { return self }}当然不同其他可以用来跳转到频道主页的模型都可以这样约束。数据提供方式不够优雅埋点数据除了 pageName 不属于 model 以外,其他都属于 model 本身的属性(module_name 属于额外添加),所以和参数一样,同样用 protocol 约束 Channel ,让 Channel 拥有一个直接用于提供数据的属性。protocol AnalyticsModelType { var analytics: [String: Any] { get }}让 Channel 同时遵守这两个协议,并且添加 analytics 属性extension Channel: CommonModelType, AnalyticsModelType { var analytics: [String : Any] { let value = [ “module_name” : module_name, “channel_id” : id, “channel_name”: name ] return value }}最后完整的入口函数是这样的extension UIViewController { func pushToChannellDetail(_ model: CommonModelType?) { guard let model = model else { return } let viewController = ChannelDetailViewController() viewController.id = model.id navigationController?.pushViewController(viewController, animated: true) guard let value = model as? AnalyticsModelType else { return } var properties = value.analytics properties[“page_name”] = pageName SensorsAnalyticsSDK.sharedInstance.track(key: “ChannelClick”, properties: properties) }}总的来说入口函数够抽象,无论后期增加多少种模型只要它遵循CommonModelType即可,甚至对于不熟悉项目的人来说直接传入 id 也是可以正常跳转的。埋点的细节也被隐藏到了入口函数内,而需要上报的数据又由相应的模型负责提供只要它遵循AnalyticsModelType即可。未完待续… ...

March 6, 2019 · 2 min · jiezi

嵌套滚动效果实现讨论

本文要讨论的是类似于即刻、淘票票首页,抖音、简书个人主页这样的嵌套滚动效果,事实上网上已经有很多的相关的文章,比如:嵌套UIScrollview的滑动冲突解决方案iOS 嵌套UIScrollview的滑动冲突另一种解决方案多层 UIScrollView 嵌套滚动解决方案而且绝大多数的文章都是从如何解决手势冲突出发给出相应的解决方案,原因是他们大多数都采用了三级 Scrollview 的解决方案,如下图蓝色视图:一级 ScrollView红色视图:HeaderView绿色视图:MenuView橘色视图:二级 ScrollView黑色、深黑、浅黑:三级 ScrollView可以看到三级 ScrollView 和 一级 ScrollView都需要在纵向滚动,所以重点要解决的就是这里的滚动冲突,具体的细节我就不再赘述,大家还可以参考HGPersonalCenter这个项目,里面有详细的注释。之所以在前面给出了四个例子,是因为淘票票和简书采用的是上面提到的方案,而抖音和即刻两个则不是,并且即刻在体验上更完美,这个后面会讲到。简单粗暴的用越狱手机+Reveal验证下淘票票上层的 MVNestTableView:一级 ScrollView中间的 UIScrollView:二级 ScrollView下层的 MVNestTableView:三级 ScrollView当然通过点击状态栏看也可以粗略判断实现方式,比如淘票票在点击状态栏后视图只会滚动到子 ScrollView 的顶部而不是最外面 ScrollView 的,简书虽然滚动到最外层的顶部但效果明显不够自然,原因就是三级 ScrollView 在纵向没有延伸到顶部。抖音和即刻在点击状态栏返回到顶部的效果非常自然,所以有理由相信它们在实现上不同于上述方案,那么抖音和即刻的实现方式具体有什么不同?同样的用Reveal看下即刻的视图结构从整体结构上来看即刻只有二级 ScrollView,所以在纵向上 ChildScrollView 会完全接管手势,横向滚动时又由 MainScrollView 控制,这样子带来的好处在于无需关心手势冲突问题,但要实现前面提到的效果还必须处理是以下问题:HeaderView 和 MenuView 的位置需要根据 ChildScrollView 的滚动而改变在切换的 Tab 的时候需要同步下一个 ChildScrollView 的 offsetChildScrollView 必须在顶部留出 HeaderView 和 MenuView 高度总和的空白区域HeaderView 不能拦截滚动手势在这里就不给出具体的实现细节,文章后面最后有通过两种方案实现的开源库,欢迎 Star。前面提到的即刻和抖音采用的都是这种二级 ScrollView 的方案,但即刻在体验上更好,比如抖音的个人主页如果手指开始滚动的地方有可交互的控件(Tab栏),那么这时候滑动是会失效的,还有在切换Tab后将视图下拉滚动到顶部然后返回到之前的Tab页,抖音是直接返回到了原始的位置而即刻还是能保留之前进度。头部滚动失效解决方案即刻为了达到完美的效果,在每个 ChildScrollView 顶部都添加了 HeaderView 和 MenuView,这样子作为一个整体,即使开始触摸的地方有可交互控件也可以上下滚动。然后在左右滑动的时又让ChildScrollView 内的 HeaderView 和 MenuView 隐藏,当停止滚动的时让原本在外层 ScrollView 内的 HeaderView 和 MenuView 显示。保留进度解决方案关于保留进度首先要做的就是判断当前 ChildScrollView 是不是处于一种特殊状态,这种状态就是 offset.y的值是否大于 HeaderView 的偏移量,然后再通过判断 ChildScrollView 当前的滚动方向,来决定是否要调整 HeaderView 和 MenuView 的位置。对比两个方案最终的实现各有优缺点方案一优点:无障碍配合使用第三方下拉刷新库ChildViewController 无需额外设置缺点:实现较复杂滚动有细微的停顿感切换Tab不能保留进度点击状态栏不能返回到顶部方案二优点:实现简单滚动无停顿感切换Tab可保留进度点击状态栏可返回到顶部缺点ChildViewController 需要额外的设置(ChildScrollView 必须在顶部留出 HeaderView 和 MenuView 高度)下拉刷新只能在 ChildViewController 内实现这里要提的是,由于方案二中 MainScrollView 并不会在纵向有滚动,所以下拉刷新必须放在 ChildViewController 内实现,但又因为 HeaderView 和 MenuView 需要根据 ChildScrollView 的偏移而移动,在配合MJRefresh时它们的偏移有明显的Bug(在本文发布前我并没深究解决方案),或许即刻也是因为这个原因而采用上面提到的解决办法。方案一开源库:Aquaman方案二开源库:Shazam上面两个解决方案中的 MenuView 都设计成了交由开发者实现,因为即使集成各种样式的也难满足设计上的千奇百怪的要求,参考我的Demo就能很快实现一个自己想要的效果。 ...

March 6, 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

UIPullRefreshFlash模块demo示例

UIPullRefreshFlash 模块概述:UIPullRefreshFlash模块对引擎新推出的下拉刷新接口进行了一层封装,app可以通过此模块来实现带炫酷动画效果的下拉刷新功能。使用此模块,在用户下拉时本模块会随用户下拉高度而放大缩小下拉出的提示图标,同时会随用户下拉高度播放一组关键帧图片,该图片数组是通过 api.setCustomRefreshHeaderInfo 接口以图片数组(参考下文 pull 参数)的形式传给模块的,每下拉一定距离(阈值/图片数量),播放一帧图片;当下拉高度达到一定阈值后触发加载事件:进入加载状态时,刷新提示图标开始播放加载关键帧图片数组,此时图片每帧间隔为 50 毫秒,同时将下拉刷新事件回发给前端。前端得到下拉刷新事件后开始加载数据;数据加载完毕,调用接口 api.refreshHeaderLoadDone 以停止加载状态;详见模块文档:https://docs.apicloud.com/Cli…使用攻略:①对于 APICloud 平台上的普通模块,在相应接口调用前需要先require该模块,但由于本模块是基于引擎下拉刷新功能扩展的模块,所以本模块使用方法比较特殊。可以不必require模块,改为在 config.xml 文件内配置模块。config.xml 文件配置示例如下:<preference name=“customRefreshHeader” value=“UIPullRefreshFlash”/>复制代码在 config.xml 配置后,则本模块为全局对象,可以在任意可弹动的窗体(frame、window)中调用 api.setCustomRefreshHeaderInfo 接口设置该下拉刷新样式,以及开始、停止刷新加载状态(api.refreshHeaderLoading、api.refreshHeaderLoadDone)。②若想在不同的 window 或 frame 使用不同的下拉刷新模块,开发者可以在 window 或 frame 打开时传入参数 customRefreshHeader:‘下拉刷新模的块名’,以指定该窗体的下拉刷新模块。{api.openFrame({ name: ‘UIPullRefreshFlash-con’, url: ‘./UIPullRefreshFlash-con.html’, customRefreshHeader: ‘UIPullRefreshFlash’, bounces: true, rect: { x: offset.l, y: offset.t + offset.h, w: offset.w, h: bodyHeight - offset.h } });}复制代码UIPullRefreshFlash模块有三个接口:setCustomRefreshHeaderInfo:配置下拉刷新样式;refreshHeaderLoading:手动开始下拉刷新的加载状态,注:下拉刷新状态亦可通过用户下拉到阈值自动触发;refreshHeaderLoadDone:手动停止下拉刷新的加载状态;本文出自APICloud官方论坛,感谢论坛版主粉红顽皮新的分享。

March 1, 2019 · 1 min · jiezi

刚刚,阿里开源 iOS 协程开发框架 coobjc!

阿里妹导读:刚刚,阿里巴巴正式对外开源了基于 Apache 2.0 协议的协程开发框架 coobjc,开发者们可以在 Github 上自主下载。coobjc是为iOS平台打造的开源协程开发框架,支持Objective-C和Swift,同时提供了cokit库为Foundation和UIKit中的部分API提供了协程化支持,本文将为大家详细介绍coobjc的设计理念及核心优势。开源地址https://github.com/alibaba/coobjciOS异步编程问题从2008年第一个iOS版本发布至今的11年时间里,iOS的异步编程方式发展缓慢。基于 Block 的异步编程回调是目前 iOS 使用最广泛的异步编程方式,iOS 系统提供的 GCD 库让异步开发变得很简单方便,但是基于这种编程方式的缺点也有很多,主要有以下几点:容易进入"嵌套地狱"错误处理复杂和冗长容易忘记调用 completion handler条件执行变得很困难从互相独立的调用中组合返回结果变得极其困难在错误的线程中继续执行(如子线程操作UI)难以定位原因的多线程崩溃(手淘中多线程crash已占比60%以上)锁和信号量滥用带来的卡顿、卡死针对多线程以及尤其引发的各种崩溃和性能问题,我们制定了很多编程规范、进行了各种新人培训,尝试降低问题发生的概率,但是问题依然很严峻,多线程引发的问题占比并没有明显的下降,异步编程本来就是很复杂的事情,单靠规范和培训是难以从根本上解决问题的,需要有更加好的编程方式来解决。解决方案上述问题在很多系统和语言开发中都可能会碰到,解决问题的标准方式就是使用协程,C#、Kotlin、Python、Javascript 等热门语言均支持协程极其相关语法,使用这些语言的开发者可以很方便的使用协程及相关功能进行异步编程。2017 年的 C++ 标准开始支持协程,Swift5 中也包含了协程相关的标准,从现在的发展趋势看基于协程的全新的异步编程方式,是我们解决现有异步编程问题的有效的方式,但是苹果基本已经不会升级 Objective-C 了,因此使用Objective-C的开发者是无法使用官方的协程能力的,而最新 Swift 的发布和推广也还需要时日,为了让广大iOS开发者能快速享受到协程带来的编程方式上的改变,手机淘宝架构团队基于长期对系统底层库和汇编的研究,通过汇编和C语言实现了支持 Objective-C 和 Swift 协程的完美解决方案 —— coobjc。核心能力提供了类似C#和Javascript语言中的Async/Await编程方式支持,在协程中通过调用await方法即可同步得到异步方法的执行结果,非常适合IO、网络等异步耗时调用的同步顺序执行改造。提供了类似Kotlin中的Generator功能,用于懒计算生成序列化数据,非常适合多线程可中断的序列化数据生成和访问。提供了Actor Model的实现,基于Actor Model,开发者可以开发出更加线程安全的模块,避免由于直接函数调用引发的各种多线程崩溃问题。提供了元组的支持,通过元组Objective-C开发者可以享受到类似Python语言中多值返回的好处。内置系统扩展库提供了对NSArray、NSDictionary等容器库的协程化扩展,用于解决序列化和反序列化过程中的异步调用问题。提供了对NSData、NSString、UIImage等数据对象的协程化扩展,用于解决读写IO过程中的异步调用问题。提供了对NSURLConnection和NSURLSession的协程化扩展,用于解决网络异步请求过程中的异步调用问题。提供了对NSKeyedArchieve、NSJSONSerialization等解析库的扩展,用于解决解析过程中的异步调用问题。coobjc设计最底层是协程内核,包含了栈切换的管理、协程调度器的实现、协程间通信channel的实现等。中间层是基于协程的操作符的包装,目前支持async/await、Generator、Actor等编程模型。最上层是对系统库的协程化扩展,目前基本上覆盖了Foundation和UIKit的所有IO和耗时方法。核心实现原理协程的核心思想是控制调用栈的主动让出和恢复。一般的协程实现都会提供两个重要的操作:Yield:是让出cpu的意思,它会中断当前的执行,回到上一次Resume的地方。Resume:继续协程的运行。执行Resume后,回到上一次协程Yield的地方。我们基于线程的代码执行时候,是没法做出暂停操作的,我们现在要做的事情就是要代码执行能够暂停,还能够再恢复。 基本上代码执行都是一种基于调用栈的模型,所以如果我们能把当前调用栈上的状态都保存下来,然后再能从缓存中恢复,那我们就能够实现yield和 resume。实现这样操作有几种方法呢?第一种:利用glibc 的 ucontext组件(云风的库)。第二种:使用汇编代码来切换上下文(实现c协程),原理同ucontext。第三种:利用C语言语法switch-case的奇淫技巧来实现(Protothreads)。第四种:利用了 C 语言的 setjmp 和 longjmp。第五种:利用编译器支持语法糖。上述第三种和第四种只是能过做到跳转,但是没法保存调用栈上的状态,看起来基本上不能算是实现了协程,只能算做做demo,第五种除非官方支持,否则自行改写编译器通用性很差。而第一种方案的 ucontext 在iOS上是废弃了的,不能使用。那么我们使用的是第二种方案,自己用汇编模拟一下 ucontext。模拟ucontext的核心是通过getContext和setContext实现保存和恢复调用栈。需要熟悉不同CPU架构下的调用约定(Calling Convention). 汇编实现就是要针对不同cpu实现一套,我们目前实现了 armv7、arm64、i386、x86_64,支持iPhone真机和模拟器。Show me the code说了这么多,还是看看代码吧,我们从一个简单的网络请求加载图片功能来看看coobjc到底是如何使用的。下面是最普通的网络请求的写法:下面是使用coobjc库协程化改造后的代码:原本需要20行的代码,通过coobjc协程化改造后,减少了一半,整个代码逻辑和可读性都更加好,这就是coobjc强大的能力,能把原本很复杂的异步代码,通过协程化改造,转变成逻辑简洁的顺序调用。coobjc还有很多其他强大的能力,本文对于coobjc的实际使用就不过多介绍了,感兴趣的朋友可以去官方github仓库自行下载查看。性能提升我们在iPhone7 iOS11.4.1的设备上使用协程和传统多线程方式分别模拟高并发读取数据的场景,下面是两种方式得到的压测数据。测试机器:iPhone7 iOS11.4.1数据文件大小:20M协程最多使用线程数:4数据测试结果(统计的是所有并发访问结束的总耗时):从上面的表格我们可以看到使用在并发量很小的场景,由于多线程可以完全使用设备的计算核心,因此coobjc总耗时要比传统多线程略高,但是由于整体耗时都很小,因此差异并不明显,但是随着并发量的增大,coobjc的优势开始逐渐体现出来,当并发量超过1000以后,传统多线程开始出现线程分配异常,而导致很多并发任务并没有执行,因此在上表中显示的是大于20秒,实际是任务已经无法正常执行了,但是coobjc仍然可以正常运行。我们在手机淘宝这种超级App中尝试了协程化改造,针对部分性能差的页面,我们发现在滑动过程中存在很多主线程IO调用、数据解析,导致帧率下降严重,通过引入coobjc,在不改变原有业务代码的基础上,通过全局hook部分IO、数据解析方法,即可让原来在主线程中同步执行的IO方法异步执行,并且不影响原有的业务逻辑,通过测试验证,这样的改造在低端机(iPhone6及以下的机器)上的帧率有20%左右的提升。优势简明概念少:只有很少的几个操作符,相比响应式几十个操作符,简直不能再简单了。原理简单:协程的实现原理很简单,整个协程库只有几千行代码。易用使用简单:它的使用方式比GCD还要简单,接口很少。改造方便:现有代码只需要进行很少的改动就可以协程化,同时我们针对系统库提供了大量协程化接口。清晰同步写异步逻辑:同步顺序方式写代码是人类最容易接受的方式,这可以极大的减少出错的概率。可读性高:使用协程方式编写的代码比block嵌套写出来的代码可读性要高很多。性能调度性能更快:协程本身不需要进行内核级线程的切换,调度性能快,即使创建上万个协程也毫无压力。减少卡顿卡死: 协程的使用以帮助开发减少锁、信号量的滥用,通过封装会引起阻塞的IO等协程接口,可以从根源上减少卡顿、卡死,提升应用整体的性能。总结程序是写来给人读的,只会偶尔让机器执行一下。——Abelson and Sussman基于协程实现的编程范式能够帮助开发者编写出更加优美、健壮、可读性更强的代码。协程可以帮助我们在编写并发代码的过程中减少线程和锁的使用,提升应用的性能和稳定性。本文作者:淘宝技术阅读原文本文来自云栖社区合作伙伴“ 阿里技术”,如需转载请联系原作者。

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

YYCache 源码学习(一):YYMemoryCache

其实最近是在重新熟练Swift的使用,我想出了一个比较实用的方法,那就是一边看OC的项目,看懂之后用Swift实现一遍。这样既学习了优秀的源码又练习了Swift,一举两得。之前看过几篇文章是剖析YYKit里面的一些小模块,对源码对一些解读。不得不说作者ibireme的设计思维和技术细节的处理都非常的棒。所以就选了YYKit里面的一些小模块入手。YYCache主要分为了两部分:YYMemoryCache内存缓存和磁盘缓存YYDiskCache。平常使用的时候我们一般都只直接操作YYCache这个类,他是对内存缓存和磁盘缓存的封装。这篇文章主要是讲解YYCache模块里面的YYMemoryCache部分。API我们先可以看一下YYMemoryCache的.h文件,浏览一起属性和方法。大多数的都可以见名知意的。@interface YYMemoryCache : NSObject#pragma mark - Attribute///=============================================================================/// @name Attribute///=============================================================================@property (nullable, copy) NSString *name;@property (readonly) NSUInteger totalCount;@property (readonly) NSUInteger totalCost;#pragma mark - Limit///=============================================================================/// @name Limit///=============================================================================@property NSUInteger countLimit;@property NSUInteger costLimit;@property NSTimeInterval ageLimit; //过期时间@property NSTimeInterval autoTrimInterval;//自动处理的间隔时间@property BOOL shouldRemoveAllObjectsOnMemoryWarning;@property BOOL shouldRemoveAllObjectsWhenEnteringBackground;@property (nullable, copy) void(^didReceiveMemoryWarningBlock)(YYMemoryCache *cache);@property (nullable, copy) void(^didEnterBackgroundBlock)(YYMemoryCache *cache);@property BOOL releaseOnMainThread;@property BOOL releaseAsynchronously;#pragma mark - Access Methods///=============================================================================/// @name Access Methods///=============================================================================- (BOOL)containsObjectForKey:(id)key;- (nullable id)objectForKey:(id)key;- (void)setObject:(nullable id)object forKey:(id)key;- (void)setObject:(nullable id)object forKey:(id)key withCost:(NSUInteger)cost;- (void)removeObjectForKey:(id)key;- (void)removeAllObjects;#pragma mark - Trim///=============================================================================/// @name Trim///=============================================================================- (void)trimToCount:(NSUInteger)count;- (void)trimToCost:(NSUInteger)cost;- (void)trimToAge:(NSTimeInterval)age;@end我把乱七八糟的注释都删掉了,这样可以直观的来看,api分为四个部分,前两部分都是一些属性,后面两个是方法。第一个Attribute部分是YYMemoryCache类储存的一些基本的属性:name,totalCount(储存对象的总个数),totalCost(储存的总占内存)。Limit部分是一些限制条件,就不一一的说了,单说一个releaseOnMainThread这个属性,可能会有因为,如果如果能异步释放,为什么还要强制去主线程释放呢 ? 这是因为有一些类,像UIView/CALayer这种是要在主线程中释放的,源码注释中也有提到。第三部分就是一些跟储存相关的方法,最后一部分就是根据限制条件修剪处理内存的方法了 ~.m代码剖析LRU 缓存淘汰算法YYMemoryCache是提供了内存修剪的方法的,既然有修剪,那么我们得有一个算法来确定是修剪掉哪一些。YYMemoryCache 和 YYDiskCache 都是实现的 LRU (least-recently-used) ,即最近最少使用淘汰算法。具体怎么样实现我们往后再说。实现缓存方式.m的最前面是实现了两个内部类_YYLinkedMap和_YYLinkedMapNode。 可以看出具体的缓存方法是通过一个双向列表和散列容器来实现的。_YYLinkedMap中给出来操作结点的方法- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;- (void)bringNodeToHead:(_YYLinkedMapNode *)node;- (void)removeNode:(_YYLinkedMapNode *)node;- (_YYLinkedMapNode *)removeTailNode;- (void)removeAll;具体细节剖析先总起来说一下这些实现的代码,其实很容易读懂,就是通过一个链表的形式来处理缓存的数据,添加缓存的时候,就往链表的尾部添加一个节点,(通过节点来表示我们实际要储存的数据),如果要根据限制条件修剪内存的话,也是循环的删除尾部的那个节点,直到符合限制条件。那我们的LRU 缓存淘汰算法具体怎么使用,我发现在每一个读取了一个数据之后,会把这个数据在链表中对应的结点移动到头部,这样在大概率的情况下使用频率高的缓存数据会在链表的前面。所以修剪的时候可以从尾部修剪。在具体的实现代码中,作者有很多很亮眼的操作,我们来欣赏一下。1.定时修剪内存// 根据限制条件修剪内存的占用 并根绝设定的时间递归调用- (void)_trimRecursively { __weak typeof(self) _self = self; //注意这个dispatch_after后面使用的dispatch_get_global_queue 异步 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_autoTrimInterval * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{ __strong typeof(_self) self = _self; if (!self) return; [self _trimInBackground]; [self _trimRecursively]; });}这个就是通过一个延时来调用修剪内存的方法,然后在递归调用本身。注意的是dispatch_after后面使用的dispatch_get_global_queue 来进行异步操作。2.修剪内存的逻辑- (void)_trimToCost:(NSUInteger)costLimit { BOOL finish = NO; pthread_mutex_lock(&_lock); if (costLimit == 0) { [_lru removeAll]; finish = YES; } else if (_lru->_totalCost <= costLimit) { finish = YES; } pthread_mutex_unlock(&_lock); if (finish) return; NSMutableArray *holder = [NSMutableArray new]; while (!finish) { if (pthread_mutex_trylock(&_lock) == 0) { if (_lru->_totalCost > costLimit) { _YYLinkedMapNode *node = [_lru removeTailNode]; if (node) [holder addObject:node]; } else { finish = YES; } pthread_mutex_unlock(&_lock); } else { usleep(10 * 1000); //10 ms } } //释放 if (holder.count) { dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue(); dispatch_async(queue, ^{ [holder count]; // release in queue }); }}我们以这个按照占内存大小的限制来修剪内存为例子来看一下具体的实现方法。开始先做几个判断,看是否超过限制需要修剪,不需要修剪就直接return。修剪的过程就跟上面说到的是一样的,判断是否超过内存限制,超过就删掉尾部的结点,如此循环操作,只要符合限制要求。异步释放资源:我们可以看到在removeNode的时候,会使用一个holder数组来接收被移除的这些node,然后最后释放这些结点。为什么要这样做?其实这就是通过异步释放这些资源来减少主线程资源的开销。这里作者在异步中调用了[holder count]; 其实最开始我也不知道这个是什么意思,但是作者标注了release in queue,我猜测是通过调用你这个holder的随便一个方法,让这个异步的线程来管理这个holder,进而通过此异步线程来实现holder中对象的释放。锁的使用:还有一个比较重要的点,为什么使用pthread_mutex_trylock这个方式加锁,然后在失败之后,线程要sleep。这个问题就需要我们去研究一下各种锁了。很惭愧我对锁的了解不是很深刻,但是通过看了大神的博客,有了一些了解。(下面内容引用自大神的博客,文末有地址)作者都是使用的pthread_mutex_t互斥锁,这个锁有一个特性,在多个线程竞争一个资源的时候,除了竞争成功的线程,其他的线程都会被动挂起状态,当竞争成功的线程解锁是,会去主动将挂起的其他线程激活,这个过程包含了上下文切换,CPU抢占,信号发送等开销,很明显,开销有些大。所以作者使用了pthread_mutex_trylock()尝试解锁,若解锁失败该方法会立即返回,让当前线程不会进入被动的挂起状态(也可以说阻塞),在下一次循环时又继续尝试获取锁。这个过程很有意思,感觉是手动实现了一个自旋锁。而自旋锁有个需要注意的问题是:死循环等待的时间越长,对 cpu 的消耗越大。所以作者做了一个很短的睡眠 usleep(10 * 1000),有效的减小了循环的调用次数,至于这个睡眠时间的长度为什么是 10ms, 作者应该做了测试。其他部分其他部分就不一一细说了,作者整体思路很清晰,然后代码逻辑也很好懂,像上面提到的一些细节的处理可见作者的技术水平了。参考https://www.jianshu.com/p/408… ...

February 28, 2019 · 2 min · jiezi

YYCache 源码学习(二):YYDiskCache

整体思路从作者的《YYCache 设计思路》一文中可以看出,作者在设计YYDiskCache之前做了充分的测试:iPhone 6 64G 下,SQLite 写入性能比直接写文件要高,但读取性能取决于数据大小:当单条数据小于 20K 时,数据越小 SQLite 读取性能越高;单条数据大于 20K 时,直接写为文件速度会更快一些。YYDiskCache的磁盘缓存结合使用了文件储存和数据库储存。个人理解:在进行磁盘缓存的时候,会判断要储存数据的大小,如果数据小于20K,则直接存入数据库(数据储存到inline_data字段,此时filename为空)。如果数据大于20K,先把数据以文件形式进行存储,然后再在数据库中储存对应的文件名(此时inline_data为NULL,filename为文件地址),具体的可以结合下文中提到的磁盘缓存的文件结构来看。磁盘缓存的核心类是YYKVStorage,他主要封装了文件储存操作和SQLite数据库的操作。YYDiskCache是对YYKVStorage的封装,抛出的API和内存缓存相似,都有数据读写和修剪内存。磁盘缓存的文件结构/* File: /path/ /manifest.sqlite /manifest.sqlite-shm /manifest.sqlite-wal /data/ /e10adc3949ba59abbe56e057f20f883e /e10adc3949ba59abbe56e057f20f883e /trash/ /unused_file_or_folder SQL: create table if not exists manifest ( key text, filename text, size integer, inline_data blob, modification_time integer, last_access_time integer, extended_data blob, primary key(key) ); create index if not exists last_access_time_idx on manifest(last_access_time); */这个结构我们不需要多说什么,只提一个小点,作者在path路径下面设计了一个/data/和一个/trash/。删除文件是一个比较耗时的操作,在删除文件的时候,先进行文件的移动,然后在一个子线程中处理要删掉的文件,提高了整体的效率。实现 LRU磁盘缓存对缓存淘汰算法的实现就比较简单了,因为每次存储都有对应的数据库记录,而且表中设计了last_access_time这个字段,我们可以直接使用数据库的排序语句就可以找到最不常用的文件了。代码分析1.- (sqlite3_stmt *)_dbPrepareStmt:(NSString *)sql { if (![self _dbCheck] || sql.length == 0 || !_dbStmtCache) return NULL; sqlite3_stmt *stmt = (sqlite3_stmt *)CFDictionaryGetValue(_dbStmtCache, (__bridge const void *)(sql)); if (!stmt) { int result = sqlite3_prepare_v2(_db, sql.UTF8String, -1, &stmt, NULL); if (result != SQLITE_OK) { if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite stmt prepare error (%d): %s", FUNCTION, LINE, result, sqlite3_errmsg(_db)); return NULL; } CFDictionarySetValue(_dbStmtCache, (__bridge const void *)(sql), stmt); } else { sqlite3_reset(stmt); } return stmt;}这个方法是提前生成了sql语句的句柄,可以理解成提前把sql语句编译成字节码留给后面的执行函数(当前不执行)。同时,作者使用_dbStmtCache对语句进行缓存,下次使用时可以更快度的加载出来。2.- (BOOL)_dbClose { if (!_db) return YES; int result = 0; BOOL retry = NO; BOOL stmtFinalized = NO; if (_dbStmtCache) CFRelease(_dbStmtCache); _dbStmtCache = NULL; do { retry = NO; result = sqlite3_close(_db); // 状态为busy或者lock if (result == SQLITE_BUSY || result == SQLITE_LOCKED) { if (!stmtFinalized) { stmtFinalized = YES; sqlite3_stmt *stmt; //sqlite3_stmt *sqlite3_next_stmt(sqlite3 *pDb, sqlite3_stmt *pStmt); //表示从数据库pDb中对应的pStmt语句开始一个个往下找出相应prepared语句,如果pStmt为nil,那么就从pDb的第一个prepared语句开始。 while ((stmt = sqlite3_next_stmt(_db, nil)) != 0) { //释放数据库中的prepared语句资源 sqlite3_finalize(stmt); retry = YES; } } } else if (result != SQLITE_OK) { if (_errorLogsEnabled) { NSLog(@"%s line:%d sqlite close failed (%d).", FUNCTION, LINE, result); } } } while (retry); _db = NULL; return YES;}这个是关闭数据库的方法,_dbStmtCache中缓存了我们使用的句柄,所以首先要释放掉了_dbStmtCache。在真正关闭数据库的代码中使用了do-while循环,因为一次访问数据库并不一定成功,数据库可能是busy或者lock的状态,所以要使用一个循环来多次访问。如果为能关闭数据库,作者使用了sqlite3_next_stmt一个个的找出prepared语句,并使用sqlite3_finalize释放了prepared资源(防止内存泄露)。其他的就没什么好说的了,主要就是一些sql语句的用法,这些大家看一下,碰到陌生的api谷歌一下就有了 ~ 具体的文件的操作,比较常用,看起来就容易很多。 ...

February 28, 2019 · 2 min · jiezi

使用Quasar设计Material和IOS风格的响应式网站

GITHUB:使用Quasar设计旅游网站文章链接:使用Quasar设计Material和IOS风格的响应式网站QuasarQuasar是一款基于Vue.js开发的UI框架,可以让你轻松构建网站简洁明快的界面,更重要的是它还能让你轻松做好RWD(响应式网站设计),除此之外还能帮助你加上PWA,使你的网页变成手机上的App。以下内容来自官网Quasar Framework,概括了Quasar的主要特点。Quasar是MIT许可的开源框架(基于Vue),可帮助Web开发人员创建:响应式网站PWA通过Apache Cordova构建移动App多平台桌面应用程序(使用Electron)选择Quasar的5个理由内建了Material及IOS两种主题组件均内建RWD快速响应多样的基础UI组件库自带了Vuex、VueRouter、Vuei18n(多国语系)强大的部署工具安装指南首先安装Node.js和vue-cli,具体安装方法查看官网资料。然后安装Quasar,npm install -g quasar-cli。最后搭建项目:quasar init <folder name>取代main.js的quasar.config.js设置文件:引用Quasar内建的组件,可以不用在每个地方import componentsi18n设置多国语系icons移除注释即可使用开发模式下的HTTPS以及port设置CSS动画设置其他外部插件的设置PWA、manifest等设置quasar.config.jsplugins以前在Vue安装其他的plugin会在main.js里引入,而在Quasar就会取代main.js成为全部配置文件。cssCSS的引入都会放在这个文件,默认的位置/src/css,所需的CSS库已经准备好,可以直接使用。extra这里是设置是否引入quasar-extras的内容。Packagename说明Roboto Fontroboto-fontMaterial主题的建议字体Roboto Font Latin Extendedroboto-font-latin-extMaterial主题的建议字体Material Iconsmaterial-iconsMaterial风格的iconMDI (Material Design Icons)mdiMaterial风格的icon扩展Font Awesomefontawesome自由选择iconIoniconsioniconsionicons的iconAnimate.cssanimations网页组件动画scopeHoiting默认true,用来提升webpack运行时的性能VueRouterMode设置Vue Router的模式,有history、hash两种值。vueCompiler包含两种Vue的编译模式vue runtime+compiler,默认只有runtime-only运行时编译gzip使网站支持gzip的格式。analyze在build时会运行webpack-bundle-analyzer工具。extractCSS提取CSS到文件中。VueLoaderextendWebpack在dev模式中服务器的设置。httpsport设置成指定的port,当quasar在运行dev模式时,遇到相同的port时会自己再+1。open是否在dev指令执行完成后,自动开启此网站的分页在浏览器上。Layoutquasar dev打开初始页面,页面的header和drawer都是在layout/MyLayout.vue里。一些常用的属性:属性取值说明sideString有两个值left,right,决定要出现在左边还是右边overlayBoolean设置侧边栏弹出时是挤压q-page-container还是直接盖在上面content-styleObject设置侧边栏的CSScontent-classString/Object/Array设置侧边栏的classminiBoolean把侧边栏缩小到只有icon这里的CSS要用Object的方式传入。:content-style="{color: ‘red’}“旅游网站-Header演示项目:ToolbarToolbarTitleButton<p class=“codepen” data-height=“265” data-theme-id=“0” data-default-tab=“html” data-user=“whjin” data-slug-hash=“MLNxZN” style=“height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid black; margin: 1em 0; padding: 1em;” data-pen-title=“旅游网站-Header”> <span>See the Pen 旅游网站-Header by whjin (@whjin) on CodePen.</span></p><script async src=“https://static.codepen.io/ass...;></script>ToolbarcolorQuasar内建色:color-palette在/src/css/themes/common.variables.styl里面增加调色板的颜色。inverted让背景色变成白色,然后使原来白色的文字变成设置的背景色。glossy加上玻璃效果QToolbarTitlegt-xs用来设置显示像素高于576px的页面样式。q-mr-mdmr等于margin-rightsm就是size的值为small其他非外观的功能属性:disable,:disable=“true"时按钮禁用label设置按钮的文字type可以是button、submit、reset其中一种loading,值为true显示读取中percentage显示读取的圆圈,要跟loading一起使用dark-percentage用在亮色系的按钮上List&ListItem修改Drawerv-model=“rightDrawerOpen"控制弹出侧边栏的位置content-class=“bg-grey-10"背景色side=“right"按钮设置在多边设置了rightDrawerOpen需要加到data里。export default { name: ‘MyLayout’, data () { return { rightDrawerOpen: false } }}手机端按钮控制显示的class用lt-sm只要像素小于sm(768px)就会显示该区域。设置List和ListItem使用Dark属性使得组件内容在暗色背景下更好显示List中可用组件,这些组件需要自己去配置文件中自行引入。QListHeaderList上的标题QItemSeparator分割线QItemQItemSide可分成左右两侧的区块QItemMain中间的区块QItemTitle区块中的标题旅游网站-Carousel页面轮播建立新的首页在src/pages/下新建Index文件夹,并在此文件夹下新建Index.vue作为首页,删除原来的Index.vue文件。修改Vue Router在src/router/routers.js修改Index.vue的路径。建立轮播的区块在src/pages/Index下新建SectionCarousel.vue,并在Index.vue中引入。然后再template下的q-page中加入section-carousel<template> <q-page> <section-carousel></section-carousel> </q-page></template>开始写轮播官方Carousel在设置文件quasa.config.js中引入:framework: { components: [ ‘QCarousel’, ‘QCarouselSlide’, ‘QCarouselControl’ … ],}加入轮播的图片/页面<template> <q-carousel color=“white” infinite arrows autoplay height=“400px” > <q-carousel-slide img-src=“statics/images/papercut1.jpg”/> <q-carousel-slide img-src=“statics/images/papercut2.jpg”/> </q-carousel></template>加入文字区块Quasar在carousel中有个子组件QCarouselControl用来自定义按钮在页面上。根据官方文档的范例,QCarouselControl要放在QCarousel的最后面,也就是QCarouselSlide的后面。<q-carousel-control position=“center” slot=“control-nav” slot-scope=“carousel” class=“carouselInput”></q-carousel-control>在q-carousel-control中加入内容:<div class=“main”> <h6 class=“title”>新锐旅游网站</h6> <p class=“subtitle”>您身边的好玩专家</p> <p>发现周边好玩的地方,玩得快乐,玩得精彩。</p></div>加上CSS<style lang=“stylus” scoped> .carouselInput { width: 90% } .carouselInput .main { text-align center color: #f50057 } .carouselInput .title { font-size 48px } .carouselInput .subtitle { font-size 24px }</style>调整手机版CSS@media (min-width: 768px) { .carouselInput .title { font-size 48px } .carouselInput .subtitle { font-size 24px }}@media (max-width: 768px) { .carouselInput .title { font-size 24px } .carouselInput .subtitle { font-size 16px }}旅游网站-搜索框加入搜索框input首先到quasar.config.js中引入QInputframework: { components: [‘QInput’]}在<div class=“main”>后面加入q-input内容:<q-input inverted-light color=“white” placeholder=“输入城市/景点 或是想去的地方” :after=”[{icon:‘fas fa-search-location’}]” v-model=“search”></q-input>inverted显示背景color主题颜色after用来显示输入框前后icon最后绑定v-model=“search”,此时需要在data中添加value值,否则会报错。 data() { return { search: ’’ } }调整排版使用Flex CSS调整组件长度。<div class=“row”> <div class=“col-md-2 col-xs-1”></div> <div class=“col-md-8 col-xs-10”> <q-input …></q-input> </div> <div class=“col-md-2 col-xs-1”></div></div>自动填入autocomplete引入QAutocomplete组件:framework: { components: [‘QAutocomplete’]}加入q-autocomplete:<q-input …> <q-autocomplete :static-data="{field: ’label’, list: countries}”/></q-input>static-datafield用于搜索数据的栏位list搜索的数据来源设置静态数据countries: [ {label: ‘广州市’, icon: ‘fas fa-map-marker-alt’}, {label: ‘深圳市’, icon: ‘fas fa-map-marker-alt’}, {label: ‘珠海市’, icon: ‘fas fa-map-marker-alt’}, {label: ‘[网美景点]香山公园,秋季赏枫胜地’, stamp: ‘北京市’}, {label: ‘珠海长隆[海豚剧场]精彩不容错过!精彩变身演出抢先看’, stamp: ‘珠海,长隆’, rightTextColor: ‘pink-13’}]自定义过滤器filter在q-autocomplete中加入filter:<q-autocomplete :static-data="{field: ’label’, list: countries}” :filter=“advFilter”/>引入lodash处理filter。旅游网站-Popover弹出框加入Popover组件在quasar.config.js中引入QPopover。Popoverno-focus不设焦点fit弹出框跟输入框等长v-show="!search"弹框和候选框不同时出现内容排版使用FlexCSS来进行排版。<div class=“row viewList”> <div class=“col-sm-4 col-xs-12”></div> <div class=“col-sm-4 col-xs-12”></div> <div class=“col-sm-4 col-xs-12”></div></div>设配手机端,在CSS底部加入:@media (max-width: 576px) { .viewList { width: 280px }}解决在手机像素下原来Popover不能自动fix的问。这里应该是小于Popover的fix的最小宽度。设置内容(List&Item)列表类直接用list做最快。<div class=“col-sm-4 col-xs-12”> <q-list> <q-list-header>热门目的地</q-list-header> <q-item> <q-item-main>珠海</q-item-main> </q-item> </q-list></div>加入右侧Icon及文字在src/components下新建LIcon.vue,提升组件复用。主要使用了q-icon来引入Font Awesome的icon。在原来的页面引入子组件具体代码:SectionCarousel.vuesrc/components/LIcon.vue旅游网站-Cards卡片建立并引入第二个区块在src/pages/Index下新建SectionCards.vue组件,用来作为卡片区块。在Index.vue中引入SectionCards.vue。区块内版面规划<div class=“row”> <div class=“col-12”><b>本月最精选</b></div> <div class=“col-lg-3 col-sm-6 col-xs-12”>卡片一</div> <div class=“col-lg-3 col-sm-6 col-xs-12”>卡片二</div> <div class=“col-lg-3 col-sm-6 col-xs-12”>卡片三</div> <div class=“col-lg-3 col-sm-6 col-xs-12”>卡片四</div></div>制作卡片卡片内的内容都会大量重复,所以直接把卡片独立成一个组件。在src/components/下新建一个LCard.vue。在quasar.config.js中引入卡片组件Cardsframework: { components: [ ‘QCard’, ‘QCardTitle’, ‘QCardMain’, ‘QCardMedia’, ‘QCardSeparator’, ‘QCardActions’ ]}卡片主要分成三个部分:q-card-media放照片影片的区块q-card-title卡片的标题q-card-main卡片的主内容q-card-actions用来放按钮等操作的区块q-card-separator分隔线在SectionCards.vue中引入LCard.vue。<div class=“col-lg-3 col-sm-6 col-xs-12”> <l-card/></div>import LCard from ‘src/components/LCard.vue’export default { components:{ LCard },}加上Icon继续补上评分和地标的Icon。让LCard的文字能从父组件引入让LCard.vue能够动态获取数据:<p class=“codepen” data-height=“365” data-theme-id=“0” data-default-tab=“html” data-user=“whjin” data-slug-hash=“OqJpKq” style=“height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid black; margin: 1em 0; padding: 1em;” data-pen-title=“Quasar-LCard.vue”> <span>See the Pen Quasar-LCard.vue by whjin (@whjin) on CodePen.</span></p><script async src=“https://static.codepen.io/ass...;></script>在SectionCards设置数据新增一个data数据monthBestList,然后在template模板中使用v-for获取数据并显示。<template v-for="(data,index) in monthBestList”> <div class=“col-lg-3 col-sm-6 col-xs-12 q-pa-sm” :key=“index”> <l-card :title=“data.title” :rate=“data.rate” :comment=“data.comment” :view=“data.view” :locate=“data.locate” :image=“data.image” /> </div></template>调整CSS及layout旅游网站-制作景点详情页在src/pages下新建place文件夹,并在此文件夹下新建Index.vue作为文章的主要页面。设置Vue Router要进行页面导航/切换需要用到Vue Router。在router/routes.js中加入导航:const routes = [ { path: ‘/’, component: () => import(’layouts/MyLayout.vue’), children: [ {path: ‘’, component: () => import(‘pages/Index’)}, {path: ‘Place’, component: () => import(‘pages/Place’)} ] }];在http://localhost:8080/#/place中查看效果。加入视差(Parallax)组件视差(Parallax)在quasar.config.js中引入QParallax组件。<template> <q-page> <q-parallax :src=“localData.image” :height=“400”> <p>{{ localData.title }}</p> </q-parallax> </q-page></template>主题部分的页面排版按照8格+4格进行排版:<div class=“row place-main”> <div class=“col-8”></div> <div class=“col-4”></div></div>CSS补上左右margin 5%让页面看起来不会太满。.place-main { margin-left 5% margin-right 5%}设置左边画面这里要用Quasar的面包屑BreadCrumbs组件。在quasar.config.js中引入:‘QBreadcrumbs’,‘QBreadcrumbsEl’,加上景点信息及评分的排版这里按照8格+4格设定,左侧栏要设为文字靠右对齐。加上景点信息引入LIcon.vue图标组件:<div class=“col-8 info-left”> <l-icon class=“q-mt-sm” :text="‘景点编号:’ + localData.id” :icon="‘fas fa-tag’” :color="‘grey’"/><br> …</div>加上评分评分组件Rating表单组件-Field表单字段(Field)Field的组件有QInput、QSeclet、QDatetime、QChipsInput引入组件在quasar.config.js中引入组件基本范例<q-field label=“信箱”> <q-input suffix="@gmail.com" v-model=“model”/></q-field>label设置标题文字icon设置标题的iconicon-color设置标题icon的颜色helper组件地下的辅助文字error控制组件在错误时会变成红色警示error-label错误时会显示的文字warning控制组件是否为警告状态warning-label同error-labelcount显示目前输入多少文字inset用来为没有icon/label的栏位留空orientation组件的排列方向(水平horizontal/垂直vertical)label-width文字区块的宽度(以12格宽度划分)假设文字的宽度要和输入一样长,则设定为6dark是文字反白,适用在暗色背景下表单组件-Chips InputChips Input<q-chips-input float-label=“兴趣” v-model=“model” />export default { data() { return { model: [] } }}外观属性chips-color改变chips的颜色chips-bg-color改变chips的背景颜色add-icon替换输入时显示在右边的enter按钮icon基本属性prefix加入前缀文字(不影响array内的值)suffix加入后缀文字,可以跟前缀一起用hide-underline移除原本输入框的底线no-parent-field如果外面套有QField,可以避免跟QField的效果连结upper-case自动转大写lower-case自动转小写大部分组件都会重复的基本属性float-label悬浮标题stack-label固定式标题color组件颜色inverted是否有背景色inverted-light改善亮色背景下组件的显示dark改善暗色背景下组件的显示error错误warning警告disable跟readonly类似,但是会有灰键效果事件属性@input(newVal)输入文字的同事就会触发@change(newVal)数组数值改变触发@clear(clearVal)数组被清空时触发@duplicate(val)输入重复的值时触发@add(val)输入时触发@remove({index, value})其中一个组件被删除时触发方法属性(Vue Methods)这里的用法通常都是在组件中加入red属性,然后再其他地方使用this.$refs来对这些组件进行操作。add(value)加入值到组件的数组中remove(index)删除指定索引的值focus()聚焦在组件上select()选择组件clear()清除组件中数组的值<q-chips-input ref=“myChipInput” />addSomething() { this.$refs.myChipInput.add(‘Hello Quasar’)}表单组件-Radio引入组件QRadio,单选框(Radio)与QField一起使用<q-field label=“黄金周去哪玩?” orientation=“vertical”> <q-radio v-model=“model” label=“去杭州” val=“hangzhou”/> <q-radio v-model=“model” label=“去北京” val=“beijing”/> <q-radio v-model=“model” label=“去成都” val=“chengdu”/></q-field>基本属性val存储绑定变量的值label组件上的文字left-label设定为true时,文字会改变显示在选项的左边checked-icon改变选取时的iconunchecked-icon改变未选取时的iconcolor改变组件的颜色keep-color没选取时也会有颜色(默认是灰色)readonly只读属性disable禁用dark在暗色背景时,凸显组件文字no-focus不会触发聚焦事件基本事件@input选取时触发@blur失去焦点(点到其他地方)时触发@focus聚焦(点选该组件)时触发表单组件-Checkbox复选框(Checkbox)引入组件在quasai.config.js中引入QCheckbox。复选框需要绑定数据类型为Array,也需要和QField一起使用。基本属性val数值,加入到v-model绑定的变量中true-value如果model不是数组,在选取时会给model值true,用来取代truefalse-value同上解析indeterminate-value用来替换nulltoggle-indeterminate使点击可以让状态在以上三个中转换表单组件-Toggle切换键Toggle引入组件在quasar.config.js中引入QToggle基本属性val,v-model是Array,会加在Array内icon如果底下两个(checke-icon、unchecked-icon)icon 会被覆盖掉表单组件-Option Group选项组option-group把选项写进一个Array中,然后直接用v-for全部渲染出来。引入组件每一步都是一样的,在quasar.config.js中引入QOptionGroup。基本范例CheckBox<template> <q-field orientation=“vertical” label=“要选购的商品”> <q-option-group type=“checkbox” v-model=“model” :options=“optionList” /> </q-field ></template><script> export default { name: “index”, data() { return { model: [], optionList: [ {label: ‘鸡蛋’, value: ’egg’}, {label: ‘海带’, value: ‘seaweed’}, {label: ‘鸡腿’, value: ’lag’}, {label: ‘牛肉’, value: ‘beef’} ] } } }</script>toggle、radio和checkbox类似,只需要修改type属性值即可表单组件-Datetime时间日期输入框Datetime,有Material和IOS两种风格。引入组件有两个组件需要引入,一个是输入时显示,一个是默认就是显示的。分别为:日期时间输入Datetime Inputframework: { components: [‘QDatetime’]}日期时间选择器Datetime Pickerframework: { components: [‘QDatetimePicker’]}基本操作// Datetime Input<q-datetime v-model=“model” type=“date”/>// Datetime Picker<q-datetime-picker v-model=“model” type=“date”/>基本属性type,一共有三个值,默认是datedate单纯日期time单纯时间datetime时间+日期minimal,不显示标题min max,设置能够选择的日期时间范围<q-datetime v-model=“model” type=“datetime” max=“2019/02/27 2:30”/>format-model存储的时间格式,有四种选择:auto2019-02-27T12:01:00.000+08:00date"2019-02-27T04:00:00.000Z"number1541044860000string2019-02-27T12:01:00.000+08:00format24h设为24时制的时钟基本方法Inputshow()显示输入hide()隐藏输入toggle()切换输入clear()清空modelPickersetYear(val)设置年setMonth(val)设置月setDay(val)设置日setHour(val)设置时setMinute(val)设置分setView(val)设置要显示的模式clear()清空model表单组件-Editor内建的文章编辑器Editor编辑器(WYSIWYG)在quasar.config.js中引入QEditor组件。<q-editor v-model=“model”/>主要设置页面的属性有三个:Toolbar<q-editor v-model=“model” :toolbar="[ [‘bold’,‘italic’,‘strike’,‘underline’], [‘hr’,’link’], [‘fullscreen’], [‘print’] ]"/>Definitionslabel要显示的文字icon要显示的icontip小提示cmd如果不想用默认的功能名称,可以用这个绑回你要的功能handler自定义methods的function名称save: { label:‘保存’, handler: functionName}disable禁用<q-editor v-model=“model” :toolbar="[ [‘bold’,‘italic’,‘strike’,‘underline’], [‘hr’,’link’], [‘fullscreen’], [‘print’] ]" :definitions="{ bold:{label:‘粗体’,icon:null,tip:‘这是粗体’} }"/>Font<q-editor v-model=“model” :toolbar="[ [‘arial’,‘arial_black’,‘comic_sans’], ]" :fonts="{ arial:‘Arial’, arial_black:‘Arial Black’, comic_sans:‘Comic Sans MS’ }"/>基本事件@input输入时触发@fullscreen(true/false)切换全屏时触发表单组件-Knob旋转按钮旋转按钮(Knob)在quasar.config.js中引入QKnob组件。<q-knob v-model=“model” :min=“0” :max=“25”> <q-icon class=“q-mr-xs” name=“volume_up”/> {{model}}</q-knob>属性size调整组件的大小,默认120pxstep数值的间距decimals小数点显示的位数min max最小值和最大值color、track-color颜色、未到达的旋转轴颜色line-width线条的宽度,默认6px事件@input(val)改变时立即触发@change(val)改变时触发@drag-value(val)拖动时就会触发弹窗-Action Sheet操作表(ActionSheet)在quasar.config.js中引入ActionSheet组件,有Material和IOS两种风格。pluginS形式framework: { plugins: [‘ActionSheet’]}components形式framework: { components: [‘QActionSheet’]}作为Plugins的使用方法Vue内this.$q.actionSheet(configObj)Vue外import { ActionSheet } from ‘quasar’;ActionSheet.create(configObj)this.$q.actionSheet({ title: ‘操作选择’, grid: true, //使用格状排版(一排三个) dismissLabel: ‘取消’, //取消按钮的文字 只有IOS主题下可用 默认是cancel actions: [ { label: ‘抓虫’, color: ‘green’, icon: ‘fas fa-bug’, handler() { console.log(‘抓虫大师’) } }, { label: ‘分享到微博’, color: ‘blue’, icon: ‘fas fa-weibo’ }, { label: ‘请人帮忙’, color: ‘black’, icon: ‘fas fa-alipay’ } ]}).then(action => {}).watch(() => {});作为Component的使用方法跟上面的操作基本上一样,只是能够多监听@show和@hide时间。事件@ok点选选项时触发@cancel取消时触发@show显示时触发@hide隐藏时触发@escape-key按Esc时触发弹窗-Dialog基本跟上面的Action Sheet一样的操作方法。<p class=“codepen” data-height=“365” data-theme-id=“0” data-default-tab=“js” data-user=“whjin” data-slug-hash=“GegPVd” style=“height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid black; margin: 1em 0; padding: 1em;” data-pen-title=“弹出窗口-Dialog”> <span>See the Pen 弹出窗口-Dialog by whjin (@whjin) on CodePen.</span></p><script async src=“https://static.codepen.io/ass...;></script>作为Component的使用方法在配置文件中引入QDialog组件。<q-dialog> <span slot=“title”>标题</span> <span slot=“message”>正文</span> <span slot=“body”>主体</span> <span slot=“buttons”>按钮</span></q-dialog>弹窗-Modal模态框(Modal)引入QModal组件,另外加入directives的CloseOverlay。使用按钮或是method将modal设为true才能打开modal。全页显示<q-btn @click=“model=true”>Open</q-btn><q-modal v-model=“model” content-css=“padding: 50px” maximized> <h4>想去哪里玩?</h4> <p>自由行·出国度假</p> <p>泰国、首尔、珠海、九寨沟</p> <q-btn class=“q-mt-lg” color=“primary” @click=“model=false” label=“订购行程” /></q-modal>最小窗口minimized设置position后会自动清除content-css定义的css,所以要在里面多一个div来做padding。<q-modal v-model=“model” minimized> <div style=“padding: 20px”> … </div></q-modal>基本属性minimized,maximized设置窗口最小化或是最大化no-route-dismiss、no-esc-dismiss、no-backdrop-dismiss分别为控制换页、按下Esc、按黑色背景是否会触发开闭事件content-css,content-classes,Modal内的CSS及class,在设置了position后会无效position设置弹窗出来的位置position-classes设置position后就要用这个来设class,默认是items-center,justify-centertransition,enter-class,leave-class可以用自定义的CSS来做出场的动画no-refocus是否让关闭窗口时聚焦回到打开窗口前的最后一个组件Vue方法控制打开关闭窗口的一些方法,要搭配this.$refs.窗口名称来使用。showhidetoggle弹窗-Notify通知框(Notify)<p class=“codepen” data-height=“365” data-theme-id=“0” data-default-tab=“js” data-user=“whjin” data-slug-hash=“wOaGJO” style=“height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid black; margin: 1em 0; padding: 1em;” data-pen-title=“弹窗-Notify”> <span>See the Pen 弹窗-Notify by whjin (@whjin) on CodePen.</span></p><script async src=“https://static.codepen.io/ass...;></script>在Vue外使用import {Notify} from ‘quasar’;Notify.create(‘已保存’);//方式二Notify.create({ message: ‘已保存’});进度条-Ajax Bar&Loading BarAjax栏(Ajax Bar)在quasar.config.js中引入QAjaxBar组件。基本使用Ajax Bar因为在每个页面都会用到,所以放在最上层App.vue。<div id=“q-app”> <router-view></router-view> <a-ajax-bar/></div>position设置组件位置size载入条的宽度,默认4pxcolor默认redreverse使进度方向相反基本事件@start开始动作时触发@stop结束动作时触发基本方法start()stop()Loading Bar进度条-Inner Loading内部加载(Inner Loading)注意事项使用InnerLoading时会作用在relative-position这个class下,如果没有添加这个会变成整页。基本操作index.vue<p class=“codepen” data-height=“265” data-theme-id=“0” data-default-tab=“html” data-user=“whjin” data-slug-hash=“rRVJYL” style=“height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid black; margin: 1em 0; padding: 1em;” data-pen-title=“Quasar-InnerLoading-Index.vue”> <span>See the Pen Quasar-InnerLoading-Index.vue by whjin (@whjin) on CodePen.</span></p><script async src=“https://static.codepen.io/ass...;></script>MyField.vue<p class=“codepen” data-height=“265” data-theme-id=“0” data-default-tab=“html” data-user=“whjin” data-slug-hash=“XGbZEN” style=“height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid black; margin: 1em 0; padding: 1em;” data-pen-title=“Quasar-InnerLoading-MyField.vue”> <span>See the Pen Quasar-InnerLoading-MyField.vue by whjin (@whjin) on CodePen.</span></p><script async src=“https://static.codepen.io/ass...;></script>效果CSS Helpers空间排版CSS Spacing ClassesCSS间距类CSS Spacing Classes基本范例q-ma-xsq作为前缀ma:m类型,margina方向,allxs范围的大小跟flex css一致语法q-[类型][方向]-[大小]类型m(margin向外扩)p(padding向内扩)方法总共有7种选择,除了基本的t(top),r(right),l(left)、b(bottom),a(all)之外,还有两种x(left+right),y(top+bottom)。大小有none,auto(只能用在margin),xs,sm,md,lg,xl。阴影CSS ShadowsCSS阴影(立面图)CSS Shadows可视范围CSS Visibility可视范围CSS Visibility位置排版CSS Positioning ClassesCSS定位类CSS Positioning Classes自定义颜色调色板(Color Palette)在src/css/app.styl文件中自定义全局CSS新增颜色.text-redsp color: #D03F38.bg-redsp background: #D03F38 这里text和bg需要同时设定。使用<q-btn color=“redsp”>Open</q-btn>多国语系I18nQuasar的I18n多国语系(I18n)在quasar.config.js中设置:framework: { i18n: ‘zh-hans’}读取当前语系let lang = this.#q.i18n.getLocal()动态设置Quasar的切换语系不像是传统的vue-i18n直接换就能用,必须重新载入新语系的语系档。<template> <q-btn @click=“setLang(‘zh-hans’)">简体中文</q-btn></template><script> export default { name: “I18n”, methods: { setLang(lang) { import(‘quasar-framework/i18n/${lang}’).then(lang => { this.$q.i18n.set(lang.default) }) } } }</script>Vue-I18nVue-I18n在src/i18n里面,参照已经设定的内容添加自己想要的语系。应用<p>{{$t(‘apple’)}}</p><q-btn @click=“setLang()” :label="$t(‘setting’)"></q-btn>动态切换语系methods: { setLang() { this.$i18m.local = ‘zh-CN’ }} ...

February 28, 2019 · 5 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

NB-IoT 的“前世今生”

作者:个推B2D研发工程师 海晏根据《爱立信2018移动报告》(Ericsson Mobility Report,June 2018)的预测,蜂窝物联网设备连接数将在2023年达到35亿,年增长率达到30%。图片来源:《爱立信2018移动报告》(Ericsson Mobility Report,June 2018)报告中还强调了中国IoT产业的迅猛发展对上述数据产生的巨大影响:文字节选于《爱立信2018移动报告》(Ericsson Mobility Report,June 2018)IoT(Internet of Things,即所谓的物联网)可能早已为大家耳熟,无论是能打电话、看视频的智能手表,还是只有信用卡大小的树莓派,其实都是IoT设备。而在诸多的IoT技术之中,NB-IoT无疑是目前国内最受关注的一个,前有三大运营商积极建设基站、狂砸补贴,后有ofo、摩拜创新研发NB-IoT智能锁。一、NB-IoT是何方神圣?NB-IoT全称是NarrowBand IoT,也称窄带物联网,是由3GPP组织开发的为大范围蜂窝网与设备服务的低功耗广域网络(LPWAN)广播技术,其规范在3GPP Release 13中完全定型,并在Rel-14中做了部分增强(Rel-13 主要是对LTE的改进,并制定了一些物联网规范)。其实在NB-IoT之前已经有了其他IoT技术的存在,曾几何时,我们也使用2/3G网络连接物联网设备,虽然现在看来2/3G网络频谱效率、吞吐率等不高,但是对于数据量极小(相对PC、智能手机以及可以打电话、看视频的智能手表来说)的物联网设备来说已经足够了。随着时间的推移,人们对物联网设备的需求与落后的技术之间的矛盾越来越大:一方面考虑到IoT设备的数量,大量使用GPRS模块成本偏高;另一方面低电池容量的IoT设备也与GPRS的相对复杂的通信协议天生难以相容。此外,考虑到GPRS即将被“退群”,人民群众对新的物联网通信解决方案的需求也日渐高涨。二、通信大厂之间的技术角逐窥见这一技术空白的各通信大厂自然明争暗斗,各种标准不断被推出。2014年5月华为、Vodafone提出了NB M2M技术,而后又进化成NB-CIoT,2015年7月,Nokia、Ericsson、Intel提出了NB-LTE技术,随后3GPP在上述两者之上着手制定标准,并在2016年7月确定标准(在上文提及的Release13中),至此NB-IoT正式诞生。出生于4G时代的NB-IoT复用了一系列LTE的设计,并与GSM、GPRS、LTE等技术良好兼容,它上下行共同使用180 kHz 的最小系统带宽,因此GSM运营商可以重耕GSM频段,将某一个GSM载波频段用于NB-IoT(带宽200 kHz,可以放一个NB-IoT总带宽加两个10 kHz的保护带),而LTE运营商则可以划分一个PRB(Physical Resource Blocks)给NB-IoT(具体部署方式有stand-alone、in-band、guard-band三种)。NB-IoT与LTE的关系,该图来自《A Primer on 3GPP Narrowband Internet of Things (NB-IoT)》为了降低NB-IoT模块的成本与能耗,NB-IoT在复用的同时也对LTE物理信道、UE (User Equipment,终端)处理流程、模块结构做了许多精简,减轻了设备制造复杂度,在降低成本的同时也减少了能耗。此外,NB-IoT的PSM(powersaving mode)和eDRX(echanced Discontinuous Reception)模式也是降低能耗的“秘密武器”,在PSM模式下,设备进入休眠状态不进行通信活动,该模式一般适用于通信频率低的设备;而在eDRX模式下,UE可以更快地进入接收模式,而不需要从休眠模式转换到激活模式,相比DRX模式接受间隔更长,在较高频率通信的设备上可以减少信令,更加省电。为了增强覆盖率,NB-IoT增加重传减低数据速率,引入单个子载波NPUSCH(Narrowband Physical Uplink SharedChannel,窄带物理上行链路共享信道)传输和/2-BPSK调制来维持接近0dB PAPR(Peak to Average Power Ratio,峰值平均比例)来减小PA(power amplifier,功率放大器)功率回退造成对覆盖的影响,一般来说,NB-IoT的MCL(maximumcoupling loss,最大耦合损耗)比LTE (Rel-12)高20db。当然,上述这些也意味着NB-IoT的传输速率不高。但是正是基于这种通信模型,NB-IoT才得以在低成本的情况下,做到单个小区仅使用一个PRB支持上万台设备,而且NB-IoT本就不是为高频高实时需求的设备设计的,它的主要用途正是在低功耗、低成本、高覆盖、低移动性的设备上(关于低移动性这一点上,Rel-13中不支持漫游,Rel-14对移动性进行了增强,目前已经有厂商在进行漫游试验了,期待NB-IoT未来在这方面有所发展)。三、NB-IoT发展的“内忧”和“外患”虽然有各通信大厂的背书,但是NB-IoT在国内的发展却并不是一路绿灯。一方面,模组成本没有下降到预期的水平,智慧农业、智慧城市等对NB-IoT兴趣不足,使得模组出货量达不到预期,没有形成规模效应;另一方面,由于NB-IoT技术刚刚兴起,其相关的基础设施、产业链尚未完善,缺乏专业人才支持,也使得其制造成本难以快速压缩。此外,作为一种蜂窝网络技术,NB-IoT的发展无疑需要运营商的支持。在国内三大运营商之中,中国电信无疑是表现最好的选手,目前中国电信在国内NB-IoT基站已达40万,已经实现“城乡全覆盖”,并且还有对外开放的Wing中国电信物联网开放平台,帮助开发者管理终端。相比之下,虽然中国联通声音较小,但也在去年5月宣布完成了30万个NB-IoT基站升级工作,并与阿里、腾讯、百度等三十多家战略合作伙伴成立了中国联通物联网产业联盟。与前两者相比,中国移动的位置则略显尴尬。由于中国移动之前并没有LTE FDD牌照,这意味着它要么在已分配的GSM频段上部署NB-IoT,要么等待NB-IoT支持TDD,要么等待FDD-LTE的牌照,不管从时间还是从成本上来说,中国移动都不占优势。不过好消息是,中国移动在去年4月份终于获得了FDD-LTE牌照,接下来将会大力发展建设物联网,努力追上其他运营商的步伐,同时对外开放OneNET——中国移动物联网开放平台。虽然三大运营商的动作如此迅速,但是考虑到部分NB-IoT终端的工作环境,目前的NB-IoT网络覆盖率仍然不够。相对较低基站的数量与覆盖率也迫使终端必须抬高发射功率,导致超长电池寿命大打折扣。运营商的角力对于消费者和厂家来说是一件好事,既能降低资费,又能多一些选择,提升消费体验,但是对于开发者来说绝非如此了:多个管理平台意味着必须考虑多种设备多种SDK的情况,有时候同一种设备同一种协议上层的API却不相同。当发生这种情况时,开发人员需要耗费更多的精力和时间。除了需要面对“内忧”,NB-IoT 的“外患”也不少。当姗姗来迟的NB-IoT到来之时,LoRa和Sigfox技术已经攻占了大片城池:目前,有一百多个国家被LoRa网络覆盖,其中法国电信Orange已经宣布实现全法覆盖LoRa。而我国的北京、上海、广州、深圳、南京、苏州、杭州等地也有LoRa网络的覆盖,其中北京市六环以内已经实现了全覆盖,京杭大运河江苏段也实现全段覆盖;Sigfox在欧美地区同样兵强马壮,其在法国的覆盖率已达92%,在美国也已经覆盖一百多个城市。除此之外,NB-IoT也并不是物联网的唯一选择,有时候甚至不是最好的选择。比如能打电话、看视频的智能手表就更适用于使用LTE Cat M1 甚至 LTE Cat 1,而信用卡大小的的树莓派也未必会上NB-IoT,而更可能选择LoRa(毕竟企业自主组网更geek,还无需担忧网络覆盖与资费的问题)。四、NB-IoT技术未来大有作为那么刚出襁褓的NB-IoT该如何应对呢?其实不同的技术有不同的适用场景,比如NB-IoT简直就是为水表、电表行业量身打造的。通过NB-IoT模组,设备仅需在每天汇报数据时唤醒并与基站进行通信,其他大部分时间几乎没有电能消耗,在这种场景中,NB-IoT可以在保证电池寿命的前提下,妥善完成监控电量、水量的任务。切换到烟雾传感器、火警报警器等类似的场景,NB-IoT的优势也十分明显,而对于水文、空气监控来说,NB-IoT模组可以大部分时间休眠,当需要进行较实时的汇报与反馈时,eDRX与DRX模式的作用就显现了,工作在此模式时可以即时反馈变化情况,当不需要实时监控时,又可以切换回PSM,省电工作两不误。除了上面的这些场景,智慧停车、智能门锁、路灯故障监控、二轮车标识定位、农牧业监控、资产管理等领域,NB-IoT也是大有作为。工信部也在去年发布了关于推进移动物联网(NB-IoT)建设发展的通知,要求加快推进移动物联网部署,构建NB-IoT网络基础设施。目前各种IoT技术百花齐放,在市场与政策的双重扶持下,NB-IoT未来的发展前景一片光明。参考文献:[1]Y.-P.Eric Wang, Xingqin Lin, Ansuman Adhikary, Asbjörn Grövlen, Yutao Sui, YufeiBlankenship, Johan Bergman, and Hazhir S. Razaghi,Ericsson Research, Ericsson AB,”A Primer on3GPP Narrowband Internet of Things (NB-IoT)”[2]Ericsson,”EricssonMobility Report”,June 2016.[3]A. Adhikary, X. Lin and Y.-P. E. Wang, “Performanceevaluation of NB-IoT coverage,” Submitted to IEEE Veh. Technol. Conf.(VTC),September 2016, Montréal, Canada.[4]TR 36.888 v12.0.0, “Study on provision of low-costmachine-type communications (MTC) user equipments (UEs) based on LTE,”Jun.2013.[5]Ericsson, “New WI proposal for L1/L2 eMTC and NB-IoT enhancements,” RP-160878, 3GPP TSG RAN Meeting #72, June2016.[6]Vodafone, Huawei, and HiSilicon, “NB-IoT enhancementsWork Itemproposal,” RP-160813, 3GPP TSG RAN Meeting #72, June 2016. ...

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

webToImage (网页转图片)模块试用分享

模块介绍:本模块封装了把 webview 转换成图片的功能。调用本模块的transImage接口,可把当前 webview显示的内容转换成一张图片。注意,本模块只能把当前的webview页面转换为图片,如果当前页面上打开了一个带 UI 界面的模块,会被忽略掉。模块文档注意:本模块目前仅支持iOS。模块不支持WKWebView。openFrame或openWin时,要把useWKWebView参数设置为false。为测试模块效果,写的测试网页包含列表/图片/H5 Video标签。测试结果表明,不支持Video标签。模块使用方法介绍模块只有两个接口:transImage(把模块所依附的当前webview转换为图片)clearCache(当webToImage接口内save参数未传path值,则模块会将转换后的图片保存在缓存目录。调用本接口,可清除本模块产生的所有图片)使网页转为图片,只需调第一个接口即可,示例:var webToImage = api.require(‘webToImage’);webToImage.transImage({ save: { path: ‘fs://webToImage’, // 保存到fs目录的图片,可以使用fs模块清除 name: ‘currentWebviewImage’ } }, function(ret) { console.log(JSON.stringify(ret)); });复制代码本文出自APICloud官方论坛,感谢论坛版主uoaccw的分享。

February 25, 2019 · 1 min · jiezi

web 移动端 ios 浏览器中 animation 动画异常

关键字:animation,ios,移动端,异常解决问题的办法:页面dom加载完毕时延时给dom加上动画类名。即在vue的mounted钩子中用定时器延时100ms左右给需要动画的dom加上类名。我们在写动画的时候常常会遇到添加简单css动画的需求,首选利用animation和@keyframe来实现。当需要一个无限动画的时候,animation相对于transition来说有一个优势。不用js就可以一直执行动画。我在vue项目中的animation动画,在iphone中异常,动画效果紊乱且不明显。解决办法:1.现在样式表中写入动画样式:/箭头本身样式/.next-arrow width: .5rem; position: absolute; left:50%; bottom: 1rem; transition: translate(-50%,0)/css动画样式,此处用sass/.next-arrow-animation animation: 1.2s float infinite ease-in;/动画内容/@keyframes float { 0% { bottom: 1rem; } 100% { bottom: .5rem; } }2.在vue的data中加入对应的控制类名的布尔值data() { return { animation: false }; }3.vue模板中,此处用的pug。 img.next-arrow(:class="{’next-arrow-animation’:animation}")4.在vue的mounted钩子中将animation变为truemounted() { setTimeout(() => { this.animation=true }, 100);}然后就可以看到动画在ios中表现正常。100ms是个经验值,可以改变。如果不是用的vue且遭遇到了同样问题,可用同样思路延时操作dom,给其添加动画类名,即可解决。至于为什么会出现这种情况,我目前没有深入调查。等有时间,如果调查出来会补上。

February 21, 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中的associated object释放时机问题

如果对象A持有对象B,B作为A的associated object,并且表面上B没有其他被强引用的地方,那么对象A被释放时,对象B一定会同时释放吗?大部分情况下是,但真有不是的时候。最近实现代码的时候不小心就碰到了这样的特殊情况。需求需要监听对象A释放(dealloc)并执行对象A的a方法。此时引入对象B,并作为对象A的associated object。A释放时触发B释放,在B的dealloc方法中执行A的a方法。对象B需要一个指向对象A的属性,并声明为unsafe_unretained(或assign),因为weak指针此时已经失效了。示例代码@interface MyObject1 : NSObject@end@implementation MyObject1- (void)foo { NSLog(@“success”);}@end@interface MyObject2 : NSObject@property (nonatomic, unsafe_unretained) MyObject1 *obj1;@end@implementation MyObject2- (void)dealloc { [self.obj1 foo];}+ (instancetype)create { return [[self class] new];}@end@implementation ViewController+ (void)load { [self fun1];}+ (void)fun1 { MyObject1 *mo1 = [MyObject1 new]; @synchronized (self) { MyObject2 *mo2 = [MyObject2 create]; mo2.obj1 = mo1; objc_setAssociatedObject(mo1, @selector(viewDidLoad), mo2, OBJC_ASSOCIATION_RETAIN_NONATOMIC); }}@end问题运行时出现崩溃,unsafe_unretained指针已经野了,和预期的不一样。堆栈是这样的:观察崩溃的堆栈,发现mo2对象是被自动释放池释放了。因为mo1对象是在函数退出时就立即释放,这样导致mo1比mo2先被销毁,mo2访问了无效指针导致了崩溃。这个问题和@synchronized有关系,但目前我还不知道它和arc之间有什么联系。下面给出另一个case,修改一行代码就不会崩溃了:+ (void)fun2 { MyObject1 *mo1 = [MyObject1 new]; MyObject2 *mo2 = [MyObject2 create]; @synchronized (self) { mo2.obj1 = mo1; objc_setAssociatedObject(mo1, @selector(viewDidLoad), mo2, OBJC_ASSOCIATION_RETAIN_NONATOMIC); }}实际上只是把mo2的声明移动到了@synchronized外面,堆栈变成了这样:这时,mo2的释放发生在调用方法的结束时。分析使用Hooper查看汇编代码,观察fun1和fun2的不同。节选出关键部分:fun1:fun2:核心在于:fun1中,创建mo2后调用了retain,fun2中,调用的则是objc_retainAutoreleasedReturnValue。我们再来看看create方法:关键的一行在最后,调用了objc_autoreleaseReturnValue。关于objc_retainAutoreleasedReturnValue和objc_autoreleaseReturnValue,请移步 https://www.jianshu.com/p/2f05060fa377 。大意是,这两个方法成对出现时,可以优化掉[[obj autorelease] retain]这种骚操作。结论在fun1中,由于没有objc_retainAutoreleasedReturnValue,取而代之的是retain,导致对象被放入自动释放池。对于@synchronized为什么会造成不同,我还没有那么深入。因为全局自动释放池会延迟对象的释放,如果代码非常依赖对象的释放时机则会比较危险。我认为这样做是最保险的,创建一个局部自动释放池,保证局部变量在函数结束时立即释放:+ (void)fun3 { MyObject1 *mo1 = [MyObject1 new]; @autoreleasepool { @synchronized (self) { MyObject2 *mo2 = [MyObject2 create]; mo2.obj1 = mo1; objc_setAssociatedObject(mo1, @selector(viewDidLoad), mo2, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } }}参考资料objc_autoreleaseReturnValue和objc_retainAutoreleasedReturnValue函数对ARC的优化 https://www.jianshu.com/p/2f05060fa377本文作者:三豊阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

February 19, 2019 · 1 min · jiezi

一篇文章了解保险的全方面——IT工程师该怎么保护自己

这是一篇面向IT工程师的保险科普文。本文的目的,是为了从IT工程师的角度,用一篇文章——尽可能少的篇幅和逻辑性的排版——来帮助大家入门保险,在不上当受骗的基础上,为自己、以及自己的小家增添一份保障。本文将从保险的分类,保险的坑,我们买保险时该关注的方向,香港保险的优缺,买什么保险五个层面来阐述。如果你对下面的脑图感兴趣,那么本文应该是你需要的。1. 本文诞生的起源2019年是我工作的第5个年头,这5个年头里我体重从140到了170,精力不如以往;同时我还背上了家庭的担子,有房贷车贷,还马上要结婚了。我不禁想为我的生活添加一份保障。毕竟,不怕一万就怕万一。用一句老话说:鸡蛋不要放在一个篮子里。但是当我接触保险的时候,我发现这个领域特别的复杂,以致于我需要花很多的时间去阅读各种各样的文章,去理解其中的门路。在这个理解的过程中,我还发现了很多上当受骗的案例,包括我自己:家人在08年的时候,给我买了一份两全分红型保险。每年交2100块,持续10年,共计21000元。投保后每年返还分红300多,每三年返还本金800块。列一个一元方程组就可以轻松的算出,(800/3+300)*X=21000,需要37年才能拿回本金……分红如此不给力,那保障应该给力吧?但是它却只是个人寿保险,身故或者到期(60年后)只赔偿3万元。试问,这样坑爹的保险还要存在多久?37年前21000元可以在上海买好几套房,今天,它连一平米都买不了。另外一个很经常遇到的,我们买机票的时候,系统都会推荐我们买一个30元的航空意外险,保320万。而实际上,我们每年只要花9.9元,就能保一年,赔200万。一方面我需要一些保险来保障自己现在的生活,一方面,坑爹保险横行。现在没有一个大而全的文章介绍保险中的门门道道,让我们想买保险还不知道买什么。正是在这样的一个背景下,我下决心写出本文。2. 保险的分类保险从大的方向上可以分为财险和人身险。财险就是车险之类的,这不是我们今天的重点。人身险按目的可以分为寿险,重疾险,医疗险和意外险,缺一不可。下面依次阐述它们的作用:2.1 寿险寿险:被保人死了或者全残,保险公司一次性赔一笔钱,否则不赔。寿险又分为终身寿险和定期寿险。终身寿险的意思是保障终身,而人固有一死,所以它最后是肯定要赔付的,所以这样的保险比较贵,主要是用于规避遗产税,而不是保障。定期寿险往往保障到60岁,在这之前的意外或者疾病死亡都会赔付。寿险的要买给家里的经济支柱,即我们自己。寿险对被保人本身是毫无意义的,但是对于被保人的家属,则可以通过这一笔钱有效的度过,因为家中壮劳力的失去而导致的财务危机。所以,不要给小孩和老人买寿险,没有必要。2.2 重疾险重疾险:被保人得了大病,保险公司一次性赔一笔钱,否则不赔。重疾险又分为返本型和消费型。返本型重疾险的意思是如果有50万的保额,得了大病赔50万,没得大病死了也赔50万,但是和终身寿险一个道理,贵。而消费型重疾险物美价廉,得了大病赔50万,没有得大病赔现金价值。消费型重疾险可以选择保到60,70,80岁或者终身。因为年纪大买重疾险会很贵,变得不划算,所以如果财力支持的话,最好买一个终身的消费型重疾险。另外,关于重疾保障,国家强制规定了25种重疾,所有的重疾险必须包括它们。而这25种重疾,占了重疾理赔中的95%。所以对于重疾的种类不需要担心。需要关注的是轻症。轻症是没那么严重的重疾。现在的重疾险,往往还包含轻症的赔付。得了轻症也是要治的,如果没有轻症的赔付就太糟了。所以这一块要格外注意。2.3 医疗险医疗险:被保人得了病,对治病花的钱按比例赔付,没生病则不赔付。医保其实就是一种医疗险,但是医保的额度比较低,适合保小病;商业医疗险的保额高,能保证得大病时有钱可治。那是不是医疗险和重疾险二者择一即可?并不是。医疗险是治病花了多少钱赔多少钱,还有免赔额和赔付比例,对于生病导致的误工是不赔付的。但是重疾险是一次性赔付一笔钱,能保障被保人在生病期间的生存质量。若是因疾病导致了残疾,被保人也能利用重疾险开始一段新的生活。2.4 意外险意外险:被保人受到外来的,突发的,非本意的,非疾病的伤害,保险公司会进行赔付,否则不赔付。意外险因为价格便宜,往往都按照综合意外险捆绑销售,即意外身故,意外伤残和意外治疗。对于成年人,意外伤残比较重要。寿险也能保障身故,医疗险也能保障医疗费,但是意外残疾只有意外险可以保障。而对于老年人和孩子,意外医疗更加重要。3. 保险的坑保险的水都是很深的,条款那么多,稍微一不小心就会找了道。我在这里总结了一些常见的坑:3.1 组合保险谨慎购买现在往往有不同的人身保险捆绑销售,比如说重疾险+意外险。这类保险尽管看起来很方便,但是购买的时候要擦亮眼睛。因为这类保险往往是只赔付一次的。比如说你先出了个意外,以后得重疾它就不赔付了。所以,除非是真的物超所值,才值得购买。而一般来说,分开购买保险的保额更高,保费更低,比组合购买更加划算。3.2 分红保险不要购买保险的本质还是为了用少量的钱做最大的保障,而不是为了投资。因为保险的期限往往很长,用保险来投资很难跑过货币贬值,像我在本文开头中受到的骗一样。另外,分红保险都会强调一句不保障收益,所以不要被销售员给忽悠了。如果想投资,请通过正规的理财渠道。3.3 重疾险的重疾不是越多越好国家已经规定了25种重疾必须包括在重疾险里,它们涵盖了95%的重疾赔付。如果有保单打着多加重疾多收费的名头,就要注意了。另外轻疾因为容易发生,还是要多覆盖一点比较好。3.4 百万医疗险卡单项保额现在市面上百万医疗险很多,但是有的百万医疗险名义上是百万,实际上通过卡单项保额让你根本得不到百万的赔偿,例如恶性肿瘤只赔10万。对于这样的医疗险,一定要擦亮双眼。3.5 便宜意外险却卖千元这个就没什么好说的了,总有些高额意外险却卖的很贵,故意坑人,如同买机票时推荐的航空意外险一样。选择正确的意外险即可。4. 怎么买保险在本文上面的两个部分,我们介绍了保险的种类和它们之中常用的伎俩。本部分则会从自身的角度告诉你如何选择一个适合自己的保险。4.1 性价比保险——用最少的钱做最大的保障。肯定要优中选优,防止上当受骗,找到最合适自己的保额和保费。4.2 保额不同的保险保额的额度不同。一般来说,寿险的保额要能覆盖贷款,对于没有贷款的家庭,则要保证家庭三年的生活开支重疾险的保额在自己的经济实力内尽可能高一些医疗险的保额要能覆盖一般大病的治疗费用意外险的保额对于成人和高危险群体要高一些4.3 保费还是那句话,保险是一种保障,不是一种投资,也不是一种负担。家庭的总保费应控制在家庭收入的10%以下。4.4 一次或者多次保障重疾险有一次或者多次保障的选择。多次保障肯定更好,但是也更贵。另外,得了重病是否能治好,从而获得第二次保障的资格也很难说。这个要见仁见智,依据自己的情况考虑4.5 赔付后能否续保这是医疗险的特性。当前市面上没有任何一款保证终身续保的医疗险。但是生了病保证第二年能正常续保特别重要。所以,尽可能选择能保证续保6年的医疗险。4.6 健康告知是否严格购买保险不是随便买的,如果不符合保险的条件,即使购买了保险,保险公司也会拒绝赔付。所以买保险前一定要看好保险的健康告知,如果不符合相应的条件则不能购买。4.7 免责条款是否合理不管什么样的保险,总会有免责的条款。比如说寿险就不会对我们所说的“作死”行为进行赔付。但是有些意外险对于高空坠落也不赔付,来防止骗保。这样的保险就不能完整的保障我们的权益,要谨慎购买。5. 售卖地2017年的时候非常流行去香港买保险,大家都说香港保险有四大优势:保费低、分红高、保障多、理赔易。实际上香港保险的定价确实存在优势,但是这几年随着内地的保险,尤其是网络保险的兴起,这种优势已经在逐渐缩小。另外,由于香港保险采取严进宽出,所以付港办保险很容易因为不符合健康告知而造成拒绝赔付。而大陆其实是存在通融赔付的,但是香港几乎不存在。最后,去香港买保险其实也有赴港的隐形成本,再加上找不到合适代理人的风险,综合考虑并不一定靠谱。6. 买什么保险利益相关:没有利益,纯友情推荐不同的人有不同的情况,适合的保险也不同。这里推荐我了解这些保险内容的几个微信公众号,它们都有各种保险的评测。大家也可以去了解到自己合适的,然后去官网等相应的渠道购买。微信公众号:保瓶儿,多保鱼服务号知乎:《保乎笔记》PS:本文的内容也整理自这三处7. 总结本文涉及了保险的各个方面,方便大家在对保险一头雾水的时候快速入门,了解保险的内幕,选择自己合适的保险。希望能对大家有所帮助。

February 18, 2019 · 1 min · jiezi

微信扫二维码下载apk跳转浏览器打开的方式(及微信屏蔽下载解决方案)

需求:想让用户在微信扫描二维码或者点击就能下载APP,并统计被扫描次数。两种实现方法:1.一般我们用草料生成二维码,如果没有注册的话只能生成一个包含下载网址的静态码,没有统计功能,而且除了自己截图保存外,草料是不会保存你的二维码的。如果注册草料后,可以选择生成活码。所谓活码,就是一个指向页面,然后通过这个指向页面,再到你的下载链接。这个指向页面内嵌了统计代码。你可以通过草料的统计功能,看你的二维码相关的扫描数据。2.你的App下载地址,自己内嵌一个统计代码,这样来统计扫描数据,这样你只要一个静态码就够了。不需要在草料注册,用户扫描二维码后,直接进入下载界面,没有中间的指向页面。由于不希望自己的app投放到应用市场,因此微下载行不通。比如,把你的APK文件上传到腾讯的开放平台,申请通过后,会拿到一个移动推广链接,然后替换原来的“android下载”的链接(直接此文件生成一个二维码也行),这样用户就可以在微信中扫一扫直接下载了。但以上两种方法实施起来都比较繁琐,而且容易出错,更达不到理想的推广效果。下面为大家轻松实现,在微信中扫描二维码或点击链接直接下载APP的方法。操作方式:旋风微跳是一款基于微信后端开发了一款微信营销下载推广助手,使用了本插件生成的链接,用户在微信任意环境下点击链接或者扫描二维码,可以实现直接跳转手机默认浏览器并打开指定网页。1、打开 旋风微跳 网址:http://www.zjychina.cn 2、准备好我们的推广链接:实例如:www.baidu.com 在输入框填写你的下载链接,填写完毕后。点击生成按钮3、点击生成之后,就会看到底部生成了自己的推广二维码以及短网址链接地址。 4、至此,我们已经生成了APP推广链接的宣传二维码和链接。 我们就可以直接用微信扫描二维码在微信中分享和宣传引流了。这样我们能够极大的提高自己的APP在微信中的推广转化率。解决掉了微信中下载链接被屏蔽等问题。充分利用微信的用户群体来宣传引流。

February 17, 2019 · 1 min · jiezi

微信如何实现自动跳转到用其他浏览器打开指定页面下载APP

目前的APP基本都支持二维码扫描下载,二维码下载也成为了大家用起来很顺手的一种方式。由于微信的用户基本占据了国内市场的90%,说到扫一扫用户第一个想到的就是打开微信扫一下,通过微信分享APP,再从分享的链接下载apk/ios包。故用户通常都是使用微信打开链接或扫描二维码前往下载页,这是刚需。在我们做营销活动或推广宣传的时候,容易遇到域名被封,无法跳转app下载等情况。这时需要微信跳转外部浏览器打开页面的功能,对于ios用户默认可以通过微信内置浏览器点击右上角的更多按钮从而选择“在浏览器中打开”,对于安卓用户则可以实现微信内直接跳出到手机默认浏览器。但是很多用户其实并不知道该任何实现,其实只要在代码中进行相关的处理即可。下面为大家介绍这两种方式的实现方式,不仅可以防封,还可以达到跳转手机浏览器的效果新版本微信浏览器中,已禁用下载APP应用,只支持打开微信合作商APP下载,所以无法通过微信浏览器直接下载APP应用。列举微信浏览器下载APP的种解决方案:方案:通过Url 跳转到手机默认浏览器,或者是苹果应用商店/APP Store,在应用商店/APP Store下载或打开APP。如果手机上没有安装APP,可点击下载,如果已经安装APP,可直接打开。操作方式:旋风微跳是一款基于微信后端开发了一款微信营销下载推广助手,使用了本插件生成的链接,用户在微信任意环境下点击链接或者扫描二维码,可以实现直接跳转手机默认浏览器并打开指定网页。1、打开 旋风微跳 网址:http://www.zjychina.cn 2、准备好我们的推广链接:实例如:www.baidu.com 在输入框填写你的下载链接,填写完毕后。点击生成按钮3、点击生成之后,就会看到底部生成了自己的推广二维码以及短网址链接地址。 至此,我们已经生成了APP推广链接的宣传二维码和链接。 我们就可以直接用微信扫描二维码在微信中分享和宣传引流了。这样我们能够极大的提高自己的APP在微信中的推广转化率。解决掉了微信中下载链接被屏蔽等问题。充分利用微信的用户群体来宣传引流。

February 17, 2019 · 1 min · jiezi

峰采 #2

PS峰采第二期姗姗来迟。原来我们的世界一直在进步?人类有了希望?刘作虎的英文名是什么?硅谷的尤达大师是哪位?Read on…The Listhttps://standardnotes.org/跨平台的笔记软件。比EverNote轻量。支持Android, iOS, MacOS, Windows, Linux。无需科学上网。全程加密。不限量,不限设备。非常适合像我这样用Android手机,但是其它设备都是iOS的同学。https://www.vox.com/2014/11/2…世界一直在进步的数据证明。从历史上看,哪怕是近二十年的历史,贫穷,饥饿的数据一直都在改善。西欧国家人民的休闲时间一直在增加。https://howmuch.net/articles/…world economy in one charthttps://github.com/warmchang/…KubeCon 2018 北美PPT。翻翻可以接受些新概念https://github.com/rhysd/vim….Vim被移植到Webassembly了。难道Write Once, Run Everywhere终于要实现?https://www.nytimes.com/2018/…大神膜拜时间。英文title叫"The Yoda of Silicon Valley"。硅谷的尤达大师。看照片像不像?https://github.com/Netflix-Sk…用命令行操作jirahttps://www.theverge.com/2018…你知道刘作虎的英文名叫什么?Pete Lau。。。。一个浙江人挺起来象香港人。。我也是醉了。https://www.eater.com/2016/6/…Maybe Just Don’t Drink Coffee关于咖啡鄙视链的好玩的英文文章,没啥技术东西https://mp.weixin.qq.com/s/aB…王兴:20年to C,20年to B第一篇中文文章。理论很高。服后可能有醍醐灌顶的感觉。但最后结论还是“学欧美”难免让人丧气。https://github.com/jaywcjlove…又一篇Awesome XXXX。这篇是给Mac的。https://ideas.ted.com/how-wor…TED talk。少干活可是解决你所有的问题,真的。。。。真的吗?https://www.wired.com/2017/02…标题就够炫:程序猿是新蓝领。原来美国只有8%的程序人在硅谷工作。IT的平均工资是81000刀 - 重点是其它工种的两倍。我天朝也差不多吧。。。。http://facesofopensource.com又到拜大神时间,“开源的面孔”。 第一位是Linus! 看看你能认出几个?我看了一下,里面美女还不少哦。 Eric Schmit 创造了 Lex。说明牛人早已是牛人。另外还有几个几个亚洲面孔:git 维护者,Henry Zhuhttps://github.com/rgcr/m-cli又一个命令行工具。号称macOS的瑞士军刀!PS图片有点大。下次看看能不能缩一下。

February 17, 2019 · 1 min · jiezi

构建Potatso问题集锦及解决方案

转载请注明文章出处:https://tlanyan.me/build-pota…前言半年前写过一篇构建自用Shadowsocks客户端Potatso的教程“构建自己的iOS网络代理客户端”。当时除libYAML依赖下载不正常外,编译测试使用全过程都很顺利。文章投递到几个平台被数万网友围观,不少网友根据教程在构建时遇到各种问题。最初我以为是网友看教程不仔细或构建环境差异造成,没多注意。后来陆续有网友加我QQ,让我怀疑写完文章后代码有了重大更新。终于在昨天(除夕)抽出时间,用最新版的代码构建Potatso并安装到我最新版iOS系统的iPad上。这个过程花费了几个小时,覆盖了许多网友咨询我的问题,本文中将一一给出解决方案。如果你的Xcode版本是9.4.1,使用commitID为318a5e1的代码,根据“构建自己的iOS网络代理客户端”中的教程可以顺利的编译和安装Potatso到iOS12系统以下的设备。如果你的设备升级到了最新版,或者遇到其他问题,请继续阅读本文。为什么执着于构建自用Shadowsocks客户端?由于iOS生态的封闭性,正常情况下只能通过App Store下载应用。应用下架后,会导致手机重置、购买买新设备后无法安装。安卓、Windows、MacOS则不会有这个问题,只要安装文件存在,总是有得用。所以针对iOS设备构建自用的客户端很有必要,尤其是SS这类随时有可能下架的应用。本文构建Potatso客户端最终得工程文件以及生成的ipa包已上传到百度云盘:https://pan.baidu.com/s/1twyM…如果构建过程中遇到本文列出以外的问题,欢迎留言或加Q群688196496。构建步骤这节简要回顾构建Potatso的流程:1. 安装Cocospods如果已安装,请略过此步。更新系统的gem版本:打开终端,输入:sudo gem update –system;设置国内gem源:gem sources –list输出为https://gems.ruby-china.org/请略过此步;否则先删除官方源再添加gems国内源:gem sources –remove https://rubygems.org/; gem sources –add https://gems.ruby-china.org/;安装Cocospods:sudo gem install cocoapods。2. 构建Potatso构建Potatso的步骤如下:克隆代码:git clone https://github.com/haxpor/Potatso.git;更新子模块:cd Potatso; git submodule update –init;安装依赖:打开Podfile,将第一行改成:source ‘https://mirrors.tuna.tsinghua.edu.cn/git/CocoaPods/Specs.git'(使用清华的CocoaPods源),然后运行pod install –verbose;使用XCode打开Potatso.xcworkspace;更改Potatso及PacketTunnel、TodayWidget两个子项目的Bundle ID,例如本人分别改成:potatso.tlanyan.me、potatso.tlanyan.me.PacketTunnel和potatso.tlanyan.me.TodayWidget;更改Potatso及PacketTunnel、TodayWidget两个子项目Capabilities中的App Group和Keychain Sharing的Group:在"App Groups"中删除原有的group.io.wasin.potatso,新增自己的group,例如:“group.potatso.tlanyan.me”;在"Keychain Sharing"中输入自己的group ID;打开"PotatsoBase/Potatso.m"文件,将shareGroupIdentifier函数的返回值改成自己的group id;将iPhone等iOS设备连接到电脑,目标选择新接入的设备,点击左上角的“build and run”按钮,Xcode会编译并安装App到设备上,然后启动。可能遇到的问题昨天几个小时的折腾,遇到的十来个问题。下文将一一列出,并给出解决方案。构建过程中你可能会遇到不止一个错误,请根据错误信息按Ctrl + F在本文查找。如果遇到其他问题,欢迎留言或加Q群688196496。1. the sandbox is not in sync with the Podfile.lock. Run ‘pod install’ or update your CocosPods installation.问题截图:<img src=“https://tlanyan.me/wp-content...; alt=”" width=“842” height=“206” class=“aligncenter size-full wp-image-3193” />原因: pod依赖未安装解决办法: 安装依赖,执行命令:pod install –verbose2. url: (7) Failed to connect to pyyaml.org port 80: Connection refused错误描述: 执行pod install,前面一切顺利,到libYAML会出现问题:Installing LibYAML (0.1.4)[!] Error installing LibYAML[!] /usr/bin/curl -f -L -o /var/folders/dj/ljst94xx47l7fn3wz4q9bwsw0000gn/T/d20180822-4467-1cotycr/file.tgz http://pyyaml.org/download/libyaml/yaml-0.1.4.tar.gz –create-dirs –netrc-optional –retry 2 % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 0 0 0 0 0 0 0 0 –:–:– –:–:– –:–:– 0 curl: (7) Failed to connect to pyyaml.org port 80: Connection refused原因: libYAML的官网关闭了80端口访问解决办法: 编辑" /Users/你的用户名/.cocoapods/repos/master/Specs/5/b/9/LibYAML/0.1.4/LibYAML.podspec.json"文件,将"http://pyyaml.org/download/libyaml/yaml-0.1.4.tar.gz"改成“https://pyyaml.org/download/l…”备注: 此解决方案来自貌似LibYAML官方人员的回复,亲测可以。当然可以使用前文“构建自己的iOS网络代理客户端”中所说的网络劫持方法。3. Diff:/Podfile.lock: No such file or directory使用新版代码并安装好依赖后,这应该是构建过程中最先出现的问题。问题截图:<img src=“https://tlanyan.me/wp-content...; alt=”" width=“854” height=“246” class=“aligncenter size-full wp-image-3192” />原因:根据错误描述跟踪脚本执行流程,发现是执行预构建脚本时SRCROOT环境变量的值无法获取(或被错误置为空)导致。解决方案: 尝试过更改构建时生成的临时脚本文件、注入全局环境变量等,这些方法均不凑效。后来通过diff发现脚本由文件Potatso.xcodeproj/project.pbxproj文件中的配置生成,该文件在pod install后被修改。解决办法很简单:还原更改。执行完pod install命令后,执行git checkout Potatso.xcodeproj/project.pbxproj,问题解决。4. No podspec found for CallbackURLKit in ./Library/CallbackURLKit问题截图:<img src=“https://tlanyan.me/wp-content...; alt=”" width=“665” height=“93” class=“aligncenter size-large wp-image-3194” />原因: 子模块的代码未下载解决方案: 初始化子模块代码,执行命令:git submodule update –init5. The operation couldn’t be completed. Unable to log in with account ‘xxxx’. The login details for account ‘xxxx’ / No profiles for ‘xxxx’ were found: Xcode couldn’t find any iOS App Development provisioning profiles matching ‘xxx’ / Code signing is required for product type…问题截图:<img src=“https://tlanyan.me/wp-content...; alt=”" width=“855” height=“221” class=“aligncenter size-full wp-image-3195” />原因: Apple ID过期未续费解决方案: Apple ID续费或换其他可用的ID6. No account for team ‘xxx’. Add a new account in the Accounts preference pane or verify that your accounts have valid…错误信息基本与上一条相同,只是账号换成了team ID。问题截图:<img src=“https://tlanyan.me/wp-content...; alt=”" width=“878” height=“214” class=“aligncenter size-full wp-image-3198” />原因: team ID不在已添加的账号内解决方案: 在属性页面的Team中选择自己的账号7. Your account does not have sufficient permissions to modify containers. / No profiles for ‘xxxx’ were found问题截图:<img src=“https://tlanyan.me/wp-content...; alt=”" width=“380” height=“149” class=“aligncenter size-full wp-image-3196” />原因: 该Bundle ID已经被其他Apple ID使用解决方案: 换一个新的8. An Application Group with Identifier ‘xxxx’ is not available. Please enter a different string.问题截图:<img src=“https://tlanyan.me/wp-content...; alt=”" width=“493” height=“197” class=“aligncenter size-full wp-image-3197” />原因: Group ID已经被其他Apple ID使用解决方案: 用一个新的9. Module ‘Crashlytics’ not found这个错误未截图。原因: Podfile文件里没有加这个库解决方案: 打开Podfile,在def library中添加一行:pod ‘Crashlytics’, ‘> 3.10.7’,然后执行pod install –verbose。备注: 该解决方案参考Github的issue: https://github.com/haxpor/Pot…。注意pod安装依赖后,会更改Potatso.xcodeproj/project.pbxproj文件,直接编译会出现第二个问题。正确操作应当如下:先备份Potatso.xcodeproj/project.pbxproj文件,然后执行pod install –verbose,成功后将文件覆盖。后续出现pod依赖更新的情况也应该按此步骤操作。10. Could not locate device support files问题截图:<img src=“https://tlanyan.me/wp-content...; alt=”" width=“412” height=“148” class=“aligncenter size-full wp-image-3199” />原因: Xcode版本过低,不支持iOS 12.1系统。根据官方页面,需要Xcode 10<img src=“https://tlanyan.me/wp-content...; alt=”" width=“888” height=“493” class=“aligncenter size-full wp-image-3200” />解决方案: 安装Xcode 10,文件较大,根据网速需要一定时间,请耐心等待11. Invalid redeclaration of ‘<-’ EnumOprators.swift问题截图:<img src=“https://tlanyan.me/wp-content...; alt=”" width=“294” height=“619” class=“aligncenter size-full wp-image-3201” />原因: ObjectMapper的版本过低解决办法: 使用新版的ObjectMapper:打开Podfile,将ObjectMapper那一行改成pod ‘AlamofireObjectMapper’, ‘> 5.0’备注: 解决方案参考https://stackoverflow.com/que…12. Type ‘RLMIterator<proxy>’ does not conform to protocol ‘Sequence’</proxy>问题截图:<img src=“https://tlanyan.me/wp-content...; alt=”" width=“283” height=“379” class=“aligncenter size-full wp-image-3203” /><img src=“https://tlanyan.me/wp-content...; alt=”" width=“665” height=“264” class=“aligncenter size-large wp-image-3204” />原因: 这个问题不清楚具体原因。怀疑是Realm这个库的问题,没有实现Sequence接口。我将RealmSwift改到最新的3.7.6问题亦没有解决。不懂Swift,不过感觉是RMLIterator或者Proxy/RuleSet等存在问题。解决办法: 注销PotatsoMode/DBUtils.swift中的相关代码,具体是174-190和202-218行之间的代码。备注: 解决方案来自:https://github.com/haxpor/Pot…。所有错误中,只有这个错误不是完美解决。13. Initializer for conditional binding must have Optional type, not ‘[Rule]‘问题截图:<img src=“https://tlanyan.me/wp-content...; alt=”" width=“379” height=“151” class=“aligncenter size-full wp-image-3205” />原因: 非nil值不应该使用if let(我自己的理解,毕竟不懂Swift)解决办法: 将Potatso/Core/API.swift第65和256行的if和大括号去掉,65行修改示意:<pre>// 修改前// if let parsedObject = Mapper<Rule>().mapArray(JSONArray: rulesJSON as! [[String : Any]]){// let parsedObject = Mapper<Rule>().mapArray(JSONArray: rulesJSON as! [[String : Any]])// rules.append(contentsOf: parsedObject)//}// 修改后let parsedObject = Mapper<Rule>().mapArray(JSONArray: rulesJSON as! [[String : Any]])rules.append(contentsOf: parsedObject)</pre>备注: 解决办法的灵感来自:https://stackoverflow.com/que…。当然这个问题和Potatso无关。还有一个错误截图:<img src=“https://tlanyan.me/wp-content...; alt=”" width=“766” height=“661” class=“aligncenter size-full wp-image-3202” />具体什么忘了。如果你遇到了或者有解决方案,可留言告诉我。参考构建自己的iOS网络代理客户端 ...

February 11, 2019 · 3 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

生成带二维码图片并通过微信分享

【本文出自APICloud官方论坛,感谢论坛版主初级码农的分享。】需要用到的模块 clipBoard 复制文本信息 trans 转换data:base64格式的图片为普通图片 FNScanner 二维码生成 photoBrowser 预览待分享图片 wx 微信分享需要用到的api对象 imageCache 缓存网络图片 saveMediaToAlbum 保存图片到本机相册 download 下载 (安卓平台才会用到ios可以不用)需要引入的一个JS为 html2canvas.min.js 用此JS进行canvas 图片的绘制;整个demo 的思路 在一个容器里面把需要分享的内容布置好再用html2canvas.min.js进行canvas绘制生成dataBase64位的图片,转换base64的图片为普通图片 保存在fs路径下 然后进行分享!样式可以自己控制,不要吐槽我的页面写的丑! 需要的伙伴自己下载源码研究。下载网址:https://community.apicloud.com/b … 8&extra=&highlight=分享&page=1

January 31, 2019 · 1 min · jiezi

百度人脸识别模块使用分享

【本文出自APICloud官方论坛,感谢鲍永道的分享。】首先介绍下百度人脸识别模块(baiduFaceRec):baiduFaceRec模块封装了百度AI人脸识别功能,使用此模块可实现百度人脸检测(包括age,beauty,expression,faceshape,gender,glasses,landmark,race,quality,facetype信息)、人脸对比功能(比对两张图片中人脸的相似度,并返回相似度分值)。暂仅支持 android 平台。不啰嗦,直接上代码:<!DOCTYPE html><html xmlns=“http://www.w3.org/1999/html&quot;><head><meta http-equiv=“Content-Type” content=“text/html; charset=utf-8”/><meta name=“viewport” content=“maximum-scale=1.0,minimum-scale=1.0,user-scalable=0,width=device-width,initial-scale=1.0”/><title>frame2</title><link rel=“stylesheet” href=”../css/api.css"><link rel=“stylesheet” href="../css/aui.css"><style> html, body { background: #ffffff; } .my-card { border: solid 1px #dddddd; margin: 10px; } .aui-btn-block { margin-bottom: 10px; }</style></head><body><section class=“aui-content-padded my-card”><div class=“aui-card-list”> <div class=“aui-card-list-header”> 百度人脸识别(V3版本)自定义模块 </div> <div class=“aui-card-list-content-padded”> 人脸识别 </div> <div class=“aui-card-list-footer”> 2018-06-03 </div></div></section><div class=“aui-content-padded”><p><div class=“aui-btn aui-btn-info aui-btn-block”>获取access_token</div></p><p><div class=“aui-btn aui-btn-info aui-btn-block”>人脸检测</div></p><p><div class=“aui-btn aui-btn-info aui-btn-block”>人脸对比</div></p></div></body></html><script src="../script/api.js"></script><script>var baiduFaceRec = null;var UIAlbumBrowser = null;apiready = function () { baiduFaceRec = api.require(‘baiduFaceRec’); UIAlbumBrowser = api.require(‘UIAlbumBrowser’);};//获取access_tokenfunction getAuth() { var params = { ak: ‘your ak’, sk: ‘your sk’ };baiduFaceRec.getAuth(params, function (ret, err) {if (ret) { console.log(JSON.stringify(ret)); alert(‘access_token=’ + ret.access_token);} else { console.log(err.msg); alert(‘错误信息:’ + err.msg);}})}//人脸检测function detect() { //先获取access_token var params = { ak: ‘your ak’, sk: ‘your sk’ }; baiduFaceRec.getAuth(params, function (ret, err) { if (ret) { console.log(JSON.stringify(ret)); var access_token = ret.access_token; //选择照片或拍照 api.actionSheet({ title: ‘选择照片’, cancelTitle: ‘取消’, buttons: [‘拍照’, ‘手机相册’] }, function (ret, err) { if (ret) { console.log(ret.buttonIndex); if (ret.buttonIndex != 3) { var sourceType = ret.buttonIndex; //获取图片 api.getPicture({ sourceType: (sourceType == 1) ? ‘camera’ : ‘album’, encodingType: ‘jpg’, mediaValue: ‘pic’, destinationType: ‘url’, allowEdit: true, saveToPhotoAlbum: false }, function (ret, err) { if (ret) { console.log(ret.data); var filePath = ret.data; var params = { filePath: filePath, access_token: access_token }; //人脸检测 baiduFaceRec.detect(params, function (ret, err) { if (ret) { console.log(JSON.stringify(ret)); alert(‘人脸检测数据’ + JSON.stringify(ret.result.face_list)); } else { console.log(err.msg); } }) } else { console.log(JSON.stringify(err)); alert(JSON.stringify(err)); } }) } else { return false; } } }); } else { console.log(err.msg); alert(‘错误:’ + ret.msg); } });}//人脸对比function match() { //先获取access_token var params = { ak: ‘your ak’, sk: ‘your sk’ }; baiduFaceRec.getAuth(params, function (ret, err) { if (ret) { console.log(JSON.stringify(ret)); var access_token = ret.access_token; //得到对比图片 UIAlbumBrowser.open({ max: 2, styles: { bg: ‘#fff’, mark: { icon: ‘’, position: ‘bottom_left’, size: 20 }, nav: { bg: ‘rgba(0,0,0,0.6)’, titleColor: ‘#fff’, titleSize: 18, cancelColor: ‘#fff’, cancelSize: 16, finishColor: ‘#fff’, finishSize: 16 } }, rotation: true }, function (ret) { if (ret) { var filePath1 = ret.list[0].path; var filePath2 = ret.list[1].path; var params = { filePath1: filePath1, filePath2: filePath2, access_token: access_token }; //人脸对比 baiduFaceRec.match(params, function (ret, err) { if (ret) { console.log(JSON.stringify(ret)); alert(‘人脸检测数据’ + JSON.stringify(ret)); } else { console.log(err.msg); } }) } }); } else { console.log(err.msg); alert(‘错误:’ + ret.msg); } });}</script>使用模块前需要先到百度AI开发者中心创建应用,获取ak和sk,然后进行身份验证,获取返回的access_token,建议每次进行人脸识别接口时先获取access_token(30期限),然后每次请求识别接口也传入access_token,这样保证每次都请求ok。另外的两个人脸识别接口,一个是人脸识别,一个是人脸对比。人脸识别主要是识别人的脸部相关参数,对应的参数很多,我就不一一说明了,文档有详细说明。另外就是人脸对比,对比两张脸的相似度值,可以根据相似度值来判断两张人脸是否是同一个人,在项目上应用于人脸对比验证,应该会使用的比较多。 ...

January 31, 2019 · 2 min · jiezi

Flutter尝鲜3——动画处理<并行和串行>

本例的代码参考这里。并行动画当多个动画定义同时指向某个组件,并使用动画控制器启动时,就产生了并行动画(Parallel Animation)。例如我们可以让一个组件:移动的同时改变大小旋转的同时边界颜色闪烁圆形图片模糊的同时形状越来越方总之,掌握了动画原理以后我们知道,只要能将一个动画抽象值与一个组件的某个外观属性值联系起来,那么就能在动画中展现出连续平滑的外观变化。这一点,任何平台(Web、Android)的原理都是一致的。例子接前一篇的例子,我们让一个移动的正方形在位移过程中逐渐变为圆形。在已有的animation基础上,再添加一个新的animation用以控制动画组件的边角半径。class ParallelDemoState extends State<ParallelDemo> with SingleTickerProviderStateMixin { … Tween<double> slideTween = Tween(begin: 0.0, end: 200.0); Tween<double> borderTween = Tween(begin: 0.0, end: 40.0); // 添加边角半径变动范围 Animation<double> slideAnimation; Animation<double> borderAnimation; // 添加边角半径动画定义 @override void initState() { … controller = AnimationController(duration: Duration(milliseconds: 2000), vsync: this); slideAnimation = slideTween.animate(CurvedAnimation(parent: controller, curve: Curves.linear)); borderAnimation = borderTween.animate(CurvedAnimation(parent: controller, curve: Curves.linear)); // 定义边角半径动画 } … @override Widget build(BuildContext context) { return Container( width: 200, alignment: Alignment.centerLeft, child: Container( margin: EdgeInsets.only(left: slideAnimation.value), decoration: BoxDecoration( borderRadius: BorderRadius.circular(borderAnimation.value), // 边角半径的属性上添加动画 color: Colors.blue, ), width: 80, height: 80, ), ); }}串行动画串行动画(Sequential Animation)顾名思义,多个动画像肉串一样一个接一个的发生。但这只是从现象上观察出的结果,实际的运行方式和并行动画差别不大。串行动画的关键之处在于,它为每个动画的发生设定了一个计时器,只有到特定时间点时,特定的动画效果才会发生。例如设计一个3秒钟的动画:移动动画从0秒开始,持续1秒旋转动画从1秒开始,持续1.5秒缩放动画从2秒开始,持续0.7秒那么,最后的动画效果便是:01秒,动画元素在移动12秒,动画元素在旋转22.5秒,动画既在旋转又在缩放2.52.7秒,动画在缩放2.7~3秒,动画静止不动例子在串行动画例子的基础上,我们加上计时器Interval的处理。Interval有三个参数,前两个参数指示了动画的开始和结束时间。这两个参数都是以动画控制器的Duration时长的比例来计算的。例如:Slide动画分别为0.0和0.5,表示动画从0秒(2000ms 0.0)这个时间点开始,至1秒(2000ms 0.5)这个时间点结束Border动画分别为0.5和1.0,表示动画从1秒(2000ms 0.5)这个时间点开始,至2秒(2000ms 1.0)这个时间点结束class SequentialDemoState extends State<ParallelDemo> with SingleTickerProviderStateMixin { … @override void initState() { … controller = AnimationController(duration: Duration(milliseconds: 2000), vsync: this); // slideAnimation = slideTween.animate(CurvedAnimation(parent: controller, curve: Curves.linear)); // borderAnimation = borderTween.animate(CurvedAnimation(parent: controller, curve: Curves.linear)); // 定义边角半径动画 // 换一种写法,加入Interval slideAnimation = slideTween.animate(CurveTween(curve: Interval(0.0, 0.5, curve: Curves.linear)).animate(controller)); borderAnimation = borderTween.animate(CurveTween(curve: Interval(0.5, 1.0, curve: Curves.linear)).animate(controller)); } … @override Widget build(BuildContext context) { return Container( width: 200, alignment: Alignment.centerLeft, child: Container( margin: EdgeInsets.only(left: slideAnimation.value), decoration: BoxDecoration( borderRadius: BorderRadius.circular(borderAnimation.value), // 边角半径的属性上添加动画 color: Colors.blue, ), width: 80, height: 80, ), ); }} ...

January 31, 2019 · 1 min · jiezi

移动端 H5 中百度地图的点击事件

根据百度地图官方解释,在移动端 H5 页面中可监听下面这 4 个事件:touchstart, touchmove, touchend, longpress而如果地图上监听了 click 事件,在移动端是不会执行这个事件里面的代码的。我之前做一个需求时,给地图监听了 touchend 事件,不曾想当我拖动地图时,也执行了 touchend 里的代码。所以需要模拟一个像 zepto 中的 tap 事件,就能解决这个问题了。我的代码是:function initMap(baseData) { var mp = new BMap.Map(‘map’); var point = new BMap.Point( baseData.data.gardenLongitude, baseData.data.gardenLatitude ); mp.centerAndZoom(point, 15); // 保存 touch 对象信息 var obj = {}; mp.addEventListener(’touchstart’, function (e) { obj.e = e.changedTouches ? e.changedTouches[0] : e; obj.target = e.target; obj.time = Date.now(); obj.X = obj.e.pageX; obj.Y = obj.e.pageY; }); mp.addEventListener(’touchend’, function (e) { obj.e = e.changedTouches ? e.changedTouches[0] : e; if ( obj.target === e.target && // 大于 750 可看成长按了 ((Date.now() - obj.time) < 750) && // 应用勾股定理判断,如果 touchstart 的点到 touchend 的点小于 15,就可当成地图被点击了 (Math.sqrt(Math.pow(obj.X - obj.e.pageX, 2) + Math.pow(obj.Y - obj.e.pageY, 2)) < 15) ) { // 地图被点击了,执行一些操作 doSomething(); } }); } ...

January 29, 2019 · 1 min · jiezi

个推用户画像的实践与应用

“以用户为核心”的概念在互联网时代深入人心,然而要真正了解用户懂得用户,就不得不提到“用户画像”。 随着大数据技术的深入研究与应用,借助用户画像,企业或APP可以深入挖掘用户需求,从而实现精细化运营以及为精准营销打下坚实基础。本文将重点介绍何为用户画像,用户画像的构建流程以及应用场景。用户画像,本质是数据能力的体现用户画像,即用户信息的标签化,而从本质上来说,用户画像是数据的标签化。常见的用户画像体系有三种:结构化体系、非结构化体系和半结构化体系。非结构化体系没有明显的层级,较为独立。半结构化层次有一定的层级概念,但是没有过于严格的依赖关系。结构化体系有较强的层级结构。以一个简单的三级结构化标签为例,一级标签有基本属性和兴趣偏好,并且由此可以延伸至二级标签和三级标签,具体到不同的属性和兴趣爱好。在互联网、电商领域,用户画像常用来作为精准营销、推荐系统的基础性工作,其作用总体包括:(1)精准营销:根据历史用户特征,运营人员可以分析产品的潜在用户和用户的潜在需求,继而通过相应的手段,针对特定群体进行营销。(2)用户分析:根据用户的属性、行为特征对用户进行分类后,可以统计不同特征下的用户数量、分布,分析不同用户画像群体的分布特征。(3)数据挖掘:以用户画像为基础,开发人员可以构建推荐系统、搜索引擎、广告投放系统,提升服务精准度。(4)服务产品:描绘产品的用户画像,对产品进行受众分析,更透彻地理解用户使用产品的心理动机和行为习惯,完善产品运营,提升服务质量。(5)行业报告&用户研究:通过用户画像分析可以使运营人员更加了解行业动态,比如人群消费习惯、消费偏好分析、不同地域品类消费差异分析等。个推用户画像的实践个推依托多年推送服务的积累和强大的大数据分析能力,推出了个推画像SDK(个像),为APP开发者提供丰富的用户画像数据以及实时的场景识别能力。个推独有的冷、热、温数据标签,可以有效分析用户的线上线下行为,深入挖掘用户特征,助力APP运营者全面了解用户属性。其中,“冷数据”是指用户的基础属性,改变的概率较小,如性别、年龄层次等;“温数据”则可以回溯用户近期活跃的应用和场景,具有一定的时效性;“热数据”是指用户当下的场景及实时的用户行为,帮助APP运营者抓住稍纵即逝的营销机会。个推不仅拥有丰富的通用标签体系,还可以根据客户特定的需求联合建模,输出定制化的标签,以满足APP在不同场景需求下的运营。规范画像构建流程用户画像的构建需要技术和业务人员的共同参与,以避免形式化的用户画像。个推也有一些做法可供开发者们进行参考。(1)标签体系设计。开发者需要先了解自身的数据,确定需要设计的标签形式。(2)基础数据收集、多数据源数据融合。个推在构建用户画像时,会整合个推以及该APP自身的数据。(3)实现用户统一标识。多数情况下,APP的众多用户分布于不同的账号体系中,个推会将其统一标识。(4)用户画像特征层构建。即将每一个数据进行特征化。(5)画像标签规则+算法建模。两者缺一不可,在实际的应用中,算法难以解决的问题,利用简单的规则也可以达到很好的效果。(6)利用算法对所有用户打标签。(7)画像质量监控。在实际的应用中,用户画像会产生一定的波动,为了解决这个问题,个推搭建了相应的监控系统,对画像的质量进行监控。个推用户画像构建的整体流程,可以分为三个部分,第一,基础数据处理。基础数据包括用户设备信息、用户的线上APP偏好以及线下场景数据等。第二,画像中间数据处理。处理结果包括线上APP偏好特征和线下场景特征等。第三,画像信息表。表中应有四种信息:设备基础属性;用户基础画像,包括用户的性别、年龄层次、相关消费水平等;用户兴趣画像,即用户更有兴趣的方向,如用户更偏好比价类APP还是海淘类APP;用户其它画像等。在个推用户画像构建的过程中,机器学习占据了较为重要的位置。机器学习主要是海量数据持续更新、数据清洗、数据存储的过程。个推更多地利用机器学习平台进行相应的预测分析、模型输出等。画像质量的关注有两个重点,第一,如何优化质量。个推会对用户画像的模型定期地进行修改和优化。第二,关注画像质量波动情况,对异常变化及时预警。个推用户画像应用个推画像SDK的集成,可以丰富APP的用户分析维度,其主要应用体现在两方面:第一,精准推荐,APP的运营者可以通过个像提供的性别、年龄层次、兴趣爱好、场景等丰富标签,为不同的用户推荐不同的内容,以达到更加精细化的运营,并提升用户活跃度和留存率。第二,用户聚类,个推可以帮助APP处理用户数据,补全用户画像,建立用户的聚类模型。同时,通过用户特征分析,个推还能够将APP的老用户映射到某一聚类,以此产出APP的目标聚类,最终助力APP运营者针对不同用户群体制定更加精准的运营策略。“千万人撩你,不如一人懂你”,当互联网逐渐步入大数据时代,APP只有真正地了解用户,才能得到用户并留住用户。基于个推完备的大数据计算架构,个推画像SDK的接入,不仅可以帮助开发人员提高开发决策的效率,也可以帮助APP运营人员开展精细化运营,从而提升企业的营销效率和市场竞争力。

January 29, 2019 · 1 min · jiezi

H5 如何唤起百度地图 App

最近接手了一个需求,要求混合式开发,前端做好 h5 后将页面嵌入到 ios 和 android 中,需要用到百度地图的地图导航。具体功能点如下:如果手机端(ios, android)安装了百度地图,点击导航按钮,唤起百度地图 app否则,打开 web 端百度地图导航需要用到的百度地图的 api 文档链接如下:http://lbsyun.baidu.com/index…最开始的代码: // 尝试唤起百度地图 app window.location.href = scheme; var timeout = 600; var startTime = Date.now(); var t = setTimeout(function () { var endTime = Date.now(); // 当成功唤起百度地图 app 后,再返回到 h5 页面,这时 endTime - startTime 一定大于 timeout + 200; 如果唤起失败, 打开 web 端百度地图导航 if (!startTime || (endTime - startTime) < (timeout + 200)) { window.location.href = ‘http://api.map.baidu.com/direction' + queryStr + ‘&output=html’; } }, timeout);问题:上面这段代码在 android 机器上运行是没有问题的,可是在 ios 上却始终执行了 setTimeout 这个计时器,所以如果在 ios 端,即使 app 处于后台,它的 h5 代码还是会执行。所以需要换一种方式,总的思路是:采用轮询的方式在 600 ms 内尝试唤起百度地图 app, 唤起失败后, 判断 h5 是处于前台还是后台,如果是前台,则打开 web 端百度地图 app。不管唤起成功还是失败,过 200 ms 后都清除定时器。修改后的代码: var startTime = Date.now(); var count = 0; var endTime = 0; var t = setInterval(function () { count += 1; endTime = Date.now() - startTime; if (endTime > 800) { clearInterval(t); } if (count < 30) return; if (!(document.hidden || document.webkitHidden)) { window.location.href = ‘http://api.map.baidu.com/direction' + queryStr + ‘&output=html’; } }, 20);完整的代码: function wakeBaidu() { var geolocation = new BMap.Geolocation(); geolocation.getCurrentPosition(function (result) { if (this.getStatus() == BMAP_STATUS_SUCCESS) { var latCurrent = result.point.lat; //获取到的纬度 var lngCurrent = result.point.lng; //获取到的经度 if (latCurrent && lngCurrent) { var scheme = ‘’; // urlObject 是我这边地址栏查询参数对象 var queryStr = ‘?origin=name:我的位置|latlng:’ + latCurrent + ‘,’ + lngCurrent + ‘&destination=’ + urlObject.lat + ‘,’ + urlObject.lng + ‘&region=’ + urlObject.city + ‘&coord_type=bd09ll&mode=driving’; if (isIOS()) { // ios 端 scheme = ‘baidumap://map/direction’ + queryStr; } else { // android 端 scheme = ‘bdapp://map/direction’ + queryStr; } // 主要实现代码 window.location.href = scheme; var startTime = Date.now(); var count = 0; var endTime = 0; var t = setInterval(function () { count += 1; endTime = Date.now() - startTime; if (endTime > 800) { clearInterval(t); } if (count < 30) return; if (!(document.hidden || document.webkitHidden)) { window.location.href = ‘http://api.map.baidu.com/direction' + queryStr + ‘&output=html’; } }, 20); window.onblur = function () { clearInterval(t); }; } else { alert(‘获取不到定位,请检查手机定位设置’); } } }); } ...

January 25, 2019 · 2 min · jiezi

知名协作工具 Slack 换新 logo 啦!

简评:本文系译文。Slack 在自家 Blog 上解释了新的设计 —— 旧版的 logo 有 11 种颜色,在非白色背景的情况下,logo 不仅难看,而且很容易在多个地方上表现不一致。新年换新装,Slack 已经用新的 logo 来迎接 2019 年了。目前在平台上已经更换完毕,所以现在已经是新的 logo 囖 ~对于新旧 logo 变更可能引发的争议,Slack 显然已经有所准备。首先,它并不是为了改变而改变,这次改变后的 logo 会比原来的更具有现代感,样式也会更简单。Slack 的第一个 logo 是在公司成立时创建的,我们把它设计成一个类似「#」的样子,它由 11 种不同的颜色组成,因此很容易出错。如果 logo 放在不是白色的其他颜色上,或者角度没控制好的话(精确的 18°),看起来都会很糟糕,看看下面的例子就知道了????对于这个问题,我们设计了不同版本的 logo 来降低影响。但这也意味着每个平台或者 App 上的按钮不尽相同,而每个按钮又与 logo 不同。尽管这几个不同版本都还不错,但是这也造成了品牌辨识度下降的问题。品牌的第一要义就是 —— 无论何时何地,人们一看到你,就能认识你。而下面这些简直就是教科书式的反例????因此,我们内部的设计团队、品牌团队协同 Michael Bierut 及其 Pentagram 团队,合力创造了一个更具品牌一体性的全新视觉形象,也就是我们今天开始使用的新图标。新的 logo 使用更简洁的色彩搭配,直接回归到最原本的四种颜色:浅蓝、红色、黄色和绿色。我们认为它比旧版 logo 要更加精致,但依然包含了原始精神 (the spirit of the original)。它也能帮助我们的产品在更多地方扩展和更好地工作。对于其中的设计理念、以及新 logo 中每个角度每条曲线的意义,本文都不会过多阐述。本文的目的也仅仅是让用户知道这个变化 —— 当你在不同设备上发现图标似乎有些不同时,惊讶之余又会觉得它们看起来有种让人心安的相似。原文链接:Say hello, new logo.

January 24, 2019 · 1 min · jiezi

AlphaWallet 1.40 版本正式发布

AlphaWallet 团队在今天正式发布了 1.40 版本,这个版本的主要变化包括:在新的 Dapp 首页设计里,可以放置你平常爱用和常用的 Dapp 了,轻轻一点,快速直达。通过 AlphaWallet 支持的链上发送锚定美元的 xDAI,享受更便宜的花费和更快捷的速度。一个更加清晰和明了的界面,让你了解被 AlphaWallet 团队审核过的资产,同时方便接受和发送你喜欢的数字货币。还有更酷的!这个功能很快也要推出了,通过 AlphaWallet 提供的 iFRAME 可以方便地查看你喜欢的 DApps 并与之互动,任何使用自定义标记语言(TBML)创建标记文件的 DApp 都将获得丰富而又美妙的超常体验,这在过去的 DApp 体验里是无法想象的。iFRAME 允许 DApp 包含它的使用说明,一些关键信息和一个直观明了的界面,并且直接在 DApp 的 Token 卡片界面内显示出来。(让我们名正言顺地干掉 DApp 浏览器吧!)AlphaWallet 是一个开源项目,这里是一些相关信息:GitHub 地址:https://github.com/alphawallet/Telegtram:https://t.me/AlphaWalletGroupiOS 下载地址:https://apple.co/2OQRS7cAndroid 下载地址:https://bit.do/alphawallet

January 23, 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

React Native工程中TSLint静态检查工具的探索之路

背景建立的代码规范没人遵守,项目中遍地风格迥异的代码,你会不会抓狂?通过测试用例的程序还会出现Bug,而原因仅仅是自己犯下的低级错误,你会不会抓狂?某种代码写法存在问题导致崩溃时,只能全工程检查代码,这需要人工花费大量时间Review代码,你会不会抓狂?以上这些问题,可以通过静态检查有效地缓解!静态检查(Static Program Analysis)主要是以不运行程序的方式对于程序源代码进行检查分析的技术,而与之相反的就是动态检查(Dynamic Program Analysis),通过实际运行程序输入测试数据产生预期结果的技术。通过代码静态检查,我们可以快速定位代码的错误与缺陷,可以减少逐行阅读代码浪费的时间,可以(根据需要)快速扫描代码中可能存在的漏洞等。代码静态检查可以在代码的规范性、安全性、可靠性、可维护性等方面起到重要作用。在客户端中,Android可以使用CheckStyle、Lint、Findbugs、PMD等工具,iOS可以使用Clang Static Analyzer、OCLint等工具。而在React Native的开发过程中,针对于JavaScript的ESLint,与TypeScript的TSLint,则成为了主要代码静态检查的工具。本文将按照使用TSLint的原因、使用TSLint的方法、自定义TSLint的步骤进行探究分析。一、使用TSLint的原因在客户端团队进入React Native项目的开发过程中,面临着如下问题:由于大家从客户端转入到React Native开发过程中,容易出现低级语法错误;开发者之前从事Android、iOS、前端等工作,因此代码风格不同,导致项目代码风格不统一;客户端效果不一致,有可能Android端显示正常、iOS端显示异常,或者相反的情况出现。虽然以上问题可以通过多次不断将雷点标记出,并不断地分享经验与强化代码Review过程等方式来进行缓解,但是仍面临着React Native开发者掌握的技术水平千差万别,知识分享传播的速度缓慢等问题,既导致了开发成本的不断增加和开发效率持续低下的问题,还难以避免一个坑被踩了多次的情况出现。这时急需一款可以满足以下目标的工具:可检测代码低级语法错误;规范项目代码风格;根据需要可自定义检查代码的逻辑;工具使用者可以“傻瓜式”的接入部署到开发IDE环境;可以快速高效地将检查工具最新检查逻辑同步到开发IDE环境中;对于检查出的问题可以快速定位。根据上述要求的描述,静态检查工具TSLint可以较为有效地达成目标。二、TSLint介绍TSLint是硅谷企业Palantir的一个项目,它是一款可以检查TypeScript代码可读性、可维护性以及功能性错误的静态检查工具,当前许多编辑器(Editors)和构建系统(Build Systems)支持这一工具,同时支持自定义编写Lint规则、配置、格式化等。当前TSLint已经包含了上百条规则,这些规则构筑了当前TSLint检查的基础。在代码开发阶段中,通过这些配置好的规则可以给工程一个完整的检查,并随时可以提示出可能存在的问题。本文内容参考了TSLint官方文档https://palantir.github.io/tslint/。2.1 TSLint常见规则以下规则主要来源于TSLint规则,是某些规则的简单介绍。2.2 常用TSLint规则包上述2.1所列出的规则来源于Palantir官方TSLint规则。实际还有多种,可能会用到的有以下:我们在项目的规则配置过程中,一般采用上述规则包其中一种或者若干种同时配置,那如何配置呢?请看下文。三、如何进行TSLint规则配置与检查首先,在工程package.json文件中配置TSLint包:在根目录中的tslint.json文件中可以根据需要配置已有规则,例如:其中extends数组内放置继承的TSLint规则包,上图包括了airbnb配置的规则包、tslint-react的规则包,而rules用于配置规则的开关。TSLint规则目前只有true和false的选项,这导致了结果要么正常,要么报错ERROR,而不会出现WARNING等警告。有些时候,虽然配置某些规则开启,但是某个文件内可能会关闭某些甚至全部规则检查,这时候可以通过规则注释来配置,如:/* tslint:disable /上述注释表示本文件自此注释所在行开始,以下的所有区域关闭TSLint规则检查。/ tslint:enable /上述注释表示本文件自此注释所在行开始,以下的所有区域开启TSLint规则检查。/ tslint:disable:rule1 rule2 rule3… /上述注释表示本文件自此注释所在行开始,以下的所有区域关闭规则rule1 rule2 rule3…的检查。/ tslint:enable:rule1 rule2 rule3… /上述注释表示本文件自此注释所在行开始,以下的所有区域开启规则rule1 rule2 rule3…的检查。// tslint:disable-next-line上述注释表示此注释所在行的下一行关闭TSLint规则检查。someCode(); // tslint:disable-line上述注释表示此注释所在行关闭TSLint规则检查。// tslint:disable-next-line:rule1 rule2 rule3…上述注释表示此注释所在行的下一行关闭规则rule1 rule2 rule3…的检查检查。以上配置信息,这里具体参考了https://palantir.github.io/tslint/usage/rule-flags/。3.1 本地检查在完成工程配置后,需要下载所需要依赖包,要在工程所在根目录使用npm install命令完成下载依赖包。IDE环境提示在完成下载依赖包后,IDE环境可以根据对应配置文件进行提示,可以实时地提示出存在问题代码的错误信息,以VSCode为例:本地命令检查VSCode目前还有继续完善的空间,如果部分文件未在窗口打开的情况下,可能存在其中错误未提示出的情况,这时候,我们可以通过本地命令进行全工程的检查,在React Native工程的根目录下,通过以下命令行执行:tslint –project tsconfig.json –config tslint.json(此命令如果不正确运行,可在之前加入./node_modules/.bin/)即为:./node_modules/.bin/tslint –project tsconfig.json –config tslint.json从而会提示出类似以下错误的信息:src/Components/test.ts[1, 7]: Class name must be in pascal case3.2 在线CI检查本地进行代码检查的过程也会存在被人遗忘的可能性,通过技术的保障,可以避免人为遗忘,作为代码提交的标准流程,通过CI检查后再合并代码,可以有效避免代码错误的问题。CI系统可以为理解为一个云端的环境,环境配置与本地一致,在这种情况下,可以生成与本地一致的报告,在美团内部可以使用基于Jenkins的Castle CI系统, 生成结果与本地结果一致:3.3 其他方式代码检查不止局限上述阶段,在代码commit、pull request、打包等阶段均可触发。代码commit阶段,通过Hook方式可以触发代码检查,可以有效地将在线CI检查阶段强制提前,基本保证了在线CI检查的完全正确性。代码pull request阶段,通过在线CI检查可以触发代码检查,可以有效保证合入分支尤其是主分支的正确性。代码打包阶段,通过在线CI检查可以触发代码检查,可以有效保证打包代码的正确性。四、自定义编写TSLint规则4.1 为什么要自定义TSLint规则当前的TSLint规则虽然涵盖了比较普遍问题的一些代码检查,但是实践中还是存在一些问题的:团队中的个性化需求难以满足。例如,saga中的异步函数需要在最外层加try-catch,且catch块中需要加异常上报,这个明显在官方的TSLint规则无法实现,为此需要自定义的开发。官方规则的开启与配置不符合当前团队情况。基于以上原因其他团队也有自定义TSLint的先例,例如上文提到的tslint-microsoft-contrib、tslint-eslint-rules等。4.2 自定义规则步骤那自定义TSLint大概需要什么步骤呢,首先规则文件根据规范进行按部就班的编写规则信息,然后根据代码检查逻辑对语法树进行分析并编写逻辑代码,这也是自定义规则的核心部分了,最后就是自定义规则的使用了。自定义规则的示例直接参考官方的规则是最直接的,我们能这里参考一个比较简单的规则"class-name"。“class-name"规则上文已经提到,它的意思是对类命名进行规范,当团队中类相关的命名不规范,会导致项目代码风格不统一甚至其他出现的问题,而"class-name"规则可以有效解决这个问题。我们可以看下具体的源码文件:https://github.com/palantir/tslint/blob/master/src/rules/classNameRule.ts。然后将分步对此自定义规则进行讲解。第一步,文件命名规则命名必须是符合以下2个规则:驼峰命名。以’Rule’为后缀。第二步,类命名规则的类名是Rule,并且要继承Lint.Rules.AbstractRule这个类型,当然也可能有继承TypedRule这个类的时候,但是我们通过阅读源码发现,其实它也是继承自Lint.Rules.AbstractRule这个类。第三步,填写metadata信息metadata包含了配置参数,定义了规则的信息以及配置规则的定义。ruleName 是规则名,使用烤串命名法,一般是将类名转为烤串命名格式。description 一个简短的规则说明。descriptionDetails 详细的规则说明。rationale 理论基础。options 配置参数形式,如果没有可以配置为null。optionExamples 参数范例 ,如没有参数无需配置。typescriptOnly true/false 是否只适用于TypeScript。hasFix true/false 是否带有修复方式。requiresTypeInfo 是否需要类型信息。optionsDescrition options的介绍。type 规则的类型。规则类型有四种,分别为:“functionality”、“maintainability”、“style”、“typescript”。functionality : 针对于语句问题以及功能问题。maintainability:主要以代码简洁、可读、可维护为目标的规则。style:以维护代码风格基本统一的规则。typescript:针对于TypeScript进行提示。第四步,定义错误提示信息这个主要是在检查出问题的时候进行提示的文字,并不局限于使用一个静态变量的形式,但是大部分官方规则都是这么编写,这里对此进行介绍,防止引起歧义。第五步,实现apply方法apply主要是进行静态检查的核心方法,通过返回applyWithFunction方法或者返回applyWithWalker来进行代码检查,其实applyWithFunction方法与applyWithWalker方法的主要区别在于applyWithWalker可以通过IWalker实现一个自定义的IWaker类,区别如下:其中实现IWaker的抽象类AbstractWalker里面也继承了WalkContext,而这个WalkContext就是上面提到的applyWithFunction的内部实现类。第六步,语法树解析无论是applyWithFunction方法还是applyWithWalker方法中的IWaker实现都传入了sourceFile这个参数,这个相当于文件的根节点,然后通过ts.forEachChild方法遍历整个语法树节点。这里有两个查看AST语法树的工具:AST Explorer:https://astexplorer.net/ 对应源码:https://github.com/fkling/astexplorerTypeScript AST Viewer:https://ts-ast-viewer.com/ 对应源码:https://github.com/dsherret/ts-ast-viewerAST Explorer优点:在AST Explorer可以高亮显示所选中代码对应的AST语法树信息。缺点:不能选择对应版本的解析器,导致显示的语法树代码版本固定。语法树显示的信息相对较少。TypeScript AST Viewer优点:解析器对应版本可以动态选择:语法树显示的信息不仅显示对应的数字代码,还可为对应的实际信息:每个版本对应对kind信息数值可能会变动,但是对应的枚举名字是固定的,如下图:从而这个工具可以避免频繁根据其数值查找对应信息。缺点: 不能高亮显示代码对应的AST语法树区域,定位效率较低。综上,通过同时使用上述两个工具定位分析,可以有效地提高分析效率。第七步,检查规则代码编写通过ts.forEachChild方法对于语法树所有的节点进行遍历,在遍历的方法里可以实现自己的逻辑,其中节点的类为ts.Node:其中kind为当前节点的类型,当然Node是所有节点的基类,它的实现还包括Statement、Expression、Declaration等,回到开头这个"class-name"规则,我们的所有声明类主要是class与interface关键字,分别对应ClassExpression、ClassDeclaration、InterfaceDeclaration,我们可以通过上步提到的AST语法树工具,在语法树中看到其为一一对应的。在规则代码中主要通过isClassLikeDeclaration、isInterfaceDeclaration这两个方法进行判断的。其中isClassLikeDeclaration、isInterfaceDeclaration对应的方法我们可以在node.js文件中找到:判断是对应的类型时,调用addFailureAtNode方法把错误信息和节点传入,当然还可以调用addFailureAt、addFailure方法。最终这个规则编写结束了,有一点再次强调下,因为每个版本所对应的类型代码可能不相同,当判断kind的时候,一定不要直接使用各个类型对应的数字。第八步,规则配置使用完成规则代码后,是ts后缀的文件,而ts规则文件实际还是要用js文件,这时候我们需要用命令将ts转化为js文件:tsc ./src/.ts –outDir dist将ts规则生成到dist文件夹(这个文件夹命名用户自定),然后在tslint.json文件中配置生成的规则文件即可。之后在项目的根目录里面,使用以下命令既可进行检查:tslint –project tsconfig.json –config tslint.json同时为了未来新增规则以及规则配置的更好的操作性,建议可以封装到自己的规则包,以便与规则的管理与传播。总结TSLint的优点:速度快。相对于动态代码检查,检查速度较快,现有项目无论是在本地检查,还是在CI检查,对于由十余个页面组成的React Native工程,可以在1到2分钟内完成;灵活。通过配置规则,可以有效地避免常见代码错误与潜在的Bug;易扩展。通过编写配置自定义规则,可以及时准确快速查找出代码中特定风险点。TSLint缺点:规则的结果只有对与错两种等级结果,没有警告等级的的提示结果;无法直接报告规则报错数量,只能依赖其他手段统计;TSLint规则针对于当前单一文件可以有效地通过语法树进行分析判定,但对于引用到的其他文件中的变量、类、方法等,则难以通过AST语法树进行判定。使用结果及分析在美团,有十余个页面的单个工程首次接入TSLint后,检查出的问题有近百条。但是由于开启的规则不同,配置规则包的差异,检查后的数量可能为几十条到几千条甚至更多。现在已开发十余条自定义规则,在单个工程内,处理优化了数百处可能存在问题的代码。最终TSLint接入了相关React Native开发团队,成为了代码提交阶段的必要步骤。通过团队内部的验证,文章开头遇到的问题得到了有效地缓解,目标基本达到预期。TSLint在React Native开发过程中既保证了代码风格的统一,又保证了React Native开发人员的开发质量,避免了许多低级错误,有效地节省了问题排查和人员沟通的成本。同时利用自定义规则,能够将一些兼容性问题在内的个性化问题进行总结与预防,提高了开发效率,不用花费大量时间查找问题代码,又避免了在一个问题上跌倒多次的情况出现。对于不同经验的开发者而言,不仅可以进行友好的提示,也可以帮助快速地定位问题,将一个人遇到的经验教训,用极低的成本扩散到其他团队之中,将开发状态从“亡羊补牢”进化到“防患未然”。作者简介家正,美团点评Android高级工程师。2017 年加入美团点评,负责美团大交通的业务开发。 ...

January 22, 2019 · 1 min · jiezi

孩子们各显神通对付 iOS 12「屏幕使用时间」的限制

简评:2018 年秋季,苹果公司推出了 iOS 12,其中备受好评的一项改变是:增加了屏幕使用时间限制,以减轻沉迷手机的状况。三个月过去后,这项功能似乎并没有对孩子造成太多困扰,道高一尺魔高一丈,孩子们很快就找到了应对方法。在一个 Reddit 话题中,父母们分享了他们的孩子如何绕过 Apple 的「屏幕时间限制」。一位父亲引起了该话题的讨论,他的儿子找到了解决屏幕时间限制的方法 —— 当他的孩子在游戏中达到时间限制时,他只需删除该应用,然后重新下载就可以了。他父亲表示:我还能说什么呢?龟儿子太聪明了,我都生不起气,甚至还有点佩服……而为了绕过 YouTube 的时间限制,另一个孩子会在 iMessage 中发送自己的 YouTube 链接。然后,即使当 YouTube 使用时间被限制后,他也可以在 iMessage 应用中观看这个视频。在这个 9000 多 upvotes 的话题中,大人们发现,早在 Apple 推出屏幕时间之前,孩子们就一直在寻找方法来绕过父母限制他们玩手机的规则。甚至还有些人编写程序来覆盖或破解密码,然后在一天结束时重新设置所有内容(这个有点厉害)。突然想起了小时候在家偷看电视的经历 ????原文链接:Reddit | My kid managed to pass Screen time limit

January 22, 2019 · 1 min · jiezi

手把手教你在Flutter项目优雅的使用ORM数据库

Flutter ORM数据库介绍Flutter现在开发上最大的槽点可能就是数据库使用了,Flutter现在只提供了sqflite插件,这表明开发者手动写sql代码,建表、建索引、transation、db线程控制等等繁琐的事情必然接踵而至,这种数据库使用方式是最低效的了。例如IOS平台有coredata、realm等等的框架提供便捷的数据库操作,但来到flutter就又倒退回去裸写sql,这对大部分团队都是重大的成本。本文将详细介绍一种在Flutter项目中优雅的使用ORM数据库的方法,我们使用的ORM框架是包含在一个Flutter插件flutter_luakit_plugin(如何使用可参考介绍文章)中的其中一个功能,本文只详细介绍这套ORM框架的使用和实现原理。我们给出了一个demo。我们demo中实现了一个简单的功能,从一个天气网站上查询北京的天气信息,解析返回的json然后存数据库,下次启动优先从数据库查数据马上显示,再发请求向天气网站更新天气信息,就这么简单的一个功能。虽然功能简单,但是我们99%日常的业务逻辑也就是由这些简单的逻辑组成的了。下面是demo运行的效果图。看完运行效果,我们开始看看ORM数据库的使用。ORM数据库的核心代码都是lua,其中WeatherManager.lua是业务逻辑代码,其他的lua文件是ORM数据库的核心代码,全部是lua实现的,所有代码文件加起来也就120k左右,非常轻量。针对上面提到的天气信息的功能,我们来设计数据模型,从demo的展示我们看到每天天气信息包含几个信息,城市名、日出日落时间、最高温度、最低温度、风向、风力,然后为了区分是哪一天的数据,我们给每条信息加上个id的属性,作为主键。想好我们就开始定义第一个ORM数据模型,有几个必要的信息,db名,表名,后面的就是我们需要的各个字段了,我们提供IntegerField、RealField、BlobField、TextField、BooleandField。等常用的数据类型。weather 就是这个模型的名字,之后我们weather为索引使用这个数据模型。定义模型代码如下。weather = { dbname = “test.db”, tablename = “weather”, id = {“IntegerField”,{unique = true, null = false, primary_key = true}}, wind = {“TextField”,{}}, wind_direction = {“TextField”,{}}, sun_info = {“TextField”,{}}, low = {“IntegerField”,{}}, high = {“IntegerField”,{}}, city = {“TextField”,{}}, },定义好模型后,我们看看如何使用,我们跟着业务逻辑走,首先网络请求回来我们要生成模型对象存到数据库,分下面几步获取模型对象local Table = require(‘orm.class.table’)local _weatherTable = Table(“weather”)准备数据,建立数据对象local t = {}t.wind = flDict[v.fg]t.wind_direction = fxDict[v.ff]t.sun_info = v.fit.low = tonumber(v.fd)t.high = tonumber(v.fc)t.id = it.city = citylocal weather = _weatherTable(t)保存数据weather:save()读取数据_weatherTable.get:all():getPureData()是不是很简单,很优雅,什么建表、拼sql、transation、线程安全等等都不用考虑,傻瓜式地使用,一个业务就几行代码搞定。这里只演示了简单的存取,更多的select、update、联表等高级用法可参考db_test demo。Flutter ORM数据库原理详解好了,上面已经介绍完如何使用了,如果大家仅仅关心使用下面的可以不看了,如果大家想了解这套跨平台的ORM框架的实现原理,下面就会详细介绍,其实了解了实现原理,对大家具体业务使用还是很有好处的,虽然我感觉大家用的时候极少了解原理。我们把orm框架分为三层接入层,cache层,db操作层,三个层分别处于对应的线程,具体可以参考下图。接入层可以在任意线程发起,接入层也是每次数据库操作的发起点,上面的demo所有操作都是在接入层,cache层,db操作层仅仅是ORM内部划分,对使用者来讲不需要关心cache层和db操作层。我们把所有的操作分成两种,db后续相关的,和db后续无关的。db后续无关的操作是从接入层不同的线程进入到cache层的队列,所有操作在这个队列里先同步完成内存操作,然后即可马上返回接入层,异步再到db操作层进行db操作。db后续无关的操作包括 save、update、delete。db后续相关的操作依赖db操作层操作的结果,这样的话就必须等真实的db操作完成了再返回接入层。db后续相关的操作包括select。要做到这种数据同步,我们必须先把orm操作接口抽象化,只给几个常用的接口,所有操作都必须通过指定的接口来完成。我们总结了如下基本操作接口。1、save2、select where3、select PrimaryKey4、update where5、update PrimaryKey6、delete where7、delete PrimaryKey这七种操作只要在操作前返回前对内存中的cache做相应的处理,即可保证内存cache始终和db保持一致,这样以后我们就可以优先使用cache层的数据了。这七种操作的实现逻辑,这里先说明一下,cache里面的对象都是以主键为key,orm对象为value的形式存储在内存中的,这些控制逻辑是写在cache.lua里面的。下面详细介绍七种基本操作的逻辑。save操作,同步修改内存cache,然后马上返回接入层,再异步进行db replace into 的操作where条件select,这个必须先同步到db线程获取查询结果,再同步修改内存里面的cache值,再返回给接入层select PrimaryKey,就是选一定PrimaryKey值的orm对象,这个操作首先看cache里面是否有primarykey 值的orm对,如果有,直接返回,如果没有,先同步到db线程获取查询结果,再同步修改内存里面的cache值,再返回给接入层update where,先同步到db线程通过where 条件select出需要update的主键值,根据主键值和需要update的内容,同步更新内存cache,然后异步进行db的update操作update PrimaryKey,根据PrimaryKey进行update操作,先同步更新内存cache,然后异步进行db的update操作delete where,先同步到db线程通过where 条件select出需要delete的主键值,根据主键值删除内存cache,然后异步进行db的delete操作delete PrimaryKey,根据PrimaryKey进行delete操作,先同步删除内存cache,然后异步进行db的delete操作只要保证上面七种基本操作逻辑,即可保证cache中的内容和db最终的内容是一致的,这种尽量使用cache的特性可以提升数据库操作的效率,而且保证同一个db的所有操作都在指定的cache线程和db线程里面完成,也可以保证线程安全。最后,由于我们所有的db操作都集中起来了,我们可以定时的transation 保存,这样可以大幅提升数据库操作的性能。结语目前Flutter领域最大的痛点就是数据库操作,本文提供了一种优雅使用ORM数据库的方法,大幅降低了使用数据库的门槛。希望这篇文章和flutter_luakit_plugin可以帮到大家更方便的开发Flutter应用。 ...

January 21, 2019 · 1 min · jiezi

视频直播技术之iOS端推流

随着网络基础建设的发展和资费的下降,在这个内容消费升级的时代,文字、图片无法满足人们对视觉的需求,因此视频直播应运而生。承载了实时性Real-Time和交互性的直播云服务是直播覆盖各行各业的新动力。网易云信推出一系列文章,对视频直播技术进行深入讲解,本篇文章将向大家介绍iOS端的推流技术。相关阅读推荐《短视频技术详解:Android端的短视频开发技术》《视频直播:Windows中各类画面源的截取和合成方法总结》《视频直播关键技术:流畅、拥塞和延时追赶》直播架构想必了解过直播的人都清楚直播主要分为3部分:推流->流媒体服务器->拉流。而我们今天需要讲的就是推流这部分,它主要包括音视频采集,音视频前处理,音视频编码,推流和传输4个方面。但是由于网络的复杂性和大数据的统计,推流还需要有全局负载均衡调度GSLB(Global Server Load Balance),以及实时的统计数据上报服务器,包括提供频道管理给用户运营,因此推流SDK需要接入GSLB中心调度,统计服务器,心跳服务器,用于推流分配到网络最好的节点,有大数据的统计和分析。下图涵盖了直播相关的所有服务,红色小标的线条代表指令流向,绿色小标的线条代表数据流向。直播技术点音视频采集采集是所有环节中的第一环,我们使用的系统原生框架AVFoundation采集数据。通过iPhone摄像头(AVCaptureSession)采集视频数据,通过麦克风(AudioUnit)采集音频数据。目前视频的采集源主要来自摄像头采集、屏幕录制(ReplayKit)、从视频文件读取推流。音视频都支持参数配置。音频可以设置采样率、声道数、帧大小、音频码率、是否使用外部采集、是否使用外部音频前处理;视频可以设置帧率、码率、分辨率、前后摄像头、摄像头采集方向、视频端显示比例、是否开启摄像头闪光灯、是否打开摄像头响应变焦、是否镜像前置摄像头预览、是否镜像前置摄像头编码、是否打开滤镜功能、滤镜类型、是否打开水印支持、是否打开QoS功能、是否输出RGB数据、是否使用外部视频采集。音视频处理前处理模块也是主观影响主播观看效果最主要的环节。目前iOS端比较知名的是GPUImage,提供了丰富的预处理效果,我们也在此基础上进行了封装开发。视频前处理包含滤镜、美颜、水印、涂鸦等功能,同时在人脸识别和特效方面接入了第三方厂商FaceU。SDK内置4款滤镜黑白、自然、粉嫩、怀旧;支持16:9裁剪;支持磨皮和美白(高斯模糊加边缘检测);支持静态水印,动态水印,涂鸦等功能。音频前处理则包括回声抑制、啸叫、增益控制等。音视频都支持外部前处理。音视频编码编码最主要的两个难点是:1 处理硬件兼容性问题2 在高FPS、低bitrate和音质画质之间找个一个平衡点由于iOS端硬件兼容性比较好,因此可以采用硬编。SDK目前支持软件编码openH264,硬件编码VideoToolbox。而音频支持软件编码FDK-AAC和硬件编码AudioToolbox。视频编码的核心思想就是去除冗余信息:空间冗余:图像相邻像素之间有较强的相关性。时间冗余:视频序列的相邻图像之间内容相似。编码冗余:不同像素值出现的概率不同。视觉冗余:人的视觉系统对某些细节不敏感。音视频发送推流SDK使用的流媒体协议是RTMP(RealTime Messaging Protocol)。而音视频发送最困难的就是针对网络的带宽评估。由于从直播端到RTMP服务器的网络情况复杂,尤其是在3G和带宽较差的Wifi环境下,网络丢包、抖动和延迟经常发生,导致直播推流不畅。RTMP基于TCP进行传输,TCP自身实现了网络拥塞下的处理,内部的机制较为复杂,而且对开发者不可见,开发者无法根据TCP协议的信息判断当时的网络情况,导致发送码率大于实际网络带宽,造成比较严重的网络拥塞。因此我们自研开发了一款实时根据网络变化的QoS算法,用于实时调节码率、帧率、分辨率,同时将数据实时上报统计平台。模块设计&线程模型模块设计鉴于推流的主流程分为上述描述的4个部分:音视频采集、音视频前处理、音视频编码、音视频发送。因此将推流SDK进行模块划分为LSMediacapture层(对外API+服务器交互)、视频融合模块(视频采集+视频前处理)、音频融合模块(音频采集+音频前处理)、基础服务模块、音视频编码模块、网络发送模块。线程模型推流SDK总共含有10个线程。视频包含AVCaptureSession的原始采集线程、前处理线程、硬件编码线程、数据流向定义的采集线程、编码线程、发送线程。音频包含AudioUnit包含的原始采集线程、数据流向定义的采集线程、编码线程、发送线程。在数据流向定义的采集线程、编码线程、发送线程之间会创建2个bufferQueue,用于缓存音视频数据。采集编码队列可以有效的控制编码码率,编码发送队列可以有效自适应网络推流。QoS&跳帧下图是直播的主要流程,用户初始化SDK,创建线程,开始直播,音视频数据采集,编码,发送。在发送线程下,音视频数据发送,QoS开启,根据网络实时评估带宽,调整帧率,码率控制编码器参数,同时触发跳帧,调整分辨率控制采集分辨率参数。用户停止直播,反初始化SDK,销毁线程。QoS&跳帧可以有效的解决用户在网络不好的情况下,直播卡顿的问题。在不同的码率和分辨率情况下,都能够做到让用户流畅地观看视频直播。以上就是iOS端推流技术的详细讲解。另外,想要阅读更多关于视频直播技术的文章,可以移步网易云信博客。

January 21, 2019 · 1 min · jiezi

2019 年值得关注的 23 个开发者博客

如果你正在寻找编程技巧,或是想了解编程界发生了哪些新鲜事?那么,今天我们带来的 2019 年最佳开发者博客列表,一定是你的菜。这些博客将会帮助你发现新的工具,并带给你编程技巧的启发。让我们一起先睹为快吧!1.The Netflix Tech Blog如果你还没有听说过 Netflix,恐怕就有点“与世隔绝”了。近年来,在线流媒体平台可谓发展迅猛。自 1997 年上线以来,Netflix 在全球已拥有近 1.18 亿流媒体用户。它也成为了当地的科技巨头之一,吸引了众多优秀的开发者为其工作。为了便于大家更深入的了解,他们在 Medium 上开通了 Netflix 技术博客。你可以了解 Netflix 是如何设计、构建、运营其系统和工程组织的相关信息。地址:http://techblog.netflix.com/2.Code as Craft如果你居住在美国,你应该会经常在 Etsy 购物。这是一个以手工艺成品买卖为特色的网络购物网站。与 Netflix 技术博客相似,Etsy 的工程师也在名为“ Code as craft ”的技术博客上,分享他们的成果和经验。地址:https://codeascraft.com/3.Phpied如果你想学习更多的 JS 技巧,那一定要勤逛 Phpied 博客。它是由 Stoyan Stefanov 运营的。博客的作者 Stoyan Stefanov 是 Facebook 的工程师、前雅虎、YSlow 2.0 性能工具的架构师,同时也是 smush.it 图像优化工具的开发者。地址:http://www.phpied.com4.BlogsDope它是为数不多的印度开发者博客中,能够提供有价值信息的博客。它由 Arun Kumar 创立,涵盖了大部分流行的编程语言。这个平台也有不少免费的课程供你选择。即使你是新手,也会有所受益。地址:https://www.codesdope.com/blog/5.Coding Horror这个博客由 Jeff Atwood 创建于 2004 年,同时作为 StackOverFlow 的联合创始人,他还创立了 StackExchange 公司。他的博客上有许多编程相关的文章,可以供你参考与学习。地址:https://blog.codinghorror.com/6.Scott Hanselman BlogScott 是一名程序员、演讲家和教师,他曾在微软和 CheckFree 等公司工作。他的博客内容涉及广泛,从技术、文化、网络到日常的生活记录等。地址:http://www.hanselman.com/blog/7.TechieDelightTechieDelight 有超过 900 篇编程相关的文章,你还会发现一些代码片段、工具等实用的东西。如果你是 Java, C++ 等开发者,可以上去瞅瞅。地址:http://www.techiedelight.com8.MongoDB Blog你正在使用 MongoDB ?那么,你需要任何关于它的帮助,浏览它们的官方博客是个不错的办法。除了 MongoDB 之外,你还会发现很多实用的内容。地址:https://www.mongodb.com/blog9.Xaprb你对 Baron Schwartz 有所了解吗?他是一位软件工程师,因其对 MySQL 数据库的贡献而闻名于世。他目前是 VividCortex 的创始人兼首席技术官。如果你想深入了解他的技术之旅,可以在 Xaprb 上阅读其文章。 他针对技术,创业等方面撰写了不少文章,或许你能找到启发。地址:http://www.xaprb.com/blog/10.LinkedIn Engineering Blog你或许经常使用 LinkedIn,但如果你想了解平台背后的技术开发等细节的话,那你一定要阅读 LinkedIn 的技术博客。最近,他们针对如何建立 LinkedIn 平台上联系人系统的研究,进行了深入的分享。地址:http://engineering.linkedin.com/blog11.Facebook Code与 Netflix 和 LinkedIn 如出一辙,Facebook 的工程师团队也经常在他们的博客“ Facebook Code ”上进行技术内容的分享,并且他们的博客上还有部分视频资源。最近他们发表了一些关于 AI 研究的精彩文章。如果你有兴趣的话,不妨上去看看。地址:https://code.facebook.com/posts/12.Twitter Engineering blogTwitter 作为知名的科技公司,拥有一支伟大的创新技术团队。作为一名工程师,我推荐你经常浏览 Twitter 的技术博客,他们会定期分享有趣的故事。前一阵子,他们分享了如何使用神经网络进行智能裁剪,有兴趣也可以去看看。地址:https://blog.twitter.com13.DropBox Tech Blog如果你是数据科学家,我相信你一定会对全球最大的云平台背后的技术深感兴趣。他们在博客上分享了大量的研究案例,以及一些有趣的东西,这个技术博客应该是你的菜。地址:https://tech.dropbox.com14.SitepointSitepoint 作为程序员众所周知的网站之一,平台上已经提供了大约 240 多个电子书以及相关资源。博客主要涵盖 Wordpress,Web 和 JavaScript 等主题内容,同时它还有一个活跃的论坛,便于同行之间的互动与交流。地址:https://www.sitepoint.com15.StickyMindsStickyMinds 是一个老牌的软件测试博客,始于 2001 年。它是软件测试人员,QA 专业人士的首选。任何对软件测试感兴趣的人都可以通过博客中文章,深入的了解最新的测试技术,并获得一些指导和建议。如果你正从事软件测试工作,那么我强烈推荐这个博客给你。地址:https://www.stickyminds.com/16.Mozilla作为开发者,Mozilla 的火狐浏览器你一定使用过,而 Mozilla 创办的技术博客,也是值得关注的技术博客之一。你不光能了解最新的互联网趋势,还能够阅读到关于 Mozilla 版本更新、大事件等相关文章。地址:https://blog.mozilla.org/17.CodePen Blog多年来,CodePen 为开发者们沉淀了丰富且宝贵的资源,而它的技术博客也同样如此。博客涵盖了像挑战、活动、会议等相关内容。对于前端开发者来说,在这里你能学到不少实用的编程技巧。地址:https://blog.codepen.io18.A List Apart Blog如果你是 Web 开发者,那你一定会喜欢这个博客。这个博客拥有很多的教程和指南,对网站开发人员非常实用。除此之外,你还能收获到 Web 字体排版、用户体验、品牌识别等设计相关的知识。地址:https://alistapart.com/19.CSS Tricks如果你想深入研究 CSS,那么 CSS Tricks 博客是一个不错的地方。除了学习 CSS 知识与技巧,你还能找到一些实用的代码片段。博客上也有专门的工作推荐栏目,帮助 UI 设计人员、前端开发者了解企业的需求,快速入职。地址:https://css-tricks.com/20.Codrops Blog与 CSS Tricks 相似,Codrops 也是一个专注于网页设计和开发的博客,它上面会发布一些最新的 Web 趋势,技术以及新探索的文章和教程。地址:https://tympanus.net/codrops/21.OverOps blog如果你热衷 Java 和 .NET 技术的话,OverOps 博客是提升你技能的好地方。最近我在它上面浏览的时候,偶然发现一篇《 2018 年 100 个最佳的 Java 库》的文章,瞬间被惊艳到了。他们还会举办网络研讨会,你也会找到一些实用的电子书。地址:https://blog.overops.com/22.Code The Web又一个专注前端开发的博客。博客的内容涵盖了 HTML,CSS 和 JavaScript,文章通俗易懂、深入浅出。地址:https://codetheweb.blog/23.CodeSignal Blog这家位于旧金山的公司,正尝试通过他们的博客“ CodeSignal ”改变开发者编程的方式。对于正在招聘初级开发者的 CTO、高级开发者来说,这个博客很实用。地址:https://codesignal.com/blog/结论今天分享的这个博客清单,希望对你有所帮助。那么,哪一个博客是你最喜欢的呢?也欢迎你留言与大家分享。 ...

January 21, 2019 · 1 min · jiezi

iOS打包详解

背景今天使用Xcode打包上传ipa时遇到一个问题: Missing private key,如下图。通过查询是.cer 证书失效的问题,需要重新生成新的证书。苹果规定 .cer证书只能存在于一台机器上,因此 如果另一台电脑想要用的话,需要导出为.p12 file ,安装到另一台没有安装.cer文件的mac电脑。首先,给大家普及下基本知识,iOS有两种证书和描述文件:证书类型 使用场景 开发(Development)证书和描述文件 用于开发测试 发布(Distribution)证书和描述文件 用于提交Appstore,可使用Application Loader提交到Appstore审核发布 iOS证书(.p12)登录iOS Dev Center打开网站iOS Dev Center,使用苹果开发者账号登录iOS Dev Center。登录成功后,打开“iOS Certificates”页面,可以看到所有已经申请的证书及描述文件。申请苹果App ID在“iOS Certificates”页面“Identifiers"下选择“App IDs",可查看到已申请的所有App ID,点击右上角的加号可创建新的“App ID”。在“App Services”栏下选择应用要使用到的服务(如需要使用到消息推送功能,则选择“Push Notifications”)。例如:设置完成后选择“Continue”,弹出确认页面,确认后选择“Submit”提交,再次确认就可以在“App IDs"页面看到刚创建的App ID。生成证书请求文件对于iOS开发来说,不管是申请开发(Development)证书还是发布(Distribution)证书,都需要使用证书请求(.certSigningRequest)文件,证书请求文件需在Mac OS上使用“Keychain Access”工具生成。在“Spltlight Search”中搜索“Keychain”并打开“Keychain Access”工具:打开菜单“Keychain Access”->“Certificate Assistant”,选择“Request a Certificate From a Certificate Authority…”。打开创建请求证书页面,在页面中输入用户邮件地址(User Email Address)、证书名称(Common Name、请求类型(Request is),最后选择保存到磁盘(Saved to disk)。点击“Save”后保存证书请求文件。申请开发(Development)证书开发(Development)证书用于测试环境下使用,可以直接安装到手机上(不用提交到Appstore),但一个描述文件最多只能绑定100台设备(因此通过这种证书正式发布应用是行不通的)。申请开发证书在“Certificates, Identifiers & Profiles”页面“Certificates"下选择“Development",可查看到已申请的所有开发(Development)证书,点击右上角的加号可创建新的证书。打开“Add iOS Certificate”页面,在“Development”栏下选中“iOS App Development”。打开证书生成页面,点击“Choose File…”选择“生成证书请求文件”章节生成的“CertificateSigningRequest.certSigningRequest”文件,点击“Generate”生成cer证书成功。生成证书成功后打开证书下载页面,选择“Download”下载保存证书(ios_development.cer)。双击保存到本地的ios_development.cer文件导入到“Keychain Access”,导入成功后,可以在证书列表中显示。选中导入的证书,右键选择“Export “Developer” …”:打开证书保存页面,输入文件名、选择路径后点击“Save”,然后打开设置证书密码页面,输入密码及确认密码后点击“OK”。打开访问“Keychain Access”密码页面,输入Mac OS管理员密码,点击“Allow”,即可保存开发(Development)证书(如“HBuilderCert.p12”)。添加调试设备开发描述文件必须绑定设备,所以在申请开发描述文件之前,先添加调试的设备。具体来说,在“Certificates, Identifiers & Profiles”页面“Devices”下选择“All",可查看到已添加的所有设备信息,点击右上角的加号可添加新设备。打开“Registering a New Device or Multiple Devices”页面,输入设备名称和UDID。将设备连接到电脑,启动iTunes,点击次区域可切换显示设备的UDID,右键选择复制UUID。输入完成后,点击“Continue”继续,确认输入信息,如果没有错误点击“Register”即可完成添加。申请开发(Development)描述文件在“Certificates, Identifiers & Profiles”页面“Provisioning Profiles”下选择“Development",可查看到已申请的所有开发(Development)描述文件,点击右上角的加号可创建新描述文件。打开“Add iOS Provisioning Profile”页面,在“Development”栏下选中“iOS App Development”。点击“Continue”按钮,打开“App ID”选择页面,选择要使用的“App ID”,点击“Continue”。打开“Select certificates”页面,选择前面创建的开发证书。点击“Continue”,打开选择调试设备页面,选择用于调试安装的设备(最多100太设备)。点击“Generage”,生成描述文件成功,然后下载描述文件即可。申请发布证书发布(Production)证书用于正式发布环境下使用,用于提交到Appstore审核发布,申请的过程和申请开发(Development)证书类似。申请发布(Production)证书在“Certificates, Identifiers & Profiles”页面“Certificates"Production",可查看到已申请的所有发布(Production)证书,点击右上角的加号可创建新证书。打开“Add iOS Certificate”页面,在“Production”栏下选中“App Store and Ad Hoc”。打开确认证书请求页面,点击“Continue”继续。生成证书成功后打开证书下载页面,选择“Download”下载保存证书(ios_production.cer)。双击保存到本地的ios_production.cer文件导入到“Keychain Access”。导入成功后,可以在证书列表中显示。选中导入的证书,右键选择“Export “Developer” …”。同样,打开证书保存页面,输入文件名、选择路径后点击“Save”。打开访问“Keychain Access”密码页面,输入Mac OS管理员密码,点击“Allow”,保存开发(Production)证书(如“HBuilderCert.p12”)。申请发布(Distribution)描述文件在“Certificates, Identifiers & Profiles”页面“Provisioning Profiles”下选择“Distribution",可查看到已申请的所有发布(Distribution)描述文件,点击右上角的加号可创建新描述文件。打开“Add iOS Provisioning Profile”页面,在“Development”栏下选中“iOS App Development”。点击“Continue”按钮,打开“App ID”选择页面,选择要使用的“App ID”,点击“Continue”。打开“Select certificates”页面,选择前面创建的发布证书。点击“Generage”,生成描述文件成功。然后,下载描述文件点击安装即可。 ...

January 20, 2019 · 1 min · jiezi

如何阅读苹果开发文档

原文:How to read Apple’s developer documentation对于很多人来说,这篇文章听起来很奇怪,因为我们已经习惯了 Apple 的 API 文档的工作方式,因此我们精神上已经经过调整以快速找到我们想要的东西。但这是一个有趣的事实:去年我最热门的文章请求之一是帮助人们真正阅读 Apple 的代码文档。您如何找到您需要的 iOS API,如何浏览所有材料以找到您真正想要的内容,以及您如何深入了解为什么事情按照他们的方式工作?所以,如果你曾经寻求帮助来理解 Apple 的开发者文档,首先我要让你知道你并不孤单 - 许多人都在努力解决这个问题。但其次,我希望这篇文章会有所帮助:我会尽力解释它的结构,它有什么好处(以及不好的地方),以及如何使用它。更重要的是,我将向您展示经验丰富的开发人员寻找额外信息的位置,这些信息通常比Apple的在线文档更有价值。<!–more–>“这是什么?” vs “你怎么用它?”任何书面的 API 文档通常采用以下五种形式之一:接口代码,显示了什么是方法名称和参数,属性名称和类型,以及类似的,带有一些描述它应该做什么的文本。API 的文本描述了它应该做什么以及一般指导用例。广泛使用的有用的 API 示例代码。如何使用 API 代码段。解决常见问题的简单教程:如何做 X,如何做 Y,以及如何做 Z 等等。粗略地说,苹果公司第一点做了很多,其次是第二点和第三点,第四点很少,第五点几乎没有。所以,如果你正在寻找“如何用 Y 做 X ”的具体例子,你最好从我的 Swift 知识库开始 - 这正是它的用途。了解 Apple 的文档解决的问题,可以帮助您从中获得最大收益。它并不是一个结构化的教程,它不会向您介绍一系列概念来帮助您实现目标,而是作为 Apple 支持的数千个 API 的参考指南。寻找一个类Apple的在线文档位于 https://developer.apple.com/d… ,虽然您能在 Xcode 中使用本地副本,但我会说大多数人使用在线版本只是因为他们可以更容易地找到内容。绝大多数 Apple 的文档都描述了接口,而这正是大多数时候你会看到的。我想使用一个实际的例子,所以请先在您的网络浏览器中打开https://developer.apple.com/d… ,这是所有Apple开发者文档的主页。您会看到所有 Apple 的 API 分为 App Frameworks 或 Graphics and Games 等类别,您已经看到了一件重要的事情:所有深蓝色文本都是可点击的,并会带您进入特定框架的API文档。是的,它使用相同的字体和大小,没有下划线,说实话,深蓝色链接和黑色文本之间没有太大区别,但你仍然需要留意这些链接 - 有很多他们,你会用它们来深入挖掘主题。现在请从 App Frameworks 类别中选择 UIKit,您将看到它的功能(为iOS创建用户界面)的简要概述,标有“重要”(Important)的大黄色框,然后是类别列表。那些黄色的盒子确实值得关注:虽然它们经常被使用,它们几乎总能阻止你犯下根本错误,这些错误导致出现奇怪的问题。此页面上重要的是共同描述 UIKit 的类别列表。这是人们经常迷路的地方:他们想要了解像 UIImage 这样的东西,所以他们必须仔细查看该列表以找到它可能出现的合适位置。在这种情况下,您可能会查看“资源管理”(Resource Management),因为它的副标题“管理存储在主可执行文件之外的图像,字符串,故事板和 nib 文件”听起来很有希望。但是,您会感到失望 - 您需要向下滚动到 “图像和 PDF”(Images and PDF)部分才能找到 UIImage。这就是为什么我谈过的大多数人只是使用自己喜欢的搜索引擎。他们输入他们关心的类,并且 - 只要它有“UI”,“SK”或类似的前缀 - 它可能是第一个结果。不要误会我的意思:我知道这种做法并不理想。但是面对搜索一个类或者去 https://developer.apple.com/d… ,选择一个框架,选择一个类别,然后选择一个类,第一个就是更快。重要提示:无论您选择哪种方法,最终都会在同一个地方,所以只做最适合您的方法。现在,请找到并选择 UIImage。阅读类的接口一旦选择了您关心的类,该页面就有四个主要组件:概述,版本摘要,接口和关系。概述是“API的文本描述,描述了它应该做什么以及一般指导用例”,我之前提到过 - 我要求你选择 UIImage,因为它是文本描述何时运行良好的一个很好的例子。当它是我第一次使用的类时,特别是如果它刚刚推出时,我通常会阅读概述文本。但是对于其他一切 - 我之前至少使用过一次的任何类 - 我跳过它并尝试找到我所得到的具体细节。请记住,Apple 文档确实不是一种学习工具:当您考虑到特定目的时,它最有效。如果您不总是为所选 Apple 平台的最新版本开发,则版本摘要 - 页面右侧的侧栏 - 非常重要。在这种情况下,您将看到 iOS 2.0 +,tvOS 9.0+ 和 watchOS 2.0+,它告诉我们何时 UIImage 类首次在这三个操作系统上可用,并且它仍然可用 - 如果它已被弃用(不再可用)你会看到像 iOS 2.0-9.0 这样的东西。此页面上的实际内容 - 以及作为 Apple 框架中特定类的主页的所有页面 - 都列在“主题”标题下。这将列出该类支持的所有属性和方法,再次分解为使用类别:“获取图像数据”,“获取图像大小和比例”等。如果您选择的类具有任何自定义初始化方法,则始终会首先显示它们。 UIImage 有很多自定义初始化方法,你会看到它们都被列为签名 - 只是描述它所期望的参数的部分。所以,你会看到这样的代码:init?(named: String)init(imageLiteralResourceName: String)提示:如果您看到 Objective-C 代码,请确保将语言更改为 Swift。您可以在页面的右上角执行此操作,也可以在重要的 iOS 测试版引入更改时启用 API 更改选项。记住,初始化方法写成 init? 而不是 init 的是容易出错的 - 它们返回一个可选项,以便在初始化失败时它们可以返回 nil。在初始化器的正下方,您有时会看到一些用于创建类的高度专业化实例的方法。这些不是 Swift 意义上的初始化器,但它们确实创建了类的实例。对于 UIImage,你会看到这样的事情:class func animatedImageNamed(String, duration: TimeInterval) -> UIImage?class func 部分意味着你将使用 UIImage.animatedImageNamed() 方式调用。在初始化程序之后,事情变得有点不那么有条理:你会发现属性方法和枚举自由混合在一起。虽然您可以滚动查找您要查找的内容,但我可以认为大多数人只需要 Cmd + F 在页面上查找文字就可以了!有三点需要注意:嵌套类型 - 类,结构和枚举 - 与属性和方法一起列出,这需要一点时间习惯。例如,UIImage 包含嵌套的枚举 ResizingMode。任何带有直线穿过的东西都是不推荐使用的。这意味着 Apple 打算在某些时候将其删除,因此您不应将其用于将来的代码,并建议开始重写任何现有代码。(在实践中,大多数API长期以来都被“弃用” - 许多许多年。)一些非常复杂的类 - 例如,UIViewController - 会将额外的文档页面与其方法和属性混合在一起。查找它们旁边的页面图标,以及一个简单的英文标题,如“相对于安全区域定位内容”(Positioning Content Relative to the Safe Area)。在页面的底部你会找到 Relationships,它告诉你它继承了哪个类(在这种情况下它直接来自 NSObject),以及它符合的所有协议。当您查看 Swift 类型时,本节更有用,其中协议关系更复杂。阅读属性或方法页面您已经选择了一个框架和类,现在是时候查看特定的属性或方法了。查找并选择此方法:class func animatedResizableImageNamed(String, capInsets: UIEdgeInsets, resizingMode: UIImage.ResizingMode, duration: TimeInterval) -> UIImage?您应该在 Creating Specialized Image Objects 类别中找到它。这不是一个复杂的方法,但它确实展示了这些页面的重要部分:Apple 有几种不同的方法来编写方法名称。之前的那个 - 长 class func animatedResizableImageNamed - 然后是方法页面标题中显示的形式(animatedResizableImageNamed(_:capInsets:resizingMode:duration:)),以及方法页面的声明部分中的形式。正如您在版本摘要中所看到的(在右侧),此方法在 iOS 6.0 中引入。因此,虽然主要的 UIImage 类从第1天开始就已存在,但这种方法是在几年后推出的。方法声明的各个部分都是可点击的,都是紫色的。但是要小心:如果你单击 UIImage.ResizingMode,你将去哪里取决于你是否点击了“UIImage”或“ResizingMode”。 (提示:您通常需要单击右侧的那个。)您将看到每个参数含义和返回值的简要说明。“讨论”(Discussion)部分详细介绍了此方法的具体使用说明。这几乎总是 - 每个页面中最有用的部分,因为在这里您可以看到“不要调用此方法”或“小心……”你可能会找到一个 See Also 部分,但这有点受欢迎 - 这里只是我们在上一页的方法列表。现在,UIImage 是一个老类,并没有太大变化,因此它的文档处于良好状态。但是一些较新的 API - 以及许多没有像 UIKit 那样被喜欢的旧 API - 仍然记录不足。例如,来自 SceneKit 的 SCNAnimation 或来自 UIKit 的 UITextDragPreviewRenderer:两者都是在 iOS 11 中引入的,并且在它们发布18个月后仍然包含“无可用概述(No overview available)”作为其文档。当你看到“没有可用的概述(No overview available)”时,你会失望,但不要放弃:让我告诉你接下来要做什么……查看代码尽管 Apple 的在线文档相当不错,但您经常会遇到“无可用概述(No overview available)”,或者您发现没有足够的信息来回答您的问题。康威定律(Conway’s law)指出,设计系统的组织受制于设计,这些设计是这些组织的通信结构的副本。“也就是说,如果你以某种方式工作,你将以类似的方式设计事物。Apple 在我们行业中的独特地位使他们以一种相当不寻常的方式工作 - 几乎可以肯定它与您自己公司的工作方式完全不同。是的,他们有 API 审核讨论,他们试图讨论API应该如何用两种语言看待,是的,他们有专门的团队来制作文档和示例代码。但是他们获取示例代码的门槛非常高:通常需要一些非常好的东西才能拿出来,并且通过多层检查来处理法律问题。因此,虽然我可以在一小时内输入一个项目并立即将其发布为文章,但 Apple 需要花费更长的时间才能完成同样的工作 - 他们非常认真地对待他们的形象,而且非常正确。如果你曾经想过为什么文章很少出现在官方 Swift 博客上,现在你知道了!现在,我说这一切的原因是 Apple 有一个快速使用的捷径:他们的工程师在他们的代码中留下评论的门槛似乎显着降低,这意味着你经常会在 Xcode 中找到有价值的信息。这些评论就像细小的金子(gold dust)一样:它们直接来自 Apple 的开发者,而不是他们的开发者出版(developer publications)团队,而且我对 devpubs 非常热爱,很高兴直接听到来自源头的声音。还记得我提到过 SceneKit 的 SCNAnimation 在 Apple 的开发者网站上没有记录吗?好吧,让我们来看看Xcode可以向我们展示的内容:按 Cmd + O 打开“快速打开(Open Quickly)”菜单,确保右侧的 Swift 图标为彩色而不是空心,然后键入“SCNAnimation”。您将看到列出的一些选项,但您正在寻找 SCNAnimation.h 中定义的选项。如果您不确定,选择 YourClassName.h 文件是您最好的选择。无论如何,如果你打开 SCNAnimation.h Xcode 将显示生成的 SCNAnimation 头文件版本。它的生成是因为原始版本是Objective-C,因此 Xcode 为 Swift 进行了实时翻译 - 这就是 Swift Quickly 框中的彩色 Swift 徽标的含义。现在,如果你按 Cmd + F 并搜索“class SCNAnimation”,你会发现:/** SCNAnimation represents an animation that targets a specific key path. /@available(iOS 11.0, )open class SCNAnimation : NSObject, SCNAnimationProtocol, NSCopying, NSSecureCoding { /! Initializers / / Loads and returns an animation loaded from the specified URL. @param animationUrl The url to load. */ public /not inherited/ init(contentsOf animationUrl: URL)这只是一个开始。 是的,该类及其所有内部都有文档,包括用法说明,默认值等。 所有这一切都应该在在线文档中,但无论出于什么原因它仍然没有,所以要准备好查找代码作为一个有用的补充。最后的提示此时,您应该能够查找在线文档以获取您喜欢的任何代码,并查找头文件注释以获取额外的使用说明。但是,在准备好面对全部 Apple 文档之前,还有两件事需要了解。首先,您经常会遇到标记为“已归档(archived”)”,“遗留(“legacy”)”或“已退休(“retired)”的文档 - 即使对于相对较新的事物也是如此。当它真的老了,你会看到诸如“这篇文章可能不代表当前发展的最佳实践”之类的消息。下载和其他资源的链接可能不再有效。“尽管 Apple 是世界上最大的公司之一,但 Apple 的工程和 devpubs 团队几乎没有人员 - 他们不可能在保留所有内容的同时覆盖新的 API。因此,当你看到“存档”文档或类似文件时,请运用你的判断:如果它在某个版本的 Swift 中至少你知道它最近是模糊的,但即使不是,你仍然可能会发现那里有很多有价值的信息。其次,Apple 拥有一些特别有价值的出色文档。这些都列在 https://developer.apple.com 的页脚中,但主要是人机界面指南。这将引导您完整地为 Apple 平台设计应用程序的所有部分,包括说明关键点的图片,并提供大量具体建议。即使这个文档是构建 iOS 应用程序时最重要的一个,但很少有开发人员似乎已经阅读过它!接下来做什么?我之前曾写过关于 Apple 文档的问题 - 我担心那里没有鼓励,但至少如果你在努力,它可能会让你觉得不那么孤单。幸运的是,我有很多可能更有用的材料:我的Swift知识库(Swift Knowledge Base)包含针对 Swift 和 iOS 开发人员的600多个问答,技巧和技术点 - 它可以帮助您更快地解决问题。我的Swift术语表(Glossary of Common Swift Terms)在 Swift 开发中定义了100多个常用术语,所有术语都在一页上。我有一本全书使用项目教授 Swift 和 iOS,它专门用于在逻辑流程中引入概念。您认为阅读Apple文档最有效的方法是什么? 在Twitter上发送你的提示:@twostraws。 ...

January 18, 2019 · 2 min · jiezi

iOS开发小技巧合集

本文主要记录日常工作中积累的一些iOS小技巧SDWebImage 加载大量高清图片时内存暴增解决方案:关闭SD加载高清大图时的解压缩static BOOL SDImageCacheOldShouldDecompressImages = YES; static BOOL SDImagedownloderOldShouldDecompressImages = YES; - (void)viewDidLoad { [super viewDidLoad]; // 关闭SD加载高清大图时的解压缩 SDImageCache *canche = [SDImageCache sharedImageCache]; SDImageCacheOldShouldDecompressImages = canche.shouldDecompressImages; canche.shouldDecompressImages = NO; SDWebImageDownloader *downloder = [SDWebImageDownloader sharedDownloader]; SDImagedownloderOldShouldDecompressImages = downloder.shouldDecompressImages; downloder.shouldDecompressImages = NO; } -(void)dealloc { SDImageCache *canche = [SDImageCache sharedImageCache]; canche.shouldDecompressImages = SDImageCacheOldShouldDecompressImages; SDWebImageDownloader *downloder = [SDWebImageDownloader sharedDownloader]; downloder.shouldDecompressImages = SDImagedownloderOldShouldDecompressImages; } SDWebImage本地缓存有时候会害人。如果之前缓存过一张图片,即使下次服务器换了这张图片,但是图片url没换,用SDWebimage下载下来的还是以前那张,所以遇到这种问题,不要先去怼服务器,清空下缓存再试就好了。禁止手机睡眠[UIApplication sharedApplication].idleTimerDisabled = YES;隐藏某行cell- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{// 如果是你需要隐藏的那一行,返回高度为0 if(indexPath.row == YouWantToHideRow){ return 0; } return 44;}// 然后再你需要隐藏cell的时候调用[self.tableView beginUpdates];[self.tableView endUpdates];禁用button高亮button.adjustsImageWhenHighlighted = NO;或者在创建的时候UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];动画切换window的根控制器[UIView transitionWithView:[UIApplication sharedApplication].keyWindow duration:.5f options:UIViewAnimationOptionTransitionCrossDissolve animations:^{ BOOL oldState = [UIView areAnimationsEnabled]; [UIView setAnimationsEnabled:NO]; [UIApplication sharedApplication].keyWindow.rootViewController = [[RootViewController alloc]init]; [UIView setAnimationsEnabled:oldState]; } completion:^(BOOL finished) { }];去除数组中重复的对象NSArray *newArr = [oldArr valueForKeyPath:@"@distinctUnionOfObjects.self"];编译的时候遇到 no such file or directory: /users/apple/XXX是因为编译的时候,在此路径下找不到这个文件,解决这个问题,首先是是要检查缺少的文件是不是在工程中,如果不在工程中,需要从本地拖进去,如果发现已经存在工程中了,或者拖进去还是报错,这时候需要去build phases中搜索这个文件,这时候很可能会搜出现两个相同的文件,这时候,有一个路径是正确的,删除另外一个即可。如果删除了还是不行,需要把两个都删掉,然后重新往工程里拖进这个文件即可iOS8系统中,tableView最好实现下-tableView: heightForRowAtIndexPath:这个代理方法,要不然在iOS8中可能就会出现显示不全或者无法响应事件的问题iOS8中实现侧滑功能的时候这个方法必须实现,要不然在iOS8中无法侧滑// 必须写的方法,和editActionsForRowAtIndexPath配对使用,里面什么不写也行- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {}三个比较特殊的通知NSSystemTimeZoneDidChangeNotification监听修改时间界面的两个按钮状态变化UIApplicationSignificantTimeChangeNotification 监听用户改变时间 (只要点击自动设置按钮就会调用)NSSystemClockDidChangeNotification 监听用户修改时间(时间不同才会调用)获取不到UICollectionView指定cell的问题[self.collectionView layoutIfNeeded];//添加这句话就好 QTMResContinueEditeCell *cell = (QTMResContinueEditeCell )[self.collectionView cellForItemAtIndexPath:[NSIndexPath indexPathForItem:_index inSection:0]];跳进app权限设置// 跳进app设置if (UIApplicationOpenSettingsURLString != NULL) { UIApplication application = [UIApplication sharedApplication]; NSURL URL = [NSURL URLWithString:UIApplicationOpenSettingsURLString]; if ([application respondsToSelector:@selector(openURL:options:completionHandler:)]) { [application openURL:URL options:@{} completionHandler:nil]; } else { [application openURL:URL]; }} 给一个view截图- (UIImage )cutImageWithView:(UIView )view{ UIGraphicsBeginImageContextWithOptions(view.frame.size, NO, 0); [view.layer renderInContext:UIGraphicsGetCurrentContext()]; UIImage image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return image;}注意对象为nil的时候,调用此对象分类的方法不会执行collectionView的内容小于其宽高的时候是不能滚动的,设置可以滚动:collectionView.alwaysBounceHorizontal = YES;collectionView.alwaysBounceVertical = YES;颜色转图片+ (UIImage )imageWithColor:(UIColor )color { //描述一个矩形 CGRect rect = CGRectMake(0.0f, 0.0f, 1.0f, 1.0f); //开启图形上下文 UIGraphicsBeginImageContextWithOptions(rect.size, NO, 0); //获得图形上下文 CGContextRef ctx = UIGraphicsGetCurrentContext(); //使用color演示填充上下文 CGContextSetFillColorWithColor(ctx, [color CGColor]); //渲染上下文 CGContextFillRect(ctx, rect); UIImage image = UIGraphicsGetImageFromCurrentImageContext(); //关闭图形上下文 UIGraphicsEndImageContext(); return image; }view设置圆角#define ViewBorderRadius(View, Radius, Width, Color)\[View.layer setCornerRadius:(Radius)];[View.layer setMasksToBounds:YES];[View.layer setBorderWidth:(Width)];[View.layer setBorderColor:[Color CGColor]] // view圆角view某个角度设置圆角UIBezierPath maskPath = [UIBezierPath bezierPathWithRoundedRect:self.whiteView.bounds byRoundingCorners:UIRectCornerTopLeft | UIRectCornerTopRight cornerRadii:CGSizeMake(10, 10)]; CAShapeLayer maskLayer = [[CAShapeLayer alloc] init]; maskLayer.frame = self.whiteView.bounds; maskLayer.path = maskPath.CGPath; self.whiteView.layer.mask = maskLayer; ##指定了需要成为圆角的角该参数是UIRectCorner类型的,可选的值有: UIRectCornerTopLeft UIRectCornerTopRight UIRectCornerBottomLeft UIRectCornerBottomRight UIRectCornerAllCorners 由角度转换弧度#define DegreesToRadian(x) (M_PI * (x) / 180.0)由弧度转换角度#define RadianToDegrees(radian) (radian180.0)/(M_PI)获取图片资源//建议使用前两种宏定义,性能高于后者#define LOADIMAGE(file,ext) [UIImage imageWithContentsOfFile:[[NSBundle mainBundle]pathForResource:file ofType:ext]]#define IMAGE(A) [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:A ofType:nil]]#define ImageNamed(_pointer) [UIImage imageNamed:[UIUtil imageName:_pointer]]#define UIImageName(name) [UIImage imageNamed:name]文件路径//获取temp#define PathTemp NSTemporaryDirectory()//获取Document#define PathDocument [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]//获取 Cache#define PathCache [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject]GCD代码只执行一次#define kDISPATCH_ONCE_BLOCK(onceBlock) static dispatch_once_t onceToken; dispatch_once(&onceToken, onceBlock);自定义NSLog//DEBUG 模式下打印日志,当前行#ifdef DEBUG# define DLog(fmt, …) NSLog((@"%s [Line %d] " fmt), PRETTY_FUNCTION, LINE, ##VA_ARGS);#else# define DLog(…)#endif//重写NSLog,Debug模式下打印日志和当前行数#if DEBUG#define NSLog(FORMAT, …) fprintf(stderr,"\nfunction:%s line:%d content:%s\n", FUNCTION, LINE, [[NSString stringWithFormat:FORMAT, ##VA_ARGS] UTF8String]);#else#define NSLog(FORMAT, …) nil#endif//DEBUG 模式下打印日志,当前行 并弹出一个警告#ifdef DEBUG# define ULog(fmt, …) { UIAlertView alert = [[UIAlertView alloc] initWithTitle:[NSString stringWithFormat:@"%s\n [Line %d] “, PRETTY_FUNCTION, LINE] message:[NSString stringWithFormat:fmt, ##VA_ARGS] delegate:nil cancelButtonTitle:@“Ok” otherButtonTitles:nil]; [alert show]; }#else# define ULog(…)#endifFont#define FONTS(size) [UIFont systemFontOfSize:(size)]#define BOLDFONTS(size) [UIFont boldSystemFontOfSize:(size)]GCD//主线程#define kDISPATCH_MAIN_THREAD(mainQueueBlock) dispatch_async(dispatch_get_main_queue(), mainQueueBlock);//异步线程#define kDISPATCH_GLOBAL_QUEUE_DEFAULT(globalQueueBlock) dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), globalQueueBlocl);通知#define NOTIF_ADD(n, f) [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(f) name:n object:nil]#define NOTIF_POST(n, o) [[NSNotificationCenter defaultCenter] postNotificationName:n object:o]#define NOTIF_REMV() [[NSNotificationCenter defaultCenter] removeObserver:self]Color// RGB颜色转换(16进制->10进制)#define UIColorFromRGB(rgbValue) [UIColor colorWithRed:((float)((rgbValue & 0xFF0000) >> 16))/255.0 green:((float)((rgbValue & 0xFF00) >> 8))/255.0 blue:((float)(rgbValue & 0xFF))/255.0 alpha:1.0]// 获取RGB颜色#define RGBA(r,g,b,a) [UIColor colorWithRed:r/255.0f green:g/255.0f blue:b/255.0f alpha:a]//清除背景色#define CLEARCOLOR [UIColor clearColor]// 随机色#define RandomCOLOR RGBCOLOR(arc4random_uniform(256),arc4random_uniform(256),arc4random_uniform(256))获取window+(UIWindow)getWindow { UIWindow win = nil; //[UIApplication sharedApplication].keyWindow; for (id item in [UIApplication sharedApplication].windows) { if ([item class] == [UIWindow class]) { if (!((UIWindow)item).hidden) { win = item; break; } } } return win;}修改textField的placeholder的字体颜色、大小[textField setValue:[UIColor redColor] forKeyPath:@"_placeholderLabel.textColor”];[textField setValue:[UIFont boldSystemFontOfSize:16] forKeyPath:@"_placeholderLabel.font"];// 或者textField.attributedPlaceholder = [[NSAttributedString alloc] initWithString:placeholder attributes:@{NSFontAttributeName : [UIFont systemFontOfSize:13],NSForegroundColorAttributeName : [UIColor grayColor]}];APP缓存/ * 存 数组数据/+(void)setObectOfArray:(NSArray *)array fileName:(NSString )fileName{ //缓存文件的 根路径 NSString path = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject]; // 缓存 文件夹 路径 NSString filePath = [path stringByAppendingPathComponent:@“XXX.dataCache”]; NSFileManager fileManager = [NSFileManager defaultManager]; if ([fileManager fileExistsAtPath:filePath] == NO) { [fileManager createDirectoryAtPath:filePath withIntermediateDirectories:YES attributes:nil error:nil]; } //缓存文件的路径 NSString * cacheFilePath = [filePath stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.plist",fileName]]; BOOL isSuccess = [array writeToFile:cacheFilePath atomically:NO]; if (isSuccess) { NSLog(@“缓存数据成功:—%@",cacheFilePath); }else { NSLog(@“缓存数据失败:—%@",cacheFilePath); } }/ 取 数组数据/+(NSArray *)cacheArrayForFileName:(NSString *)fileName{ //缓存文件的 根路径 NSString path = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject]; // 缓存 文件夹 路径 NSString filePath = [path stringByAppendingPathComponent:@“XXX.dataCache”]; //缓存文件的路径 NSString * cacheFilePath = [filePath stringByAppendingPathComponent:[NSString stringWithFormat:@”%@.plist”,fileName]]; //取得缓存文件 NSArray array = [NSArray arrayWithContentsOfFile:cacheFilePath]; return array;}/ 清除 这个全部的缓存数据 */+(void)clearCacheListData:(void (^)())completion{ // 异线程 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ // 清楚缓存 //缓存文件的 根路径 NSString path = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject]; // 缓存 文件夹 路径 NSString filePath = [path stringByAppendingPathComponent:@“FortuneDonkey.dataCache”]; NSFileManager manager = [NSFileManager defaultManager]; //移除文件夹 [manager removeItemAtPath:filePath error:nil]; // 创建一个新的文件夹 [manager createDirectoryAtPath:filePath withIntermediateDirectories:YES attributes:nil error:nil]; if (completion) { //回调主线程 dispatch_async(dispatch_get_main_queue(), ^{ completion(); }); } });}/ 清除SD缓存数据 */-(void)clearSDCacheData{ //先清除内存中的图片缓存 [[SDImageCache sharedImageCache] clearMemory]; //清除磁盘的缓存 [[SDImageCache sharedImageCache] clearDisk];}获取APP缓存大小- (CGFloat)getCachSize { NSUInteger imageCacheSize = [[SDImageCache sharedImageCache] getSize]; //获取自定义缓存大小 //用枚举器遍历 一个文件夹的内容 //1.获取 文件夹枚举器 NSString *myCachePath = [NSHomeDirectory() stringByAppendingPathComponent:@“Library/Caches”]; NSDirectoryEnumerator *enumerator = [[NSFileManager defaultManager] enumeratorAtPath:myCachePath]; __block NSUInteger count = 0; //2.遍历 for (NSString *fileName in enumerator) { NSString *path = [myCachePath stringByAppendingPathComponent:fileName]; NSDictionary fileDict = [[NSFileManager defaultManager] attributesOfItemAtPath:path error:nil]; count += fileDict.fileSize;//自定义所有缓存大小 } // 得到是字节 转化为M CGFloat totalSize = ((CGFloat)imageCacheSize+count)/1024/1024; return totalSize;}几个常用权限判断 if ([CLLocationManager authorizationStatus] ==kCLAuthorizationStatusDenied) { NSLog(@“没有定位权限”); } AVAuthorizationStatus statusVideo = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; if (statusVideo == AVAuthorizationStatusDenied) { NSLog(@“没有摄像头权限”); } //是否有麦克风权限 AVAuthorizationStatus statusAudio = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio]; if (statusAudio == AVAuthorizationStatusDenied) { NSLog(@“没有录音权限”); } [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) { if (status == PHAuthorizationStatusDenied) { NSLog(@“没有相册权限”); } }];系统相关的一些方法//获取系统版本#define IOS_VERSION [[[UIDevice currentDevice] systemVersion] floatValue]#define IS_IOS10_OR_LATER ([[[UIDevice currentDevice] systemVersion] floatValue] >= 10.0)//获取当前语言#define CurrentLanguage ([[NSLocale preferredLanguages] objectAtIndex:0])//判断是否 Retina屏、设备是否%fhone 5、是否是iPad#define isRetina ([UIScreen instancesRespondToSelector:@selector(currentMode)] ? CGSizeEqualToSize(CGSizeMake(640, 960), [[UIScreen mainScreen] currentMode].size) : NO)#define iPhone5 ([UIScreen instancesRespondToSelector:@selector(currentMode)] ? CGSizeEqualToSize(CGSizeMake(640, 1136), [[UIScreen mainScreen] currentMode].size) : NO)#define iPhone6 ([UIScreen instancesRespondToSelector:@selector(currentMode)] ? CGSizeEqualToSize(CGSizeMake(750, 1334), [[UIScreen mainScreen] currentMode].size) : NO)#define isPad (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)//判断是真机还是模拟器#if TARGET_OS_IPHONE//iPhone Device#endif#if TARGET_IPHONE_SIMULATOR//iPhone Simulator#endif长按复制功能- (void)viewDidLoad{ [self.view addGestureRecognizer:[[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(pasteBoard:)]];}- (void)pasteBoard:(UILongPressGestureRecognizer )longPress { if (longPress.state == UIGestureRecognizerStateBegan) { UIPasteboard pasteboard = [UIPasteboard generalPasteboard]; pasteboard.string = @“我是文字”; }}判断图片类型//通过图片 Data 数据第一个字节 来获取图片扩展名- (NSString )contentTypeForImageData:(NSData )data{ uint8_t c; [data getBytes:&c length:1]; switch (c) { case 0xFF: return @“jpeg”; case 0x89: return @“png”; case 0x47: return @“gif”; case 0x49: case 0x4D: return @“tiff”; case 0x52: if ([data length] < 12) { return nil; } NSString testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding]; if ([testString hasPrefix:@“RIFF”] && [testString hasSuffix:@“WEBP”]) { return @“webp”; } return nil; } return nil;}获取手机和 APP 信息 //手机序列号NSString identifierNumber = [[UIDevice currentDevice] uniqueIdentifier];//手机别名: 用户定义的名称NSString userPhoneName = [[UIDevice currentDevice] name];//设备名称NSString deviceName = [[UIDevice currentDevice] systemName];//手机系统版本NSString phoneVersion = [[UIDevice currentDevice] systemVersion];//手机型号NSString phoneModel = [[UIDevice currentDevice] model];//地方型号 (国际化区域名称)NSString localPhoneModel = [[UIDevice currentDevice] localizedModel];NSDictionary *infoDictionary = [[NSBundle mainBundle] infoDictionary];// 当前应用名称NSString *appCurName = [infoDictionary objectForKey:@“CFBundleDisplayName”];// 当前应用版本 NSString *appCurVersion = [infoDictionary objectForKey:@“CFBundleShortVersionString”];// 当前应用版本号码 int类型NSString *appCurVersionNum = [infoDictionary objectForKey:@“CFBundleVersion”]; UIImage绘制圆角- (UIImage *)circleImage{ // NO代表透明 UIGraphicsBeginImageContextWithOptions(self.size, NO, 1); // 获得上下文 CGContextRef ctx = UIGraphicsGetCurrentContext(); // 添加一个圆 CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height); // 方形变圆形 CGContextAddEllipseInRect(ctx, rect); // 裁剪 CGContextClip(ctx); // 将图片画上去 [self drawInRect:rect]; UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return image;}准确获取图片像素CGFloat fixelW = CGImageGetWidth(image.CGImage);CGFloat fixelH = CGImageGetHeight(image.CGImage);JSON字符串转字典+ (NSDictionary *)parseJSONStringToNSDictionary:(NSString *)JSONString { NSData *JSONData = [JSONString dataUsingEncoding:NSUTF8StringEncoding]; NSDictionary *responseJSON = [NSJSONSerialization JSONObjectWithData:JSONData options:NSJSONReadingMutableLeaves error:nil]; return responseJSON;}获取当前控制器//获取当前屏幕显示的viewcontroller- (UIViewController *)getCurrentVC{ UIWindow *window = [[UIApplication sharedApplication].windows firstObject]; if (!window) { return nil; } UIView *tempView; for (UIView *subview in window.subviews) { if ([[subview.classForCoder description] isEqualToString:@“UILayoutContainerView”]) { tempView = subview; break; } } if (!tempView) { tempView = [window.subviews lastObject]; } id nextResponder = [tempView nextResponder]; while (![nextResponder isKindOfClass:[UIViewController class]] || [nextResponder isKindOfClass:[UINavigationController class]] || [nextResponder isKindOfClass:[UITabBarController class]]) { tempView = [tempView.subviews firstObject]; if (!tempView) { return nil; } nextResponder = [tempView nextResponder]; } return (UIViewController *)nextResponder;}KVO监听某个对象的属性// 添加监听者[self addObserver:self forKeyPath:property options:NSKeyValueObservingOptionNew context:nil];// 当监听的属性值变化的时候会来到这个方法- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([keyPath isEqualToString:@“property”]) { [self textViewTextChange]; } else { }}Reachability判断网络状态NetworkStatus status = [[Reachability reachabilityForInternetConnection] currentReachabilityStatus]; if (status == NotReachable) { NSLog(@“当前设备无网络”); } if (status == ReachableViaWiFi) { NSLog(@“当前wifi网络”); } if (status == ReachableViaWWAN) { NSLog(@“当前蜂窝移动网络”); } AFNetworking监听网络状态// 监听网络状况 AFNetworkReachabilityManager mgr = [AFNetworkReachabilityManager sharedManager]; [mgr setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) { switch (status) { case AFNetworkReachabilityStatusUnknown: break; case AFNetworkReachabilityStatusNotReachable: { NSLog(@“当前设备无网络”); } break; case AFNetworkReachabilityStatusReachableViaWiFi: NSLog(@“当前wifi网络”); break; case AFNetworkReachabilityStatusReachableViaWWAN: NSLog(@“当前蜂窝移动网络”); break; default: break; } }]; [mgr startMonitoring];父视图透明不影响子视图的做法self.view.backgroundColor = [[UIColor whiteColor]colorWithAlphaComponent:0.7f];取图片某一点的颜色 if (point.x < 0 || point.y < 0) return nil; CGImageRef imageRef = self.CGImage; NSUInteger width = CGImageGetWidth(imageRef); NSUInteger height = CGImageGetHeight(imageRef); if (point.x >= width || point.y >= height) return nil; unsigned char rawData = malloc(height * width * 4); if (!rawData) return nil; CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); NSUInteger bytesPerPixel = 4; NSUInteger bytesPerRow = bytesPerPixel * width; NSUInteger bitsPerComponent = 8; CGContextRef context = CGBitmapContextCreate(rawData, width, height, bitsPerComponent, bytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big); if (!context) { free(rawData); return nil; } CGColorSpaceRelease(colorSpace); CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); CGContextRelease(context); int byteIndex = (bytesPerRow * point.y) + point.x * bytesPerPixel; CGFloat red = (rawData[byteIndex] * 1.0) / 255.0; CGFloat green = (rawData[byteIndex + 1] * 1.0) / 255.0; CGFloat blue = (rawData[byteIndex + 2] * 1.0) / 255.0; CGFloat alpha = (rawData[byteIndex + 3] * 1.0) / 255.0; UIColor result = nil; result = [UIColor colorWithRed:red green:green blue:blue alpha:alpha]; free(rawData); return result;判断该图片是否有透明度通道 - (BOOL)hasAlphaChannel{ CGImageAlphaInfo alpha = CGImageGetAlphaInfo(self.CGImage); return (alpha == kCGImageAlphaFirst || alpha == kCGImageAlphaLast || alpha == kCGImageAlphaPremultipliedFirst || alpha == kCGImageAlphaPremultipliedLast);}两张图片合成+ (UIImage)mergeImage:(UIImage)firstImage withImage:(UIImage)secondImage { CGImageRef firstImageRef = firstImage.CGImage; CGFloat firstWidth = CGImageGetWidth(firstImageRef); CGFloat firstHeight = CGImageGetHeight(firstImageRef); CGImageRef secondImageRef = secondImage.CGImage; CGFloat secondWidth = CGImageGetWidth(secondImageRef); CGFloat secondHeight = CGImageGetHeight(secondImageRef); CGSize mergedSize = CGSizeMake(MAX(firstWidth, secondWidth), MAX(firstHeight, secondHeight)); UIGraphicsBeginImageContext(mergedSize); [firstImage drawInRect:CGRectMake(0, 0, firstWidth, firstHeight)]; [secondImage drawInRect:CGRectMake(0, 0, secondWidth, secondHeight)]; UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return image;}制作水印// 画水印- (void) setImage:(UIImage *)image withWaterMark:(UIImage *)mark inRect:(CGRect)rect{ if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 4.0) { UIGraphicsBeginImageContextWithOptions(self.frame.size, NO, 0.0); } //原图 [image drawInRect:self.bounds]; //水印图 [mark drawInRect:rect]; UIImage *newPic = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); self.image = newPic;}让label的文字内容显示在左上/右上/左下/右下/中心顶/中心底部自定义UILabel// 重写label的textRectForBounds方法- (CGRect)textRectForBounds:(CGRect)bounds limitedToNumberOfLines:(NSInteger)numberOfLines { CGRect rect = [super textRectForBounds:bounds limitedToNumberOfLines:numberOfLines]; switch (self.textAlignmentType) { case WZBTextAlignmentTypeLeftTop: { rect.origin = bounds.origin; } break; case WZBTextAlignmentTypeRightTop: { rect.origin = CGPointMake(CGRectGetMaxX(bounds) - rect.size.width, bounds.origin.y); } break; case WZBTextAlignmentTypeLeftBottom: { rect.origin = CGPointMake(bounds.origin.x, CGRectGetMaxY(bounds) - rect.size.height); } break; case WZBTextAlignmentTypeRightBottom: { rect.origin = CGPointMake(CGRectGetMaxX(bounds) - rect.size.width, CGRectGetMaxY(bounds) - rect.size.height); } break; case WZBTextAlignmentTypeTopCenter: { rect.origin = CGPointMake((CGRectGetWidth(bounds) - CGRectGetWidth(rect)) / 2, CGRectGetMaxY(bounds) - rect.origin.y); } break; case WZBTextAlignmentTypeBottomCenter: { rect.origin = CGPointMake((CGRectGetWidth(bounds) - CGRectGetWidth(rect)) / 2, CGRectGetMaxY(bounds) - CGRectGetMaxY(bounds) - rect.size.height); } break; case WZBTextAlignmentTypeLeft: { rect.origin = CGPointMake(0, rect.origin.y); } break; case WZBTextAlignmentTypeRight: { rect.origin = CGPointMake(rect.origin.x, 0); } break; case WZBTextAlignmentTypeCenter: { rect.origin = CGPointMake((CGRectGetWidth(bounds) - CGRectGetWidth(rect)) / 2, (CGRectGetHeight(bounds) - CGRectGetHeight(rect)) / 2); } break; default: break; } return rect;}- (void)drawTextInRect:(CGRect)rect { CGRect textRect = [self textRectForBounds:rect limitedToNumberOfLines:self.numberOfLines]; [super drawTextInRect:textRect];}移除字符串中的空格和换行+ (NSString *)removeSpaceAndNewline:(NSString *)str { NSString *temp = [str stringByReplacingOccurrencesOfString:@" " withString:@""]; temp = [temp stringByReplacingOccurrencesOfString:@"\r" withString:@""]; temp = [temp stringByReplacingOccurrencesOfString:@"\n" withString:@""]; return temp;}判断字符串中是否有空格+ (BOOL)isBlank:(NSString *)str { NSRange _range = [str rangeOfString:@" “]; if (_range.location != NSNotFound) { //有空格 return YES; } else { //没有空格 return NO; }}获取一个视频的第一帧图片NSURL *url = [NSURL URLWithString:filepath];AVURLAsset *asset1 = [[AVURLAsset alloc] initWithURL:url options:nil];AVAssetImageGenerator *generate1 = [[AVAssetImageGenerator alloc] initWithAsset:asset1];generate1.appliesPreferredTrackTransform = YES;NSError *err = NULL;CMTime time = CMTimeMake(1, 2);CGImageRef oneRef = [generate1 copyCGImageAtTime:time actualTime:NULL error:&err];UIImage *one = [[UIImage alloc] initWithCGImage:oneRef]; return one;获取视频的时长+ (NSInteger)getVideoTimeByUrlString:(NSString *)urlString { NSURL *videoUrl = [NSURL URLWithString:urlString]; AVURLAsset *avUrl = [AVURLAsset assetWithURL:videoUrl]; CMTime time = [avUrl duration]; int seconds = ceil(time.value/time.timescale); return seconds;}当tableView占不满一屏时,去除下边多余的单元格self.tableView.tableHeaderView = [UIView new];self.tableView.tableFooterView = [UIView new];isKindOfClass和isMemberOfClass的区别isKindOfClass可以判断某个对象是否属于某个类,或者这个类的子类。isMemberOfClass更加精准,它只能判断这个对象类型是否为这个类(不能判断子类)禁用系统滑动返回功能- (void)viewDidAppear:(BOOL)animated{ [super viewDidAppear:animated]; if ([self.navigationController respondsToSelector:@selector(interactivePopGestureRecognizer)]) {self.navigationController.interactivePopGestureRecognizer.delegate = self; }}- (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; if ([self.navigationController respondsToSelector:@selector(interactivePopGestureRecognizer)]) {self.navigationController.interactivePopGestureRecognizer.delegate = nil; }}- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{ return NO;}UILabel设置内边距子类化UILabel,重写drawTextInRect方法- (void)drawTextInRect:(CGRect)rect { // 边距,上左下右 UIEdgeInsets insets = {0, 5, 0, 5}; [super drawTextInRect:UIEdgeInsetsInsetRect(rect, insets)];}UILabel设置文字描边子类化UILabel,重写drawTextInRect方法- (void)drawTextInRect:(CGRect)rect{ CGContextRef c = UIGraphicsGetCurrentContext(); // 设置描边宽度 CGContextSetLineWidth(c, 1); CGContextSetLineJoin(c, kCGLineJoinRound); CGContextSetTextDrawingMode(c, kCGTextStroke); // 描边颜色 self.textColor = [UIColor redColor]; [super drawTextInRect:rect]; // 文本颜色 self.textColor = [UIColor yellowColor]; CGContextSetTextDrawingMode(c, kCGTextFill); [super drawTextInRect:rect];}UIView背景颜色渐变+ (CAGradientLayer *)setGradualChangingColor:(UIView *)view fromColor:(NSString *)fromHexColorStr toColor:(NSString *)toHexColorStr{ // CAGradientLayer类对其绘制渐变背景颜色、填充层的形状(包括圆角) CAGradientLayer *gradientLayer = [CAGradientLayer layer]; gradientLayer.frame = view.bounds; // 创建渐变色数组,需要转换为CGColor颜色 gradientLayer.colors = @[(__bridge id)[UIColor colorWithHexString:fromHexColorStr].CGColor,(__bridge id)[UIColor colorWithHexString:toHexColorStr].CGColor]; // 设置渐变颜色方向,左下点为(0,0), 右上点为(1,1) gradientLayer.startPoint = CGPointMake(0, 0.5); gradientLayer.endPoint = CGPointMake(1, 0.5); // 设置颜色变化点,取值范围 0.0~1.0 gradientLayer.locations = @[@0,@1]; return gradientLayer;}UIView某个角添加圆角// 左上角和右下角添加圆角UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:view.bounds byRoundingCorners:(UIRectCornerTopLeft | UIRectCornerBottomRight) cornerRadii:CGSizeMake(20, 20)];CAShapeLayer *maskLayer = [CAShapeLayer layer];maskLayer.frame = view.bounds;maskLayer.path = maskPath.CGPath;view.layer.mask = maskLayer;UIImage和base64互转// view分类方法- (NSString *)encodeToBase64String:(UIImage *)image { return [UIImagePNGRepresentation(image) base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength];}- (UIImage *)decodeBase64ToImage:(NSString *)strEncodeData { NSData *data = [[NSData alloc]initWithBase64EncodedString:strEncodeData options:NSDataBase64DecodingIgnoreUnknownCharacters]; return [UIImage imageWithData:data];}UIWebView设置背景透明[webView setBackgroundColor:[UIColor clearColor]];[webView setOpaque:NO];设置tableView分割线颜色以及顶到头// 分割线颜色[self.tableView setSeparatorColor:[UIColor myColor]];// 顶到头- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { [cell setSeparatorInset:UIEdgeInsetsZero]; [cell setLayoutMargins:UIEdgeInsetsZero]; cell.preservesSuperviewLayoutMargins = NO;}- (void)viewDidLayoutSubviews { [self.tableView setSeparatorInset:UIEdgeInsetsZero]; [self.tableView setLayoutMargins:UIEdgeInsetsZero];}为一个view添加虚线边框CAShapeLayer *border = [CAShapeLayer layer]; border.strokeColor = [UIColor colorWithRed:67/255.0f green:37/255.0f blue:83/255.0f alpha:1].CGColor; border.fillColor = nil; border.lineDashPattern = @[@4, @2]; border.path = [UIBezierPath bezierPathWithRect:view.bounds].CGPath; border.frame = view.bounds; [view.layer addSublayer:border];UITextView中打开或禁用复制,剪切,选择,全选等功能// 继承UITextView重写这个方法- (BOOL)canPerformAction:(SEL)action withSender:(id)sender{// 返回NO为禁用,YES为开启 // 粘贴 if (action == @selector(paste:)) return NO; // 剪切 if (action == @selector(cut:)) return NO; // 复制 if (action == @selector(copy:)) return NO; // 选择 if (action == @selector(select:)) return NO; // 选中全部 if (action == @selector(selectAll:)) return NO; // 删除 if (action == @selector(delete:)) return NO; // 分享 if (action == @selector(share)) return NO; return [super canPerformAction:action withSender:sender];} ...

January 17, 2019 · 10 min · jiezi

优秀开源库SDWebImage源码浅析

世人都说阅读源代码对于功力的提升是十分显著的, 但是很多的著名开源框架源代码动辄上万行, 复杂度实在太高, 这里只做基础的分析。简洁的接口首先来介绍一下这个 SDWebImage 这个著名开源框架吧, 这个开源框架的主要作用就是:Asynchronous image downloader with cache support with an UIImageView category.一个异步下载图片并且支持缓存的 UIImageView 分类.就这么直译过来相信各位也能理解, 框架中最最常用的方法其实就是这个:[self.imageView sd_setImageWithURL:[NSURL URLWithString:@“url”] placeholderImage:[UIImage imageNamed:@“placeholder.png”]]; 当然这个框架中还有 UIButton 的分类, 可以给 UIButton 异步加载图片, 不过这个并没有 UIImageView 分类中的这个方法常用.这个框架的设计还是极其的优雅和简洁, 主要的功能就是这么一行代码, 而其中复杂的实现细节全部隐藏在这行代码之后, 正应了那句话:把简洁留给别人, 把复杂留给自己.我们已经看到了这个框架简洁的接口, 接下来我们看一下 SDWebImage 是用什么样的方式优雅地实现异步加载图片和缓存的功能呢?复杂的实现其实复杂只是相对于简洁而言的, 并不是说 SDWebImage 的实现就很糟糕, 相反, 它的实现还是非常 amazing 的, 在这里我们会忽略很多的实现细节, 并不会对每一行源代码逐一解读.首先, 我们从一个很高的层次来看一下这个框架是如何组织的.UIImageView+WebCache 和 UIButton+WebCache 直接为表层的 UIKit 框架提供接口, 而 SDWebImageManger 负责处理和协调 SDWebImageDownloader 和 SDWebImageCache. 并与 UIKit 层进行交互, 而底层的一些类为更高层级的抽象提供支持.UIImageView+WebCache接下来我们就以 UIImageView+WebCache 中的- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder;这一方法为入口研究一下 SDWebImage 是怎样工作的. 我们打开上面这段方法的实现代码 UIImageView+WebCache.m当然你也可以 git clone git@github.com:rs/SDWebImage.git 到本地来查看.- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder { [self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:nil];}这段方法唯一的作用就是调用了另一个方法[self sd_setImageWithURL:placeholderImage:options:progress:completed:]在这个文件中, 你会看到很多的 sd_setImageWithURL…… 方法, 它们最终都会调用上面这个方法, 只是根据需要传入不同的参数, 这在很多的开源项目中乃至我们平时写的项目中都是很常见的. 而这个方法也是 UIImageView+WebCache 中的核心方法.这里就不再复制出这个方法的全部实现了.操作的管理这是这个方法的第一行代码:// UIImageView+WebCache// sd_setImageWithURL:placeholderImage:options:progress:completed: #1[self sd_cancelCurrentImageLoad];这行看似简单的代码最开始是被我忽略的, 我后来才发现蕴藏在这行代码之后的思想, 也就是 SDWebImage 管理操作的办法.框架中的所有操作实际上都是通过一个 operationDictionary 来管理, 而这个字典实际上是动态的添加到 UIView 上的一个属性, 至于为什么添加到 UIView 上, 主要是因为这个 operationDictionary 需要在 UIButton 和 UIImageView 上重用, 所以需要添加到它们的根类上.这行代码是要保证没有当前正在进行的异步下载操作, 不会与即将进行的操作发生冲突, 它会调用:// UIImageView+WebCache// sd_cancelCurrentImageLoad #1[self sd_cancelImageLoadOperationWithKey:@“UIImageViewImageLoad”]而这个方法会使当前 UIImageView 中的所有操作都被 cancel. 不会影响之后进行的下载操作.占位图的实现// UIImageView+WebCache// sd_setImageWithURL:placeholderImage:options:progress:completed: #4if (!(options & SDWebImageDelayPlaceholder)) { self.image = placeholder;}如果传入的 options 中没有 SDWebImageDelayPlaceholder(默认情况下 options == 0), 那么就会为 UIImageView 添加一个临时的 image, 也就是占位图.获取图片// UIImageView+WebCache// sd_setImageWithURL:placeholderImage:options:progress:completed: #8if (url)接下来会检测传入的 url 是否非空, 如果非空那么一个全局的 SDWebImageManager 就会调用以下的方法获取图片:[SDWebImageManager.sharedManager downloadImageWithURL:options:progress:completed:]下载完成后会调用 (SDWebImageCompletionWithFinishedBlock)completedBlock 为 UIImageView.image 赋值, 添加上最终所需要的图片.// UIImageView+WebCache// sd_setImageWithURL:placeholderImage:options:progress:completed: #10dispatch_main_sync_safe(^{ if (!wself) return; if (image) { wself.image = image; [wself setNeedsLayout]; } else { if ((options & SDWebImageDelayPlaceholder)) { wself.image = placeholder; [wself setNeedsLayout]; } } if (completedBlock && finished) { completedBlock(image, error, cacheType, url); }});dispatch_main_sync_safe 宏定义上述代码中的 dispatch_main_sync_safe 是一个宏定义, 点进去一看发现宏是这样定义的#define dispatch_main_sync_safe(block)\ if ([NSThread isMainThread]) {\ block();\ } else {\ dispatch_sync(dispatch_get_main_queue(), block);\ }相信这个宏的名字已经讲他的作用解释的很清楚了: 因为图像的绘制只能在主线程完成, 所以, dispatch_main_sync_safe 就是为了保证 block 能在主线程中执行.而最后, 在 [SDWebImageManager.sharedManager downloadImageWithURL:options:progress:completed:] 返回 operation 的同时, 也会向 operationDictionary 中添加一个键值对, 来表示操作的正在进行:// UIImageView+WebCache// sd_setImageWithURL:placeholderImage:options:progress:completed: #28[self sd_setImageLoadOperation:operation forKey:@“UIImageViewImageLoad”];它将 opertion 存储到 operationDictionary 中方便以后的 cancel.到此为止我们已经对 SDWebImage 框架中的这一方法分析完了, 接下来我们将要分析 SDWebImageManager 中的方法[SDWebImageManager.sharedManager downloadImageWithURL:options:progress:completed:]SDWebImageManager在 SDWebImageManager.h 中你可以看到关于 SDWebImageManager 的描述:The SDWebImageManager is the class behind the UIImageView+WebCache category and likes. It ties the asynchronous downloader (SDWebImageDownloader) with the image cache store (SDImageCache). You can use this class directly to benefit from web image downloading with caching in another context than a UIView.这个类就是隐藏在 UIImageView+WebCache 背后, 用于处理异步下载和图片缓存的类, 当然你也可以直接使用 SDWebImageManager 的上述方法 downloadImageWithURL:options:progress:completed: 来直接下载图片.可以看到, 这个类的主要作用就是为 UIImageView+WebCache 和 SDWebImageDownloader, SDImageCache 之间构建一个桥梁, 使它们能够更好的协同工作, 我们在这里分析这个核心方法的源代码, 它是如何协调异步下载和图片缓存的.// SDWebImageManager// downloadImageWithURL:options:progress:completed: #6if ([url isKindOfClass:NSString.class]) { url = [NSURL URLWithString:(NSString *)url];}if (![url isKindOfClass:NSURL.class]) { url = nil;}这块代码的功能是确定 url 是否被正确传入, 如果传入参数的是 NSString 类型就会被转换为 NSURL. 如果转换失败, 那么 url 会被赋值为空, 这个下载的操作就会出错.SDWebImageCombinedOperation当 url 被正确传入之后, 会实例一个非常奇怪的 “operation”, 它其实是一个遵循 SDWebImageOperation 协议的 NSObject 的子类. 而这个协议也非常的简单:@protocol SDWebImageOperation <NSObject>- (void)cancel;@end这里仅仅是将这个 SDWebImageOperation 类包装成一个看着像 NSOperation 其实并不是 NSOperation 的类, 而这个类唯一与 NSOperation 的相同之处就是它们都可以响应 cancel 方法. (不知道这句看似像绕口令的话, 你看懂没有, 如果没看懂..请多读几遍).而调用这个类的存在实际是为了使代码更加的简洁, 因为调用这个类的 cancel 方法, 会使得它持有的两个 operation 都被 cancel.// SDWebImageCombinedOperation// cancel #1- (void)cancel { self.cancelled = YES; if (self.cacheOperation) { [self.cacheOperation cancel]; self.cacheOperation = nil; } if (self.cancelBlock) { self.cancelBlock(); _cancelBlock = nil; }}而这个类, 应该是为了实现更简洁的 cancel 操作而设计出来的.既然我们获取了 url, 再通过 url 获取对应的 keyNSString *key = [self cacheKeyForURL:url];下一步是使用 key 在缓存中查找以前是否下载过相同的图片.operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) { … }]; 这里调用 SDImageCache 的实例方法 queryDiskCacheForKey:done: 来尝试在缓存中获取图片的数据. 而这个方法返回的就是货真价实的 NSOperation.如果我们在缓存中查找到了对应的图片, 那么我们直接调用 completedBlock 回调块结束这一次的图片下载操作.// SDWebImageManager// downloadImageWithURL:options:progress:completed: #47dispatch_main_sync_safe(^{ completedBlock(image, nil, cacheType, YES, url);});如果我们没有找到图片, 那么就会调用 SDWebImageDownloader 的实例方法:id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) { … }]; 如果这个方法返回了正确的 downloadedImage, 那么我们就会在全局的缓存中存储这个图片的数据:[self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk]; 并调用 completedBlock 对 UIImageView 或者 UIButton 添加图片, 或者进行其它的操作.最后, 我们将这个 subOperation 的 cancel 操作添加到 operation.cancelBlock 中. 方便操作的取消.operation.cancelBlock = ^{ [subOperation cancel]; }SDWebImageCacheSDWebImageCache.h 这个类在源代码中有这样的注释:SDImageCache maintains a memory cache and an optional disk cache.它维护了一个内存缓存和一个可选的磁盘缓存, 我们先来看一下在上一阶段中没有解读的两个方法, 首先是:- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;这个方法的主要功能是异步的查询图片缓存. 因为图片的缓存可能在两个地方, 而该方法首先会在内存中查找是否有图片的缓存.// SDWebImageCache// queryDiskCacheForKey:done: #9UIImage *image = [self imageFromMemoryCacheForKey:key];这个 imageFromMemoryCacheForKey 方法会在 SDWebImageCache 维护的缓存 memCache 中查找是否有对应的数据, 而 memCache 就是一个 NSCache.如果在内存中并没有找到图片的缓存的话, 就需要在磁盘中寻找了, 这个就比较麻烦了..在这里会调用一个方法 diskImageForKey 这个方法的具体实现我在这里就不介绍了, 涉及到很多底层 Core Foundation 框架的知识, 不过这里文件名字的存储使用 MD5 处理过后的文件名.// SDImageCache// cachedFileNameForKey: #6CC_MD5(str, (CC_LONG)strlen(str), r);对于其它的实现细节也就不多说了…如果在磁盘中查找到对应的图片, 我们会将它复制到内存中, 以便下次的使用.// SDImageCache// queryDiskCacheForKey:done: #24UIImage *diskImage = [self diskImageForKey:key];if (diskImage) { CGFloat cost = diskImage.size.height * diskImage.size.width * diskImage.scale; [self.memCache setObject:diskImage forKey:key cost:cost];}这些就是 SDImageCache 的核心内容了, 而接下来将介绍如果缓存没有命中, 图片是如何被下载的.SDWebImageDownloader按照之前的惯例, 我们先来看一下 SDWebImageDownloader.h 中对这个类的描述.Asynchronous downloader dedicated and optimized for image loading.专用的并且优化的图片异步下载器.这个类的核心功能就是下载图片, 而核心方法就是上面提到的:- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock;回调这个方法直接调用了另一个关键的方法:- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock andCompletedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback它为这个下载的操作添加回调的块, 在下载进行时, 或者在下载结束时执行一些操作, 先来阅读一下这个方法的源代码:// SDWebImageDownloader// addProgressCallback:andCompletedBlock:forURL:createCallback: #10BOOL first = NO;if (!self.URLCallbacks[url]) { self.URLCallbacks[url] = [NSMutableArray new]; first = YES;}// Handle single download of simultaneous download request for the same URLNSMutableArray *callbacksForURL = self.URLCallbacks[url];NSMutableDictionary *callbacks = [NSMutableDictionary new];if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];[callbacksForURL addObject:callbacks];self.URLCallbacks[url] = callbacksForURL;if (first) { createCallback();}方法会先查看这个 url 是否有对应的 callback, 使用的是 downloader 持有的一个字典 URLCallbacks.如果是第一次添加回调的话, 就会执行 first = YES, 这个赋值非常的关键, 因为 first 不为 YES 那么 HTTP 请求就不会被初始化, 图片也无法被获取.然后, 在这个方法中会重新修正在 URLCallbacks 中存储的回调块.NSMutableArray *callbacksForURL = self.URLCallbacks[url];NSMutableDictionary *callbacks = [NSMutableDictionary new];if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];[callbacksForURL addObject:callbacks];self.URLCallbacks[url] = callbacksForURL;如果是第一次添加回调块, 那么就会直接运行这个 createCallback 这个 block, 而这个 block, 就是我们在前一个方法 downloadImageWithURL:options:progress:completed: 中传入的回调块.// SDWebImageDownloader// downloadImageWithURL:options:progress:completed: #4[self addProgressCallback:progressBlock andCompletedBlock:completedBlock forURL:url createCallback:^{ … }];我们下面来分析这个传入的无参数的代码. 首先这段代码初始化了一个 NSMutableURLRequest:// SDWebImageDownloader// downloadImageWithURL:options:progress:completed: #11NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:… timeoutInterval:timeoutInterval];这个 request 就用于在之后发送 HTTP 请求.在初始化了这个 request 之后, 又初始化了一个 SDWebImageDownloaderOperation 的实例, 这个实例, 就是用于请求网络资源的操作. 它是一个 NSOperation 的子类,// SDWebImageDownloader// downloadImageWithURL:options:progress:completed: #20operation = [[SDWebImageDownloaderOperation alloc] initWithRequest:request options:options progress:… completed:… cancelled:…}]; 但是在初始化之后, 这个操作并不会开始(NSOperation 实例,只有在调用 start 方法或者加入 NSOperationQueue 才会执行), 我们需要将这个操作加入到一个 NSOperationQueue 中.// SDWebImageDownloader// downloadImageWithURL:option:progress:completed: #59[wself.downloadQueue addOperation:operation];只有将它加入到这个下载队列中, 这个操作才会执行.SDWebImageDownloaderOperation这个类就是处理 HTTP 请求, URL 连接的类, 当这个类的实例被加入队列之后, start 方法就会被调用, 而 start 方法首先就会产生一个 NSURLConnection.// SDWebImageDownloaderOperation// start #1@synchronized (self) { if (self.isCancelled) { self.finished = YES; [self reset]; return; } self.executing = YES; self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO]; self.thread = [NSThread currentThread];}而接下来这个 connection 就会开始运行:// SDWebImageDownloaderOperation// start #29[self.connection start];它会发出一个 SDWebImageDownloadStartNotification 通知// SDWebImageDownloaderOperation// start #35[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];代理在 start 方法调用之后, 就是 NSURLConnectionDataDelegate中代理方法的调用.- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection;在这三个代理方法中的前两个会不停回调 progressBlock 来提示下载的进度.而最后一个代理方法会在图片下载完成之后调用 completionBlock 来完成最后 UIImageView.image 的更新.而这里调用的 progressBlock completionBlock cancelBlock 都是在之前存储在 URLCallbacks 字典中的.到目前为止, 我们就基本解析了 SDWebImage 中[self.imageView sd_setImageWithURL:[NSURL URLWithString:@“url”] placeholderImage:[UIImage imageNamed:@“placeholder.png”]];这个方法执行的全部过程了.总结SDWebImage 的图片加载过程其实很符合我们的直觉:查看缓存缓存命中 * 返回图片更新 UIImageView缓存未命中 * 异步下载图片加入缓存更新 UIImageView ...

January 17, 2019 · 5 min · jiezi

探索新零售时代背后的技术变革

随着线下场景布局的不断发展,以及线上技术的持续推进,一个真正属于新零售的时代已经来临。走完了广州、成都、北京、深圳等四大城市后,个推技术沙龙TechDay于上海完美收官。来自京东到家、个推、亿咖通、Pinlan的技术大咖们,在上海站的现场,为大家解析并展示了新零售时代下的新技术。《大数据时代,用户画像实践与应用》叶政君 个推大数据分析师用户画像,即用户信息的标签化,本质上来说,用户画像是数据的标签化。常见的用户画像体系有三种:结构化体系、非结构化体系和半结构化体系。非结构化体系没有明显的层级,较为独立。半结构化层次有一定的层级概念,但是没有过于严格的依赖关系。在电商行业中,较多的企业会选择半结构化的用户画像体系进行应用。以一个简单的三级结构化标签为例,一级标签有基本属性和兴趣偏好,并且由此可以延伸至二级标签和三级标签,具体到哪些属性、兴趣。在画像建设方面,开发者们可以参考一些通用的做法,如标签体系设计、基础数据收集和多数据源数据融合、实现用户统一标识、构建用户画像特征层、画像标签规则+算法建模、对所有用户进行算法打标签和画像质量监控等。个推用户画像构建的整体流程,可以分为三个部分,第一,基础数据处理。基础数据包括设备信息、线上APP偏好数据和线下场景数据。第二,画像中间数据处理。处理结果包括线上APP偏好特征和线下场景特征等。第三,画像信息表。表中应有四种信息:设备基础属性、用户基础画像、用户兴趣画像和用户其它画像。同时,用户画像的构建需要技术和业务人员的共同参与,避免形式化的用户画像。在进行用户画像构建的过程中,个推主要运用到的技术有数据存储、实时计算、机器学习和深度学习。而用户画像的应用则包括:精准营销、用户分析、数据挖掘、服务产品、行业报告以及用户研究。针对新零售时代下,APP的运营者对于用户画像的需求,个推依托多年推送服务的积累和强大的大数据能力,推出了用户画像产品“个像”,为APP开发者提供丰富的用户画像数据以及实时的场景识别能力。同时,个推独有的冷、热、温数据标签,也可以有效分析用户的线上线下行为,挖掘用户特征,助力APP运营者筛选目标人群。同时,个推还可以为APP提供定制化标签,满足APP运营者在用户数字化管理方面的需求。在实践中,定制化标签的整合也有一定的难度,个推会结合双方的数据,对其进行建模分析,输出定制化标签。总的来说,个推不仅有通用的标签维度,也有定制化标签的输出能力。举两个用户画像在个推业务中的典型应用:第一,精准推荐,APP的运营者可以通过个像提供的性别、年龄层次、兴趣爱好等标签,分别展示不同的内容给用户,以达到精准化运营,千人千面。第二,用户聚类,处理客户提供的用户数据,补全用户画像,最终进行用户聚类分析。《即时物流场景下的机器学习实践》庄学坤 达达-京东到家 物流算法团队Leader即时物流作为新零售的“水电煤”,在新零售模式中处于基础核心环节,解决的是商品的配送效率问题。达达-京东到家作为国内即时物流的领先平台,在这方面进行了大量的技术探索与积累。与传统物流模式相比,即时物流场景下的配送具有更高的复杂度,具体表现为以下四点:1. 订单类型多样化;2. 时效性要求更高;3. 配送骑士的运力难以掌控;4.送达目的地复杂多样。而即时物流形式中存在的问题和挑战,也可以总结为四个部分:高度动态的物流订单、配送成本的动态性、订单派发需要兼顾公平与效率、骑士自由抢单的管理。现如今,新的算法模型层出不穷,算法可以选择的自由度较高。但是在实践中,数据决定了机器学习的上限,而算法只是尽可能逼近这个上限。一个成功而实用的算法体系,必须非常重视特征工程。开发者研发一套优秀算法体系的前提,是获取到优质的、具有精确特征的数据。达达结合自身的配送场景,积累了海量而精确的配送场景特征集合。没有最好的算法,只有某种场景下最合适的算法。在获取到特征数据之后,即时物流场景所应用的机器学习体系可以分为四层:基础数据层、特征工程层、算法模型层、业务应用层。开发团队还需要根据业务的应用场景,对不同的算法模型进行技术选型,比如线性模型擅长处理高维微观特征,非线性模型则擅长处理较低维的宏观特征,而在路径规划与调度当中,传统的运筹学模型动态规划等可能更加合适。最后,更加通用的AI可以由两部分组成,第一部分是深度学习(DeepLearning),解决端对端的学习问题;第二部分称之为强化学习(Reinforcement Learning),允许更加通用的学习架构。如果这两个部分结合到一起,就可以变为一个非常通用的学习算法。达达智能供需调控系统的设计中,借鉴了AlphaGo的思路,充分发挥了这种模式的优势,使得调控效率的效果和自动化程度同时获得了大幅提升。《使用智能对话机器人增强新零售服务链》孔晓泉 吉利集团Ecarx算法专家与以往的零售方式不同的地方在于,新零售的过程中,没有商超反馈和中间链条,企业需要直达顾客。这会使得一个to C企业,在客服和相关支持等方面,花费更多的成本,并且承担很大的压力。而使用智能对话机器人提供新零售的服务链,则可以减少用户的等待时间,提高用户体验,并且极大地减低公司的客服成本。智能对话机器人应用最多的领域是在线客服,其次是智能问答,如智能医疗等。从技术角度来说,人机对话的流程是:语音识别(ASR)、基于文本的方式进行自然语言理解(NLU)、通过理解到的意图或实体进行对话管理(DM)、自然语言生成(NLG)和语音合成(TTS)。企业可以选择Rasa Stack作为构建智能对话机器人的基础,它是一款开源的、基于机器学习的、为开发者和公司设计的机器人,智能性较高。由于对话机器人的软件开发难度很高,自然语言的理解需要很多组件的配合,而Rasa Stack的优势是完全的数据控制、自行扩充、自定义模型和完全的自驱动,并且其背靠德国的Rasa Technologies GmbH,有一定的发展保障。Rasa NLU能够提取用户的意图和相关的实体,这相当于把用户千奇百怪的、非结构化的、长短不一的数据转化成结构化数据。Rasa NLU的特色是基于 pipeline 的工作模式,扩展能力强,并且支持多种语言,如英语、德语、中文、日文等,RASA NLU还内置多种算法和配置,如MITIE、CRF、Embedding等。RASA Core则是一个对话管理体系,如下图所示,图中的每一个箭头都代表数据的流动。Rasa Core的特性是数据驱动、扩展能力强、支持多种Policy协同工作、内置多种算法和配置,并且支持Interactive learning.实际上,强化学习不仅是一种框架,它还提供了算法和配置,但是具体的做法和参数调节,还需要在实践场景下进行确定。另外,交互式学习能够很快地测试到,用户所得到的回复是否正确,并在错误的情况下,进行相应的更改。《AI赋能营销数字转化转型》李泽洲 Pinlan联合创始人兼CTO当前,线下营销正在从以零售商为中心,转化成以购物者为中心的形式。而在这其中,机器视觉的落地,也对整个零售行业的转变起到了很大的推动作用。计算机视觉是一个跨学科的领域,涉及到如何使计算机从数字图像或视频中,获得高层次的理解。硬件和算法的进步均催生了大量计算机视觉的应用落地。在深度学习进入到计算机视觉的领域之前,计算机视觉技术主要被应用于图像处理、特征检测和匹配以及运动估计。随着深度学习网络的发展,传统的神经网络很难被单纯地应用到计算机视觉领域。图像的纬度很大,而人的观察方式是对图像当中的某个局部信息,进行认真的观察之后,才会逐渐地观察到全局信息。机器学习的流程是数据采集、数据预处理、模型训练、模型测试和模型服务。其中,零售行业的零售商更关心的是,SKU在超市中铺货时,是如何摆放的。线下零售商有两种方式可以进行数据采集。方式一是利用手持终端(SFA)采集数据; 方式二是在超市中架设摄像头,进行固定场景的拍摄。方式二相较于方式一,有一定的优势存在,如可选择高像素摄像头,图像质量高;固定场景下的拍摄,变化较小;数据可用性高;模型可以确保细粒度商品的识别。在数据预处理阶段,也有两种方式可以对已采集的图像数据信息进行处理,方式一是提高图像质量,如调整亮度、对比度,对图像进行去模糊、超分辨率重建等。方式二是训练图像增强,在AI的实现过程中,在训练CNN网络之前,对数据进行增强是一个非常重要的环节。一般情况下,现实场景中所能收集到的数据量不是很大,这对于深度学习来说是一个致命的问题,这时便可以利用图像增强或者图像数据的扩充,增加数据量,如图像裁剪、图像对比度变化、图像亮度变化和图像微旋转。在商品检测模型训练的阶段,目前前沿的算法框架包括 Faster-RCNN、RetinaNet和YOLO等。通用商品检测模型能够支持海量的多种包装类别实际场景数据,可以针对大小目标和不同包装类别,进行大类拆分,并且能够优化模型结构,增强场景适应性。同时,通用商品检测模型可以实现移动端压缩,支持移动端检测。而细粒度商品识别模型训练则需要先收集海量SKU数据,建立商品数据库,之后结合注意力机制,训练细粒度识别模型,然后在真实场景验证模型效果。在实践中,Pinlan的细粒度商品识别模型,已经能够使自然场景商品识别准确率提升至97%.建立检测模型和识别模型之后,开发者可以将两者进行结合,进行线下零售的智能陈列分析,如陈列位置检查、数量检查、陈列规范检查和陈列推荐。以数据驱动的新零售时代已经来临,面临零售场所和消费观念的转变,传统零售需要整合和重组,充分地利用电子商务、大数据云平台、移动互联网和人工智能等技术,让线上线下一体化成为可能。

January 17, 2019 · 1 min · jiezi

外墙清洗为什么要用机器人

上周文章“人工高空作业风险防不胜防”中,列举了很多发生在我们真实生活中的死亡事故,相信大家都感到触目惊心,不由自主的对“蜘蛛人”们抱以很大的同情。当然小编在文章最后也提到了一款“高楼外墙清洗机器人”,那么它将会成为“蜘蛛人”们的福音吗?今天,就让小编带领大家一起来分析一下吧,这款机器人到底有哪些优点呢?为什么外墙清洗要用机器人呢?与传统人工清洗相比,历途机器人有五大明显的优势:1、清洁效果相信大家都知道“蜘蛛人”清洗高楼的时候,用的都是抹布、清洗剂、毛头、刮水刀、铲刀等几样清洁工具,其实和普通的清洁工具没有太大的区别,而且“蜘蛛人”需要被吊到几十米的高空进行作业,所以清洁力度和清洁速度相对来说就会大打折扣,清洁效果也是因人而已参差不齐。而历途机器人则利用负压吸附和四旋翼的技术给了清洁面一定的推力,使得清洁力度相比人工来说更加持久和稳定。同时历途机器人内置滚刷,当机器人开始工作时,滚刷高速运转,对清洁面高速刷洗,即使用清水都可以轻松洗掉墙面的污垢。2、清洁速度根据市场调查,人工清洗高楼外墙的速度大概是每天300-500平方米,当然这个速度是建立在天气情况良好的状态下,如果遇到大风或者雨雪雾等天气的时候,人工清洗是需要停止作业的。而且人工清洗受限于夏季的高温和冬季的严寒,使得外墙清洗具有了淡季和旺季的特点,一年能清洗的时长也就几个月。历途机器人则不受天气情况的影响,无论是大风、高温还是能见度比较低的天气下,机器人都可以正常运行,可以有效延长旺季的时间。而且机器人是上下清洗,相比人工的单向清洗来说,可以不间断的进行工作,使得机器人的清洁速度可以达到人工清洗速度的2-3倍。3、节水并避免二次污染人工清洗属于开放式清洗,用高压水枪冲刷墙面会浪费很多水,而且在清洗过程中,清洗剂会顺着清洁面流到地面或者绿化带上,给周围的卫生以及环境带来一定的破坏。历途机器人有两个可拆卸水箱,内置过滤循环装置,5L水就可以清洗100m²的外立面,耗水量仅是人工清洗的百分之一。而且在机器人清洁过程中,也不会有多余的水渍往下流,避免了对环境的二次污染。4、安全安全这个问题相信不用小编多说大家也都心知肚明,现如今“蜘蛛人”伤亡事故屡屡发生,如果由一台机器人代替人工,那么发生伤亡事故的频率毫无疑问便会减少。更何况历途机器人采用了双安全绳保障措施,按照人工的安全标准来吊装机器人,这样的设计让使用机器人的客户也更加放心。5、清洗成本因为越来越多的年轻人不愿意从事这么高危的工作,使得这个行业的从业者年龄偏大,而且清洁市场需求又是日益增长的,使得近几年已经开始出现旺季用工荒,导致清洁成本也是年年增长。假设有一栋40000㎡的大楼需要清洗,物业公司找保洁公司需要支付3元/㎡左右的费用,那么清洗一次就需要12万左右的费用,如果一年洗两次就需要20几万。但是如果使用历途机器人进行外墙清洗,只需要物业工程部的几个普通工人就可以轻松完成清洗工作,清洁过程中的耗材费用相比人工清洗也是微不足道,购买一台机器人的成本仅是人工清洗2-3次的费用,这么算下来是不是超划算?随着我国经济的不断进步与发展,我国的环境问题越来越凸显,大气中的有害气体和油烟等污染和化学反应的侵蚀都使我们不得不对楼宇进行多次的清洗保护。在人工智能高速发展的今天,用机器人代替人工进行高危、重复且繁重的工作,已经成为当下必然的趋势。它不仅可以有效的释放劳动力,而且工作水平、效率相比人类更高、更安全,我们又有什么理由拒绝它呢?北京历途科技有限公司是一家专注于人工智能与机器人研发的高新技术企业,经过十几年的技术积累,现已自主研发出专业、高效、安全的高楼外墙清洗机器人,填补了国际外墙清洁市场智能化产品空白。公司以"人人皆创客,完美做产品"为发展理念,打造具有颠覆性的科技产品。

January 17, 2019 · 1 min · jiezi

让看不见的AI算法,助你拿下看得见的广阔市场

人工智能技术的飞速发展给各行各业都带来了深远的影响,AI已被视为企业提升运营效能、应对市场竞争的必经之路。然而对于一些企业而言,让AI真正实现落地和应用,并且创造价值,仍是一件需要努力的事情。近日,在个推技术沙龙TechDay深圳站,来自华为、个推、SheIn的技术大拿们在现场,对AI核心技术进行了深入的探讨。常越峰 《浅谈AI工具链》个推大数据研发高级主管AI在生产环境落地的整个过程中,通常会遇到三个挑战:第一,业务场景复杂。简单的一个算法也许只能优化某个环节,但整个业务场景的优化可能需要许多算法的相互配合。第二,数据问题。数据是AI的重要支撑之一,许多企业都欠缺获取高质量、有标注数据的能力。第三,技术问题。在AI落地的过程所遇到的技术问题,有四个核心:1)CPU / GPU环境的调度和管理复杂。2)AI业务的开发人员们需要一个低门槛的实验平台,使其能够进行快速的探索实验。3)拥有大规模数据的企业,需要工业级大规模分布式训练,来保证算法能够应用于全量数据中。4)企业需要提供低延迟的在线服务。人工智能最核心的是数据,而数据可以分为两个部分,实时数据和离线数据。个推使用Hive方案进行离线数据的存储,注重数据的容量和扩展性;而在线用户对延时非常在意,所以个推会使用高性能KV库,保证在线特征能够及时地被访问到。在解决了基础的数据存储和使用问题之后,对于AI落地过程中的技术问题,个推内部支持端到端的服务,能够使用标准化流程快速进行实践探索。个推也自研了一些插件和产品包,简化流程步骤和复杂度,帮助经验较少的开发者也可以在较短的时间内搭建系统。最后,个推还支持了部署发布的工具,让训练的成果能够通过标准化的方式导出到线上,进行服务部署,真正地在线上产生价值。在小微企业AI落地实践的过程中,可以使用Kubeflow等开源技术栈。首先,环境的管理与调度可以使用Kubernates作为分布式环境标准;Jupyter +开源数据分析工具包+ AI框架可以进行低门槛的快速探索实验;Kubeflow + Tensorflow / PyTorch / MXNet可以快速地部署大规模的分布式训练;最后,借助Kubernates提供的快速部署、上线、扩缩容的能力,可以提供高可用的在线服务。而在AI实际落地时,企业则需要注意以下三点:第一,快与高效。企业可以借助开源工具快速落地业务,同时也要注意沉淀流程和垂直领域。第二,集成打通。Kubernates方案并不是唯一的选择,企业需要考虑自身情况,与已有系统进行对接,选择适合自身的方案。第三,团队建设。各个技术部门之间需要进行高效的配合,企业也可以引导研发工程师逐渐地融入AI领域。马兴国 《个性化推荐闲聊》SheIn 产品研发中心 副总经理对于企业来说,如果想要做好AI个性化产品的业务,只有算法工程师是不够的,还需要工程、数据分析人员的支持,以及产品、运营人员的助力。当企业涉及到的业务较多时,也可以将业务进行通用处理,即建设偏向系统层面的推荐平台。该推荐平台需要数据、算法和系统的共同配合。推荐平台的接入,可以带来三点功能:第一,企业在进行物料同步时,可以做到格式统一,并且同步增量和全量;第二,平台在处理用户的服务请求时,可以做到标准化、高性能和智能化;第三,平台可以格式统一、实时、离线地上报用户行为。简单的机器学习过程是搭建环境、收集数据、分析数据、准备数据、训练算法、测试算法和使用算法。在这个过程中也隐藏着许多问题,比如如何解决冷启动问题,如何解决假曝光问题,如何清洗异常数据,如何选择正负样本,如何解决数据稀疏问题,如何从亿级特征中选择显著特征等。在机器学习的过程中,数据是基础,理想的状态是数据的数量大且特征完备。收集数据有“推”和“拉”两种方式,“拉”即是爬虫,“推”就是上报。而分析数据则是分析目标分布、特征分布、目标特征关系、特征间的关系和完整性等。分析数据的方式有离线分析、实时分析和融合分析,分析工具则可以在Excle、Shell(awk)、Python、Mysql、Hadoop、Spark、Matlab…当中进行选择。清洗数据需要清洗系统脏数据、业务脏数据和目标外数据。格式化数据则需要进行数据变换、采样和稀疏处理。而机器学习可以选择的算法模型较多,如热度、贝叶斯、关联规则、LR、GBDT、AR、CF(ALS)等等。在算法模型中,特征工程也是非常重要的一部分。其中,特征对象有物料、用户和上下文;特征类型有静态特征、动态特征、表征特征、枚举特征、实数特征等;特征维度则有一阶独立特征、二阶交叉特征和多阶交叉特征。特征的选择也是一件需要注意的事情,企业可以在过滤型、包裹型和嵌入型三种特征进行选择,同时,企业还需要在前向、后向和StepWise三种特征过程类型中进行选择。算法的最后还需要进行效果评估、多维度评估、实时评估和离线评估。企业还需要注意到,没有一劳永逸的模型,算法需要进行持续的关注和运营。合适环境的搭建也是算法能够正常运行的保障之一。算法的环境需要标准化、配置化、可扩展、高性能,同时支持立体监控和效果提升,保证用户体验。聂鹏鹤 《AI识别,从图像到人脸》华为算法工程师在计算机领域,上世纪90年代就有人尝试,将图像的特征和识别的过程,通过人类的规则同步给计算机,让计算机进行“图像识别”。一直到了2012、13年,人们发现,对传统神经网络的结构方式做一些小的变化,能够大幅度地提升计算机进行图像识别的可操作性,这个改善后的神经网络被称为卷积神经网络(CNN)。CNN进行图像处理的本质是信息提取,也被称为自动的特征工程,即通过巨大的神经网络一步步地抽取到关键的图像特征,从而达到图像识别的目的。而人脸识别则是一种基于人的脸部特征信息,进行身份识别的生物识别技术。现如今,人脸识别已经可以有效地对用户身份进行识别,并且被广泛地应用于支付、安检、考勤等场景。而随着人脸数据系统的建设,人脸识别也将成为反欺诈、风控等的有效手段之一,能够极大地缩短身份审核的确认时间。人脸识别最大的优点是非接触性,可以隐蔽操作,这使得它能够适用于安全问题、罪犯监控与抓逃应用。同时,非接触性的信息采集没有侵犯性,容易被大众接受。而人脸识别方便、快捷、强大的事后追踪能力,也符合人类的识别习惯。人脸识别的不足之处,在于不同人脸的相似性小,同时识别性能受外界条件的影响大。人脸识别的步骤主要包括人脸检测、人脸对齐校准、人脸特征提取、人脸特征模型建立、人脸特征匹配以及人脸识别结果的输出。其中,人脸检测的目标是找出图像中,人脸所对应的位置,算法输出的则是人脸外接矩形在图像中的坐标,可能还包括姿态,如倾斜角度等信息。人脸识别的第二步是人脸对齐,它需要在保证人脸的特征等要素没有发生扭曲和变化的前提下进行使用,在这样的情况下,输出的人脸距离才能与后期的模型进行有效对比。人脸识别的最后一步是人脸匹配,在网络足够大,样本足够丰富的情况下,人脸匹配的准确率会非常高。在人脸识别的领域,深度学习网络的发展会越来越好。深度学习有其相应的优势,它强调了数据的抽象和特征的自动学习,并且它的自主学习特征更为可靠。

January 17, 2019 · 1 min · jiezi

Cocoapods 和 Carthage 使用笔记

Cocoapods安装(可选)使用 taobao ruby-china 源替换默认 gem 源: gem source blabla..$ gem sources -l*** CURRENT SOURCES https://rubygems.org/$ gem sources –remove https://rubygems.org/https://ruby.taobao.org/ removed from sources$ gem source -a https://gems.ruby-china.com/https://gems.ruby-china.com/ added to sources$ gem source -c Removed specs cache $ gem source -usource cache successfully updated$ gem sources -l CURRENT SOURCES https://gems.ruby-china.com/sudo gem install cocoapods(可选)切换 pod 源$ pod repomaster- Type: git (master)- URL: https://github.com/CocoaPods/Specs.git- Path: /Users/qiwihui/.cocoapods/repos/master$ pod repo remove master$ pod repo add master https://git.coding.net/CocoaPods/Specs.git$ pod repo update$ pod setup或者bash$ git clone https://git.coding.net/CocoaPods/Specs.git /.cocoapods/repos/master$ pod repo update切换回官方镜像bash$ pod repo remove master$ pod repo add master https://github.com/CocoaPods/Specs.git$ pod repo updateUpdating spec repo master $ /usr/local/bin/git -C /Users/qiwihui/.cocoapods/repos/master fetch origin –progress remote: Enumerating objects: 511, done. remote: Counting objects: 100% (511/511), done. remote: Compressing objects: 100% (134/134), done. remote: Total 820 (delta 399), reused 449 (delta 367), pack-reused 309 Receiving objects: 100% (820/820), 99.24 KiB | 401.00 KiB/s, done. Resolving deltas: 100% (501/501), completed with 194 local objects. From https://github.com/CocoaPods/Specs 5b04790953c..e3ba7ee3a29 master -> origin/master $ /usr/local/bin/git -C /Users/qiwihui/.cocoapods/repos/master rev-parse –abbrev-ref HEAD master $ /usr/local/bin/git -C /Users/qiwihui/.cocoapods/repos/master reset –hard origin/master HEAD is now at e3ba7ee3a29 [Add] IOS_OC_BASIC 6.3CocoaPods 1.6.0.beta.2 is available.To update use: sudo gem install cocoapods --pre[!] This is a test version we’d love you to try.For more information, see https://blog.cocoapods.org and the CHANGELOG for this version at https://github.com/CocoaPods/CocoaPods/releases/tag/1.6.0.beta.2```如果Podfile文件中有source ‘https://github.com/CocoaPods/Specs.git'也需要把它换成repo的源,否则依然是使用GitHub源基础用法cd <project_folder>pod init编辑 Podfile, example# 平台,必需platform :ios, ‘9.0’# 隐藏警告inhibit_all_warnings!target ‘AlamofireDemo’ do # Using Swift and want to use dynamic frameworks use_frameworks! # 项目 Pods pod ‘Alamofire’, ‘> 4.5’ target ‘AlamofireDemoTests’ do inherit! :search_paths # 测试 Pods endend版本支持:- &gt;, &gt;=, &lt;, &lt;=- ~&gt;: up to next major | minor | patch- :path 本地绝对路径- :git git项目地址,还可使用 :branch, :tag, :commitpod installAlways 打开项目下 .xcworkspace 文件作为项目入口pod install 和 pod update 区别pod install [package_name]: 安装特定版本的 podspod update [package_name]: 升级 pods 到最新版本Carthage安装brew install carthage使用编辑 Cartfile,比如 SwiftyJSONgithub “SwiftyJSON/SwiftyJSON"carthage update [–platform ios]$ carthage update Fetching SwiftyJSON** Checking out SwiftyJSON at “4.2.0”*** xcodebuild output can be found in /var/folders/kl/g94q0k_571vdjtcwzzcv20s40000gn/T/carthage-xcodebuild.nN22hg.log*** Building scheme “SwiftyJSON iOS” in SwiftyJSON.xcworkspace*** Building scheme “SwiftyJSON watchOS” in SwiftyJSON.xcworkspace*** Building scheme “SwiftyJSON tvOS” in SwiftyJSON.xcworkspace*** Building scheme “SwiftyJSON macOS” in SwiftyJSON.xcworkspaceCarthage 目录下:Build(编译出来的.framework二进制代码库)Checkouts(源码)$ tree -L 3 Carthage/Carthage/├── Build│ ├── Mac│ │ ├── SwiftyJSON.framework│ │ └── SwiftyJSON.framework.dSYM│ ├── iOS│ │ ├── 22BD4B6C-0B26-35E1-AF5F-8FB6AEBFD2FD.bcsymbolmap│ │ ├── C862E8A1-24ED-398A-A8E9-A7384E34EDB1.bcsymbolmap│ │ ├── SwiftyJSON.framework│ │ └── SwiftyJSON.framework.dSYM│ ├── tvOS│ │ ├── 1ADB9C1F-36CA-3386-BF07-6EE29B5F8081.bcsymbolmap│ │ ├── SwiftyJSON.framework│ │ └── SwiftyJSON.framework.dSYM│ └── watchOS│ ├── A8A151AB-D15E-3A0B-8A17-BF1A39EC6AB4.bcsymbolmap│ ├── EA427A42-6D21-3FF4-919F-5E50BF8A5D7B.bcsymbolmap│ ├── SwiftyJSON.framework│ └── SwiftyJSON.framework.dSYM└── Checkouts └── SwiftyJSON ├── CHANGELOG.md ├── Example ├── LICENSE ├── Package.swift ├── README.md ├── Source ├── SwiftyJSON.podspec ├── SwiftyJSON.xcodeproj ├── SwiftyJSON.xcworkspace ├── Tests └── scripts添加生成的文件: 项目 “General” -> “Linked Frameworks and Libraries” -> 将 Carthage/Build/iOS 中的 .framework 文件添加到项目中"Build Phases” -> “+” -> “New Run Script Phase”/bin/sh/usr/local/bin/carthage copy-frameworks"Input Files": $(SRCROOT)/Carthage/Build/iOS/SwiftyJSON.framework"Output Files": $(BUILT_PRODUCTS_DIR)/$(FRAMEWORKS_FOLDER_PATH)/SwiftyJSON.framework添加这个 Run Script 的作用是为了让运行时能够找到这个动态库。还可以将 Carthage 所集成的第三方库生成的符号文件添加到项目中,这样我们在调试的时候,就可以步入第三方库内部的代码:Build Phrases -> New Copy Files Phrase,将 Carthage/Build/iOS 目录中的 SwiftyJSON.framework.dSYM 符号文件拖动进来Carthage 和 CoaoaPods 的区别CoaoaPods 是一套整体解决方案,我们在 Podfile 中指定好我们需要的第三方库。然后 CocoaPods 就会进行下载,集成,然后修改或者创建我们项目的 workspace 文件,这一系列整体操作。相比之下,Carthage 就要轻量很多,它也会一个叫做 Cartfile 描述文件,但 Carthage 不会对我们的项目结构进行任何修改,更不多创建 workspace。它只是根据我们描述文件中配置的第三方库,将他们下载到本地,然后使用 xcodebuild 构建成 framework 文件。然后由我们自己将这些库集成到项目中。Carthage 使用的是一种非侵入性的哲学。另外 Carthage 除了非侵入性,它还是去中心化的,它的包管理不像 CocoaPods 那样,有一个中心服务器(cocoapods.org),来管理各个包的元信息,而是依赖于每个第三方库自己的源地址,比如 Github。参考解决Cocoapods贼慢问题Carthage 包管理工具,另一种敏捷轻快的 iOS & MAC 开发体验 ...

January 16, 2019 · 3 min · jiezi

移动端优雅布局实践

移动端优雅布局实践前言:移动端有非常多的坑,布局首当其冲。背景移动端应用有各种复杂的页面需求,不仅要解决单屏、多屏、固定头部或底部等多个场景,还要兼容ios和Android内核,在经历了项目实战(手机模式打开)过后,总结出了一些经验,在这里和大家分享一下。这篇文章是基于 →Next轻量级框架与主流工具的整合 最新的代码在这里 → next-mobile-complete-demo心路历程首先,需要实现的首页类似于app应用。第一站:统一flex布局的坑→ flex教程首页如上面的图片所示,然后偷了个懒,直接用了antd-mobile的tab标签栏,它需要指定容器高度。于是,在项目最开始的时候直接通过js将容器设置为浏览器html窗口的高度:// html高度 = body高度 = 主容器高度doc.body.style.height = docEl.clientHeight + ‘px’;这样做也有许多好处:纵向布局十分方便,不管是tab标签栏、tab标签页、头部固定元素或者底部固定元素实现起来都很简单;也能很简单就能实现元素居中对齐、两端对齐、自动分配空间等;同时也避免了Android输入框引起传统布局的问题。不过这里有个唯一的不好的地方就是兼容不了ios的前进返回的操作栏和上下滚动回弹:不能在滚动时隐藏前进返回的操作栏、在上下滚动时容易触发页面的回弹效果,造成滚动卡顿。这里补充一下:在ios上如果以正常流布局,内容超过一定高度时,向下滚动会隐藏底部的前进返回栏,向上滚动的时候再显示出来。当滚动到底部或者顶部时,再继续拉动页面,会有一个回弹的效果。这两个问题极大的降低了ios的用户体验,又恰逢项目间隔期,有大把的时间,于是被推着到了布局优化上面。第二站:寻找良好用户体验的布局案例这里我们在寻找的过程中发现两个具有代表性的移动端应用:bilibili的m站、腾讯新闻它用的是传统式流布局,头部和底部fixed固定,所有的内容全部向下平铺。花瓣网H5它采用的是设置主容器的position属性为absolute来脱离文档流的形式。这两种布局方式在ios上都用户体验极好。但同时也发现他们都存在的一些问题,比如:登录页面明明不足一屏,却依然存在滚动;没有兼容iPhone X的安全域;弹窗滚动穿透等等。如果能将这两种布局和flex进行整合一下,聚合各自的优点,基本上能打造一个用户体验和兼容性都令人满意的移动端应用了。那么,如何基于现有的flex布局去整合这两种布局又成了一个问题。第三站:基于flex调整布局结构在调整之前,我的预期是这样的:去掉外层高度限制,去掉纵向flex布局(除极少数单屏页面),将顶部、底部固定和弹窗设置为fixed。考虑到少数单屏页面需要继承html窗口的高度,于是采用了主容器脱离文档流的方式。调整过后dom结构是这样的:html > body > div#root > div.main-content(position: absolute)只需要给main-content加上height: 100%,就可以满足单屏页面的需求。好事多磨!按照上面的想法进行改造过后又发现了新的问题:脱离文档流过后,切换页面,html会保持最后滚动的位置。第四站:滚动条位置、滚动穿透、兼容iPhone X解决滚动条位置被记录的问题 找了一下发现history有个scrollRestoration的属性,它有两个值,一个是默认值auto还有一个是manual。如果设置为manual就可以手动设置滚动的位置。if (‘scrollRestoration’ in history) { history.scrollRestoration = ‘manual’;}然后在切换路由过后设置滚动的位置为起始位置:Router.events.on(‘routeChangeComplete’, () => {document.scrollingElement.scrollTop = 0;});这样每次切换页面都是从初始位置开始。这里有点鸡肋,前进后退也不会记录滚动位置。如果需要前进后退记录滚动的位置,就不能用这种脱离文档流的形式,需要用body滚动的形式,也就是。解决滚动穿透 这个问题,需要针对有滚动的弹窗和无滚动的弹窗单独处理。针对无滚动的弹窗,在弹窗弹出的时候禁用touchmove事件,隐藏的时候移除事件监听。有滚动的弹窗,需要找到页面的滚动的容器设置overflow:hidden兼容iPhone X 在meta标签后增加viewport-fit=cover<meta name=“viewport” content=“initial-scale=1,maximum-scale=1,minimum-scale=1,user-scalable=no,viewport-fit=cover”/>然后留出安全域距离padding-bottom: 0.49rem; // 底部button的高度padding-bottom: calc(0.49rem + constant(safe-area-inset-bottom));padding-bottom: calc(0.49rem + env(safe-area-inset-bottom));这里需要注意些padding-bottom的时候需要写三个,第一个是为了兼容Android。总结总的来说flex布局还是带了十分大的便利,尤其是能根治居中、适配等一些老大难的问题。每种布局的方式都有一定的缺陷,到目前为止还没有一种万能的方案来解决移动端各种复杂的场景。还是需要根据自己的需求还选择使用哪种实现方式。*前面这三种方案各自的缺陷:纵向flex布局(不建议使用)body流式平铺(传统)absolute脱离文档流(激进)ios前进后退的操作栏无法隐藏、回弹效果引起卡顿内部容器无法继承html窗口高度前进后退无法记录滚动条位置

January 16, 2019 · 1 min · jiezi

在iOS-Swift项目中集成CppJieba分词

背景在垃圾短信过滤应用 SMSFilters 中,需要使用 Jieba 分词库来対短信进行分词,然后使用 TF-IDF 来进行处理` 分词库是 C++ 写的,这就意味着需要在Swift中集成 C++ 库。在官方文档 “Using Swift with Cocoa and Objective-C” 中,Apple只是介绍了怎么将 Swift 代码跟 Objective-C 代码做整合,但是没有提C++,后来在官方文档中看到了这样一段话:You cannot import C++ code directly into Swift. Instead, create an Objective-C or C wrapper for C++ code.也就是不能直接导入 C++ 代码,但是可以使用 Objective-C 或者 C 对 C++ 进行封装。所以项目中使用 Objective-C 做封装,然后在 Swift 中调用,下面就是这个过程的实践,Demo 代码见 SwiftJiebaDemo。整合过程分成三步:引入C++文件;用 Objective-C 封装;在 Swift 中 调用 Objective-C;引入C++文件Demo中使用的是"结巴"中文分词的 C++ 版本 yanyiwu/cppjieba。将其中的 include/cppjieba 和依赖 limonp 合并,并加入 dict 中的 hmm_model 和 jiaba.dict 作为基础数据,并暴露 JiebaInit 和 JiebaCut 接口://// Segmentor.cpp// iosjieba//// Created by yanyiwu on 14/12/24.// Copyright (c) 2014年 yanyiwu. All rights reserved.//#include “Segmentor.h”#include <iostream>using namespace cppjieba;cppjieba::MixSegment * globalSegmentor;void JiebaInit(const string& dictPath, const string& hmmPath, const string& userDictPath){ if(globalSegmentor == NULL) { globalSegmentor = new MixSegment(dictPath, hmmPath, userDictPath); } cout << FILE << LINE << endl;}void JiebaCut(const string& sentence, vector<string>& words){ assert(globalSegmentor); globalSegmentor->Cut(sentence, words); cout << FILE << LINE << endl; cout << words << endl;}以及//// Segmentor.h// iosjieba//// Created by yanyiwu on 14/12/24.// Copyright (c) 2014年 yanyiwu. All rights reserved.//#ifndef iosjieba__Segmentor#define iosjieba__Segmentor#include <stdio.h>#include “cppjieba/MixSegment.hpp”#include <string>#include <vector>extern cppjieba::MixSegment * globalSegmentor;void JiebaInit(const std::string& dictPath, const std::string& hmmPath, const std::string& userDictPath);void JiebaCut(const std::string& sentence, std::vector<std::string>& words);#endif /* defined(iosjieba__Segmentor) */目录如下:$ tree iosjiebaiosjieba├── Segmentor.cpp├── Segmentor.h├── cppjieba│ ├── DictTrie.hpp│ ├── FullSegment.hpp│ ├── HMMModel.hpp│ ├── HMMSegment.hpp│ ├── Jieba.hpp│ ├── KeywordExtractor.hpp│ ├── MPSegment.hpp│ ├── MixSegment.hpp│ ├── PosTagger.hpp│ ├── PreFilter.hpp│ ├── QuerySegment.hpp│ ├── SegmentBase.hpp│ ├── SegmentTagged.hpp│ ├── TextRankExtractor.hpp│ ├── Trie.hpp│ ├── Unicode.hpp│ └── limonp│ ├── ArgvContext.hpp│ ├── BlockingQueue.hpp│ ├── BoundedBlockingQueue.hpp│ ├── BoundedQueue.hpp│ ├── Closure.hpp│ ├── Colors.hpp│ ├── Condition.hpp│ ├── Config.hpp│ ├── FileLock.hpp│ ├── ForcePublic.hpp│ ├── LocalVector.hpp│ ├── Logging.hpp│ ├── Md5.hpp│ ├── MutexLock.hpp│ ├── NonCopyable.hpp│ ├── StdExtension.hpp│ ├── StringUtil.hpp│ ├── Thread.hpp│ └── ThreadPool.hpp└── iosjieba.bundle └── dict ├── hmm_model.utf8 ├── jieba.dict.small.utf8 └── user.dict.utf8接下来开始在项目中集成。首先创建一个空项目 iOSJiebaDemo,将 iosjieba 加入项目中。单页应用SwiftJiebaDemo添加 SwiftJiebaDemo添加 iosjieba:见代码: https://github.com/qiwihui/Sw...C++ 到 Objective-C 封装这个过程是将 C++ 的接口进行 Objective-C 封装,向 Swift 暴露。这个封装只暴露了 objcJiebaInit 和 objcJiebaCut 两个接口。//// iosjiebaWrapper.h// SMSFilters//// Created by Qiwihui on 1/14/19.// Copyright © 2019 qiwihui. All rights reserved.//#import <Foundation/Foundation.h>@interface JiebaWrapper : NSObject- (void) objcJiebaInit: (NSString *) dictPath forPath: (NSString *) hmmPath forDictPath: (NSString *) userDictPath;- (void) objcJiebaCut: (NSString *) sentence toWords: (NSMutableArray *) words;@end//// iosjiebaWrapper.mm// iOSJiebaTest//// Created by Qiwihui on 1/14/19.// Copyright © 2019 Qiwihui. All rights reserved.//#import <Foundation/Foundation.h>#import “iosjiebaWrapper.h”#include “Segmentor.h”@implementation JiebaWrapper- (void) objcJiebaInit: (NSString *) dictPath forPath: (NSString *) hmmPath forDictPath: (NSString *) userDictPath { const char *cDictPath = [dictPath UTF8String]; const char *cHmmPath = [hmmPath UTF8String]; const char *cUserDictPath = [userDictPath UTF8String]; JiebaInit(cDictPath, cHmmPath, cUserDictPath); }- (void) objcJiebaCut: (NSString ) sentence toWords: (NSMutableArray ) words { const char cSentence = [sentence UTF8String]; std::vector<std::string> wordsList; for (int i = 0; i < [words count];i++) { wordsList.push_back(wordsList[i]); } JiebaCut(cSentence, wordsList); [words removeAllObjects]; std::for_each(wordsList.begin(), wordsList.end(), [&words](std::string str) { id nsstr = [NSString stringWithUTF8String:str.c_str()]; [words addObject:nsstr]; });}@end见代码: https://github.com/qiwihui/Sw...Objective-C 到 Swift在 Swift 中调用 Objecttive-C 的接口,这个在官方文档和许多博客中都有详细介绍。加入 {project_name}-Bridging-Header.h 头文件,即 SwiftJiebaDemo_Bridging_Header_h,引入之前封装的头文件,并在 Targets -> Build Settings -> Objective-C Bridging Header 中设置头文件路径 SwiftJiebaDemo/SwiftJiebaDemo_Bridging_Header_h。//// SwiftJiebaDemo-Bridging-Header.h// SwiftJiebaDemo//// Created by Qiwihui on 1/15/19.// Copyright © 2019 Qiwihui. All rights reserved.//#ifndef SwiftJiebaDemo_Bridging_Header_h#define SwiftJiebaDemo_Bridging_Header_h#import “iosjiebaWrapper.h”#endif / SwiftJiebaDemo_Bridging_Header_h */将使用到 C++ 的 Objective-C 文件修改为 Objective-C++ 文件,即 将 .m 改为 .mm: iosjiebaWrapper.m 改为 iosjiebaWrapper.mm。见代码:https://github.com/qiwihui/Sw…使用使用时需要先初始化 Jiaba分词,然后再进行分词。class Classifier { init() { let dictPath = Bundle.main.resourcePath!+"/iosjieba.bundle/dict/jieba.dict.small.utf8" let hmmPath = Bundle.main.resourcePath!+"/iosjieba.bundle/dict/hmm_model.utf8" let userDictPath = Bundle.main.resourcePath!+"/iosjieba.bundle/dict/user.dict.utf8" JiebaWrapper().objcJiebaInit(dictPath, forPath: hmmPath, forDictPath: userDictPath); } func tokenize(_ message:String) -> [String] { print(“tokenize…”) let words = NSMutableArray() JiebaWrapper().objcJiebaCut(message, toWords: words) return words as! [String] }}控制台输出结果:可以看到,测试用例 小明硕士毕业于中国科学院计算所,后在日本京都大学深造 经过分词后为〔拼音〕[“小明”, “硕士”, “毕业”, “于”, “中国科学院”, “计算所”, “,”, “后”, “在”, “日本”, “京都大学”, “深造”],完成集成。见代码: https://github.com/qiwihui/Sw…遇到的问题由于自己对于编译链接原理不了解,以及是 iOS 开发初学,因此上面的这个过程中遇到了很多问题,耗时两周才解决,故将遇到的一些问题记录于此,以便日后。“cassert” file not found将 .m 改为 .mm 即可。compiler not finding <tr1/unordered_map>设置 C++ Standard Library 为 LLVM libc++参考: mac c++ compiler not finding <tr1/unordered_map>warning: include path for stdlibc++ headers not found; pass ‘-std=libc++’ on the command line to use the libc++ standard library instead [-Wstdlibcxx-not-found]Build Setting -> C++ Standard Library -> libstdc++ 修改为 Build Setting -> C++ Standard Library -> libc++use of unresolved identifier这个问题在于向项目中加入文件时,Target Membership 设置不正确导致。需要将对于使用到的 Target 都勾上。相关参考: Understanding The “Use of Unresolved Identifier” Error In Xcode参考SwiftArchitect 对问题 “Can I have Swift, Objective-C, C and C++ files in the same Xcode project?” 的回答SwiftArchitect 对问题 “Can I mix Swift with C++? Like the Objective - C .mm files” 的回答在Swift代码中整合C++类库 ...

January 16, 2019 · 4 min · jiezi

h5页面在ios端点击高亮闪烁

记得那是第一次独自完成一个项目,现在看来,那个项目会很简单的,但那个时候还是挺有成就感的。 当时碰到过一个问题,h5页面在ios端点击的时候,整个页面会高亮的闪烁一下,特别明显,被测试狂崔。。。最后发现是 css样式的问题 加一行就好了 { -webkit-tap-highlight-color: transparent; —–》解决 ios端 高亮闪烁的问题}原因如下当用户点击iOS的Safari浏览器中的链接或JavaScript的可点击的元素时,覆盖显示的高亮颜色。该属性可以只设置透明度。如果未设置透明度,iOS Safari使用默认的透明度。当透明度设为0,则会禁用此属性;当透明度设为1,元素在点击时不可见。

January 15, 2019 · 1 min · jiezi

谈谈iOS获取调用链

本文由云+社区发表iOS开发过程中难免会遇到卡顿等性能问题或者死锁之类的问题,此时如果有调用堆栈将对解决问题很有帮助。那么在应用中如何来实时获取函数的调用堆栈呢?本文参考了网上的一些博文,讲述了使用mach thread的方式来获取调用栈的步骤,其中会同步讲述到栈帧的基本概念,并且通过对一个demo的汇编代码的讲解来方便理解获取调用链的原理。一、栈帧等几个概念先抛出一个栈帧的概念,解释下什么是栈帧。应用中新创建的每个线程都有专用的栈空间,栈可以在线程期间自由使用。而线程中有千千万万的函数调用,这些函数共享进程的这个栈空间,那么问题就来了,函数运行过程中会有非常多的入栈出栈的过程,当函数返回backtrace的时候怎样能精确定位到返回地址呢?还有子函数所保存的一些寄存器的内容?这样就有了栈帧的概念,即每个函数所使用的栈空间是一个栈帧,所有的栈帧就组成了这个线程完整的栈。栈帧下面再抛出几个概念:寄存器中的fp,sp,lr,pc。寄存器是和CPU联系非常紧密的一小块内存,经常用于存储一些正在使用的数据。对于32位架构armv7指令集的ARM处理器有16个寄存器,从r0到r15,每一个都是32位比特。调用约定指定他们其中的一些寄存器有特殊的用途,例如:r0-r3:用于存放传递给函数的参数;r4-r11:用于存放函数的本地参数;r11:通常用作桢指针fp(frame pointer寄存器),栈帧基址寄存器,指向当前函数栈帧的栈底,它提供了一种追溯程序的方式,来反向跟踪调用的函数。r12:是内部程序调用暂时寄存器。这个寄存器很特别是因为可以通过函数调用来改变它;r13:栈指针sp(stack pointer)。在计算机科学内栈是非常重要的术语。寄存器存放了一个指向栈顶的指针。看这里了解更多关于栈的信息;r14:是链接寄存器lr(link register)。它保存了当目前函数返回时下一个函数的地址;r15:是程序计数器pc(program counter)。它存放了当前执行指令的地址。在每个指令执行完成后会自动增加;不同指令集的寄存器数量可能会不同,pc、lr、sp、fp也可能使用其中不同的寄存器。后面我们先忽略r11等寄存器编号,直接用fp,sp,lr来讲述 如下图所示,不管是较早的帧,还是调用者的帧,还是当前帧,它们的结构是完全一样的,因为每个帧都是基于一个函数,帧伴随着函数的生命周期一起产生、发展和消亡。在这个过程中用到了上面说的寄存器,fp帧指针,它总是指向当前帧的底部;sp栈指针,它总是指向当前帧的顶部。这两个寄存器用来定位当前帧中的所有空间。编译器需要根据指令集的规则小心翼翼地调整这两个寄存器的值,一旦出错,参数传递、函数返回都可能出现问题。其实这里这几个寄存器会满足一定规则,比如:fp指向的是当面栈帧的底部,该地址存的值是调用当前栈帧的上一个栈帧的fp的地址。lr总是在上一个栈帧(也就是调用当前栈帧的栈帧)的顶部,而栈帧之间是连续存储的,所以lr也就是当前栈帧底部的上一个地址,以此类推就可以推出所有函数的调用顺序。这里注意,栈底在高地址,栈向下增长而由此我们可以进一步想到,通过sp和fp所指出的栈帧可以恢复出母函数的栈帧,不断递归恢复便恢复除了调用堆栈。向下面代码一样,每次递归pc存储的*(fp + 1)其实就是返回的地址,它在调用者的函数内,利用这个地址我们可以通过符号表还原出对应的方法名称。while(fp) { pc = *(fp + 1); fp = fp;}二、汇编解释下如果你非要问为什么会这样,我们可以从汇编角度看下函数是怎么调用的,从而更深刻理解为什么fp总是存储了上一个栈帧的fp的地址,而fp向前一个地址为什么总是lr?写如下一个demo程序,由于我是在mac上做实验,所以直接使用clang来编译出可执行程序,然后再用hopper工具反汇编查看汇编代码,当然也可直接使用clang的-S参数指定生产汇编代码。demo源码#import <Foundation/Foundation.h>int func(int a);int main (void){ int a = 1; func(a); return 0;}int func (int a){ int b = 2; return a + b;}汇编语言 ; ================ B E G I N N I N G O F P R O C E D U R E ================ ; Variables: ; var_4: -4 ; var_8: -8 ; var_C: -12 _main:0000000100000f70 push rbp0000000100000f71 mov rbp, rsp0000000100000f74 sub rsp, 0x100000000100000f78 mov dword [rbp+var_4], 0x00000000100000f7f mov dword [rbp+var_8], 0x10000000100000f86 mov edi, dword [rbp+var_8] ; argument #1 for method _func0000000100000f89 call _func0000000100000f8e xor edi, edi0000000100000f90 mov dword [rbp+var_C], eax0000000100000f93 mov eax, edi0000000100000f95 add rsp, 0x100000000100000f99 pop rbp0000000100000f9a ret ; endp0000000100000f9b nop dword [rax+rax] ; ================ B E G I N N I N G O F P R O C E D U R E ================ ; Variables: ; var_4: -4 ; var_8: -8 _func:0000000100000fa0 push rbp ; CODE XREF=_main+250000000100000fa1 mov rbp, rsp0000000100000fa4 mov dword [rbp+var_4], edi0000000100000fa7 mov dword [rbp+var_8], 0x20000000100000fae mov edi, dword [rbp+var_4]0000000100000fb1 add edi, dword [rbp+var_8]0000000100000fb4 mov eax, edi0000000100000fb6 pop rbp0000000100000fb7 ret需要注意,由于是在mac上编译出可执行程序,指令集已经是x86-64,所以上文的fp、sp、lr、pc名称和使用的寄存器发生了变化,但含义基本一致,对应关系如下:fp—-rbpsp—-rsppc—-rip接下来我们看下具体的汇编代码,可以看到在main函数中在经过预处理和参数初始化后,通过call _func来调用了func函数,这里call _func其实等价于两个汇编命令:Pushl %rip //保存下一条指令(第41行的代码地址)的地址,用于函数返回继续执行Jmp _func //跳转到函数foo于是,当main函数调用了func函数后,会将下一行地址push进栈,至此,main函数的栈帧已经结束,然后跳转到func的代码处开始继续执行。可以看出,rip指向的函数下一条地址,即上文中所说的lr已经入栈,在栈帧的顶部。而从func的代码可以看到,首先使用push rbp将帧指针保存起来,而由于刚跳转到func函数,此时rbp其实是上一个栈帧的帧指针,即它的值其实还是上一个栈帧的底部地址,所以此步骤其实是将上一个帧底部地址保存了下来。下一句汇编语句mov rbp, rsp将栈顶部地址rsp更新给了rbp,于是此时rbp的值就成了栈的顶部地址,也是当前栈帧的开始,即fp。而栈顶部又正好是刚刚push进去的存储上一个帧指针地址的地址,所以rbp指向的时当前栈帧的底部,但其中保存的值是上一个栈帧底部的地址。至此,也就解释了为什么fp指向的地址存储的内容是上一个栈帧的fp的地址,也解释了为什么fp向前一个地址就正好是lr。另外一个比较重要的东西就是出入栈的顺序,在ARM指令系统中是地址递减栈,入栈操作的参数入栈顺序是从右到左依次入栈,而参数的出栈顺序则是从左到右的你操作。包括push/pop和LDMFD/STMFD等。三、获取调用栈步骤其实上面的几个fp、lr、sp在mach内核提供的api中都有定义,我们可以使用对应的api拿到对应的值。如下便是64位和32位的定义_STRUCT_ARM_THREAD_STATE64{ __uint64_t __x[29]; / General purpose registers x0-x28 / __uint64_t __fp; / Frame pointer x29 / __uint64_t __lr; / Link register x30 / __uint64_t __sp; / Stack pointer x31 / __uint64_t __pc; / Program counter / __uint32_t __cpsr; / Current program status register / __uint32_t __pad; / Same size for 32-bit or 64-bit clients /};_STRUCT_ARM_THREAD_STATE{ __uint32_t r[13]; / General purpose register r0-r12 / __uint32_t sp; / Stack pointer r13 / __uint32_t lr; / Link register r14 / __uint32_t pc; / Program counter r15 / __uint32_t cpsr; / Current program status register */};于是,我们只要拿到对应的fp和lr,然后递归去查找母函数的地址,最后将其符号化,即可还原出调用栈。总结归纳了下,获取调用栈需要下面几步:1、挂起线程thread_suspend(main_thread);2、获取当前线程状态上下文thread_get_state_STRUCT_MCONTEXT ctx;#if defined(x86_64) mach_msg_type_number_t count = x86_THREAD_STATE64_COUNT; thread_get_state(thread, x86_THREAD_STATE64, (thread_state_t)&ctx.__ss, &count);#elif defined(arm64) _STRUCT_MCONTEXT ctx; mach_msg_type_number_t count = ARM_THREAD_STATE64_COUNT; thread_get_state(thread, ARM_THREAD_STATE64, (thread_state_t)&ctx.__ss, &count);#endif3、获取当前帧的帧指针fp#if defined(x86_64) uint64_t pc = ctx.__ss.__rip; uint64_t sp = ctx.__ss.__rsp; uint64_t fp = ctx.__ss.__rbp;#elif defined(arm64) uint64_t pc = ctx.__ss.__pc; uint64_t sp = ctx.__ss.__sp; uint64_t fp = ctx.__ss.__fp;#endif4、递归遍历fp和lr,依次记录lr的地址while(fp) { pc = *(fp + 1); fp = fp;}这一步我们其实就是使用上面的方法来依次迭代出调用链上的函数地址,代码如下void t_fp[2];vm_size_t len = sizeof(record);vm_read_overwrite(mach_task_self(), (vm_address_t)(fp),len, (vm_address_t)t_fp, &len);do { pc = (long)t_fp[1] // lr总是在fp的上一个地址 // 依次记录pc的值,这里先只是打印出来 printf(pc) vm_read_overwrite(mach_task_self(),(vm_address_t)m_cursor.fp[0], len, (vm_address_t)m_cursor.fp,&len);} while (fp);上面代码便会从下到上依次打印出调用栈函数中的地址,这个地址总是在函数调用地方的下一个地址,我们就需要拿这个地址还原出对应的符号名称。5、恢复线程thread_resumethread_resume(main_thread);6、还原符号表这一步主要是将已经获得的调用链上的地址分别解析出对应的符号。主要是参考了运行时获取函数调用栈 的方法,其中用到的dyld链接mach-o文件的基础知识,后续会专门针对这里总结一篇文章。enumerateSegment(header, [&](struct load_command *command) { if (command->cmd == LC_SYMTAB) { struct symtab_command *symCmd = (struct symtab_command *)command; uint64_t baseaddr = 0; enumerateSegment(header, [&](struct load_command *command) { if (command->cmd == LC_SEGMENT_64) { struct segment_command_64 *segCmd = (struct segment_command_64 *)command; if (strcmp(segCmd->segname, SEG_LINKEDIT) == 0) { baseaddr = segCmd->vmaddr - segCmd->fileoff; return true; } } return false; }); if (baseaddr == 0) return false; nlist_64 *nlist = (nlist_64 *)(baseaddr + slide + symCmd->symoff); uint64_t strTable = baseaddr + slide + symCmd->stroff; uint64_t offset = UINT64_MAX; int best = -1; for (int k = 0; k < symCmd->nsyms; k++) { nlist_64 &sym = nlist[k]; uint64_t d = pcSlide - sym.n_value; if (offset >= d) { offset = d; best = k; } } if (best >= 0) { nlist_64 &sym = nlist[best]; std::cout << “SYMBOL: " << (char *)(strTable + sym.n_un.n_strx) << std::endl; } return true; } return false;});参考函数调用栈空间以及fp寄存器函数调用栈也谈栈和栈帧 运行时获取函数调用栈 深入解析Mac OS X & iOS 操作系统 学习笔记此文已由作者授权腾讯云+社区在各渠道发布获取更多新鲜技术干货,可以关注我们腾讯云技术社区-云加社区官方号及知乎机构号 ...

January 14, 2019 · 3 min · jiezi

【译】远程调试 iOS Safari

如今在移动设备上测试网站变得越来越重要了,我们会经常发现在移动设备的浏览器上面网站会表现 的和桌面浏览器不一样,因此在开发网站时用真机测试变得非常重要。大多数在桌面电脑的开发服务器都只是在 localhost 中打开一个端口,然后通过 URL http://localhost1234 来访问内容。这种方式在电脑端非常管用,但你不可以把这个 URL 复制到手机端测试。 一种可行(通常都可以)但并不高明的方式是先查找电脑端当前的 IP 地址,然后移动端通过 http://<ip-address-of-desktop>:<port> 来访问网站。然而,基于电脑端的 IP 地址来调试是非常烦人的,因为这个地址会经常发生变化。这意味着你不能保存该地址到书签中,而且当 IP 地址发生变化的时候,你将会丢失该域下的 数据,如 cookies, localStorage 等等。 然而,有一种简单的方式去解决这个问题, 这种方式只需要设置一次,不会受到 IP 地址变化的影响,甚至不需要数据线! 你只需要一台 Mac 和 Safari 即可。这个方案可以用在 macOS 10.12(Sierra), 10.13(High Sierra),10.14(Mojave),可能可以用在更老的 macOS 版本中。在你的移动设备中打开 localhost:port先假设你的测试服务器在 localhost 中打开了 8080 端口。在你的电脑端,你当然可以打开 localhost:8080 来访问网网站了。现在我们要让你的移动设备来打开它。实际上,我们不会使用 localhost:<port> 或者 <ip-address>:<port>,因为有一种更好的方式来代替它:计算机名。你可以在 系统偏好设置 -> 共享 中找到你的计算机名(注意,接下来我会用 <computer-name> 来指定计算机名)。接下来你需要至少激活一种在列表中共享服务,激活哪个都没所谓。这可能有点蠢,或者你可以激活打印机共享,因为通常只有你会使用到。现在确保你的 Mac 和 iOS 设备处在同一网络环境中,然后在你的 iOS 设备中打开 http://<computer-name>.local:8080。现在你的网站就会显示在你的 iOS 设备上了!你还是打不开网站吗? 看起来你需要设置你的服务器,用 0.0.0.0 代替 localhost (并且允许从 *.local 建立的链接)。把你的 IP 地址改成 0.0.0.0 会让你的服务器可以从外部访问(在同一 WiFi 内)。如果你正在使用 webpack-dev-server 的话,你只需要稍微改动一下配置就可以了。默认情况下,它会在 localhost 上创建服务器,并且不会允许外部链接(例如通过手机访问)。所以你需要修改一下 webpack dev server 的配置文件(准确来说就是 host 和 allowedHosts 字段):devServer: { host: ‘0.0.0.0’, allowedHosts: [ ‘.local’, ],},注意我们添加 .local 到 allowedHosts 中,这会让所有 .local 结尾的 host 可以 访问到我们的网站,这样当我们需要共享的时候非常有用。 重启服务器,你的 iOS 设备应该可以正常访问网站了!如果你收到报错信息 invalid host header 的话,那很可能你的计算机名输错了。实际上同一 WiFi 下的所有 iPhone, iPad,Mac 都可以访问到你开发环境中的网站的,而且 iOS 上的 Firefox 和 Chrome 都可以访问到。然而,你不可以在 Firefox 和 Chrome 中使用远程调试。远程调试现在你的手机和平板电脑都可以访问网站了,你可能通过它们来远程调试。设置起来是非常简单的。打开 Safari 的 偏好设置 -> 高级 然后启用 在菜单栏中显示“开发”菜单。在移动端,启用 设置 -> Safari 浏览器 -> 高级 -> Web 检查器,然后用数据线连接你的 Mac。用移动端的 Safari 打开网站,然后在 Mac 上的 Safari 选择 开发 -> <设备名字> -> <你想调试的 Tab>。如果你是第一次设置的话,那么你需要点击信任设备。现在所有的设置已经完成了。当你点击 <你想调试的 Tab> 的时候,Mac 上的 Safari 会创建一个调试用的 session,它会允许你在 Mac 的 Safari 中调试 iOS 设备。远程调试(无数据线)其实你不需要用数据线连接电脑也可以远程调试移动端的 Safari 的,但这种情况下你需要在 Mac 上安装 Safari Technology Preview,因为当前稳定版的 Safari 并不支持无线远程调试。重复上面做的事情,用数据线连接电脑然后打开 iOS 上的 Safari。在 Safari Technology Preview 中,确保你已经启用了 在菜单栏中显示“开发”菜单,然后启用 Develop -> <Your mobile Device Name> -> Connect via Network。现在你可以把数据线拔掉,看看 Develop -> <Your mobile Device Name> 是否还显示在 Safari Technology Preview 中。然后选择你想调试的 Tab 就可以了。Safari Technology Preview 会在 macOS 中建立一条无线连接到 iOS 设备上,这样你就可以在 Mac 上调试 iPhone 和 iPad 了。看,不需要数据线吧。Happy Testing!译注按照我的经验,用 Safari Technology Preview 来无线调试用起来是非常舒服的,但有个致命缺点就是非常不稳定。经常调试着调试着就搜不到设备了。当出现这种情况的时候,可以试着把Safari Technology Preview 杀掉然后重启,看看能不能找到设备,如果找不到, 就把 iOS 上的 Safari 杀掉再重启,再测试。多试几次通常就能正常调试了。 如果还是找不到,那过一段时间(通常是 5 ~ 10 分钟左右)再打开 Safari Technology Preview 试试。实在不行又赶着调试的话,还是乖乖插上数据线调试算了。(然而体验过无线调试之后就回不去有线调试了 XD)。出处http://scarletsky.github.io/2…完。参考资料(Wireless) Remote Debugging with Safari on iOS ...

January 11, 2019 · 2 min · jiezi

Flutter系列:3.APP基础设施搭建

前言在上一篇文章Flutter系列:2.实现一个简单的登录界面通过一个简单的登录页面带入了Flutter中页面构建的方式以及一些简单控件的使用;在开发一个app前首要的任务往往是搭建app需要的基础结构,比如底部菜单,路由导航,网络请求以及一些常用的颜色、图标、按钮、toast组件等。本次的demo将实现一个简单的app所需的基础结构,实现一个简单的app,基于底部TabBar的方式模块切分,实现网络层调用豆瓣api展示电影列表,任意界面登录验证,app如下图。[GitHub源码传送]TabBar菜单目前app设计中大部分app都是由底部TabBar菜单+顶部导航信息的方式构建的,在iOS开发中UITabBarController 和 UINavigationController 几乎是APP的标配, 同样在Flutter中基于Scaffold的构建方式也直接提供了appBar+body+bottomNavigationBar的方式来切分导航栏、内容和底部菜单,所以我们只需要在首页的Scaffold构造中传入bottomNavigationBar即可。在Flutter中为我们提供了material design风格的BottomNavigationBar和iOS风格的CupertinoTabBar,我们只需要选择其一稍作封装即可,本demo选择CupertinoTabBar,并封装到BottomNavWidget中,相关细节请看源码。body的切换虽然Scaffold提供了appBar+body+bottomNavigationBar的组合,但是并没有实现bottomNavigationBar点击切换body页面显示功能,所以需要开发者自己去处理bottomNavigationBar的点击回调来动态切换body中的内容。不同的bottomNavigationBarItem对应着不同的显示页面,电影tabBar对应显示电影列表页面,发现tabBar对应显示发现页面… 他们都在body中,他们之间有着频繁的切换但同时只能显示一个页面;基于此使用Stack布局的方式来实现,每个页面组成一个数组成为Stack Widget的children并缓存避免重复创建,使用Offstage组件来包装每个tab页面,并将bottomNavigationBar当前选择的index对应的页面的offstage设置为false, 这样只有当前选择的tab对应的页面显示在body中,而其他的界面并不会显示也不会接收事件占用空间。路由导航路由导航也是app常见的基础功能,服务器通过下发路由信息可以实现动态的控制app的页面跳转,常用于动态页面,push和web跳转。Flutter中的导航有点类似iOS的方式,都是通过栈的方式来管理路由页面。Navigator就是Flutter中管理导航路线的Widget,注意Navigator管理的是页面导航的路线,称为Route的东西而不是像iOS中直接管理的controller,而每个Route(CupertinoPageRoute)则可以通过builder来指定显示的Widget,同时Navigator也提供了对Route 栈操作的方法,push和pop。Navigator管理的对象是Route,Flutter提供了MaterialPageRoute和iOS风格的CupertinoPageRoute,MaterialPageRoute是根据手机平台自动调整页面的出现动画,本Demo选用CupertinoPageRoute以从右到左的页面出现动画,然后指定其builder即可实现页面的跳转。MaterialApp内置了一个顶层的导航器Navigator,routes属性支持配置静态的路由表,如果在routes中找不到对应的路由配置时则调用onGenerateRoute来支持动态的路由跳转,它的定义如下:所以我们需要通过一个函数来实现MaterialApp的onGenerateRoute就可以根据RouteSettings中的路由信息动态的生成页面的Route,同时以Uri的方式来指定Route的名称就可以实现动态传参了,具体详见Demo源码中RouteManager类。登录注册登录注册页面可能在app的任何页面推出,同时可能不支持返回需要强制登录的情况,在iOS中常常以present的方式出现,所以在Flutter中需要指定CupertinoPageRoute的fullscreenDialog属性为true即可页面的跳转在iOS的开发中基于UITabBarController 和 UINavigationController的构建方式中页面跳转是在UINavigationController内跳转的,同时通过设置Controller的hidesBottomBarWhenPushed属性支持动态的显示和隐藏底部的TarBar, 每个TabBar对应的是一个独立控制的UINavigationController,他们各自有自己路由的导航栈,在Flutter中提供的CupertinoTabScaffold通过为每个TabBar指定显示为CupertinoTabView来实现了同样的机制。往往在开发中进入二级界面后底部的导航栏都是隐藏的,所以我们完全可以只使用MaterialApp内置的顶层Navigator来实现我们的导航控制,本Demo也是如此。网络请求移动端的网络环境是千变万化的,所以app的网络请求应该是一个异步的过程,不能阻塞主线程,本Demo是基于Dart的第三方Http网络请求库dio。dio是一个强大的Dart Http请求库,支持Restful API、FormData、拦截器、请求取消、Cookie管理、文件上传/下载、超时等…网络层实现了通过dio请求到网络数据然后反序列为Model对象,dart中的json反序列化要比其他语言麻烦,借助的是json_annotation这个库。从请求api到回调再到反序列为Model对象这个过程都应该是一个异步的过程,所以他们返回的都是一个Futrure对象,使用Completer就可以很方便的生成一个Future, 然后在恰当的时候传入数据或者错误来结束这个Future。列表列表的展示是基于FutureBuilder的方式,因为其依赖api请求返回的future,当future的状态变更时FutureBuilder会接收到最新的快照信息AsyncSnapshot,通过其当前快照来控制ListView或者CircularProgressIndicator的显示。其他App开发中还有许多其他的基础模块,比如和原生通信组件(channel)、图片组件、日志组件、其他公共的弹窗、上下拉刷新组件等,本Demo还来不及一一实现,随着学习的深入以后再慢慢总结吧,有不妥的地方还望指正。Demo源码地址:[GitHub源码传送]

January 10, 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

mac小技巧

Mac安装mysql后配置文件Mac安装mysql后配置文件mysql -u root -proot 本机mysql账号密码关于zsh终端的一个坑修改完 ~/.bash_profile 添加全局变量后,用zsh终端重新打开,还是找不到命令。解决办法,在 ~/.zshrc 里添加source ~/.bash_profile即每次打开终端,自动执行一遍配置文件,就能找到对应命令了Mac 自带apache 的使用sudo apachectl start // 启动sudo apachectl stop // 停止配置文件目录/etc/apache2/httpd.conf站点根目录/Library/WebServer/Documents/Mac 安装 nginx安装brew install nginx安装完提示Docroot is: /usr/local/var/wwwThe default port has been set in /usr/local/etc/nginx/nginx.conf to 8080 so thatnginx can run without sudo.nginx will load all files in /usr/local/etc/nginx/servers/.To have launchd start nginx now and restart at login: brew services start nginxOr, if you don’t want/need a background service you can just run: nginx根据提示,直接输入 nginx 命令就可以打开服务器了。站点根目录在 /usr/local/var/www。配置文件在 /usr/local/etc/nginx/nginx.conf。Mac 使用小技巧在终端打开访达,直接运行 open 命令即可。比如想往一个目录复制文件,在命令行里cd到路径后,通过open命令打开访达,复制文件更方便open . // 打开当前目录 ...

January 6, 2019 · 1 min · jiezi

企业版app ipa包部署到自己服务器

前言:最近开始进军uni-app混合开发的坑,已采坑无数,每跨过一个坑,实力就能提升一点点。现在需要企业版app ipa包需要部署到自己的服务器下载。所有的ipa包部署自己的服务器最终都有如下4个文件:test.ipa、manifest.plist、test.mobileprovision(描述文件)、index.html(下载页面)1.test.ipa包生成的方式有很多 Xcode打包生成、HbuilderX打包等等2.manifest.plist生成的方式也有很多第8区plist文件制作、Xcode打包自动会生成plist文件plist文件配置查看:<?xml version=“1.0” encoding=“UTF-8”?><!DOCTYPE plist PUBLIC “-//Apple//DTD PLIST 1.0//EN” “http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version=“1.0”><dict> <key>items</key> <array> <dict> <key>assets</key> <array> <dict> <key>kind</key> <string>software-package</string> <key>url</key> <string>https://www.dapeis.com/apps/dpc.ipa</string> </dict> <dict> <key>kind</key> <string>display-image</string> <key>url</key> <string>https://www.dapeis.com/apps/icon57.png</string> </dict> <dict> <key>kind</key> <string>full-size-image</string> <key>url</key> <string>https://www.dapeis.com/apps/icon512.png</string> </dict> </array> <key>metadata</key> <dict> <key>bundle-identifier</key> <string>io.dcloud.dpc</string> <key>bundle-version</key> <string>1.0.0</string> <key>kind</key> <string>software</string> <key>title</key> <string>搭配钱包</string> </dict> </dict> </array></dict></plist>3.index.html页面地址代码如下:<!DOCTYPE html><html><head> <title>iosAPP下载</title></head><body> <!– ios app下载地址 –> <a href=“itms-services://?action=download-manifest&url=https://www.dapeis.com/apps/manifest.plist”> <img src=“https://www.dapeis.com/apps/icon57.png"> </a> <h1>IOS下载</h1> <!– ios证书信任方法 –> <a href=“https://www.dapeis.com/apps/dpcProfile.mobileprovision">前往信任</a></body>4.test.mobileprovision(描述文件),这个文件需要有开发者账号才能获取,怎么获取可自己查询比较简单,不会可以留言。这个文件主要作用是index.html证书信任使用,有了这个可以自动跳转到手机信任页面,不需要用户查询怎么去信任

January 5, 2019 · 1 min · jiezi

个数是如何用大数据做行为预测的?

“个数”是“个推”旗下面向 APP 开发者提供数据统计分析的产品。“个数”通过可视化埋点技术及大数据分析能力从用户属性、渠道质量、行业对比等维度对 APP 进行全面的统计分析。“个数”不仅可以及时统计用户的活跃、新增等,还可以分析卸载用户的成分、流向,此外还能实现流失、付费等用户关键行为的预测,从而帮助 APP 开发者实现用户精细化运营和全生命周期管理。其中很值得一提的是,“个数”在“可视化埋点”及“行为预测”方面的创新,为 APP 开发者在实际运营中带来了极大便利,所以,在下文中,我们也将围绕这两点做详细的分析。可视化埋点埋点是指在产品流程的关键部位植入相关统计代码,以追踪用户行为,统计关键流程的使用程度,并将数据以日志的方式上报至服务器的过程。目前,数据埋点采集模式主要有代码埋点、无埋点、可视化埋点等方式。“代码埋点”是指在监控页面上加入基础 js,根据需求添加监控代码,它的优点是灵活,可以自定义设置,可以选择自己需要的数据来分析,但对复杂网站来说,每次修改一个页面就得重新出一份埋点方案,成本较大。目前,采用这种埋点方案的代表产品有百度统计、友盟、腾讯云分析、Google Analytics 等。“可视化埋点”通常是指开发者通过设备连接用户行为分析工具,直接在数据接入管理界面上对可交互且交互后有效果的页面元素(如:图片、按钮、链接等)进行操作实现数据埋点,下发采集代码生效回数的埋点方式。目前,可视化埋点的代表产品有个数、Mixpanel、神策数据等。“无埋点”与“全埋点”相似,它的原理是“全部采集,按需选取”,也就是说它可以对页面中所有交互元素的用户行为进行采集,它是先尽可能多收集检测页面的内容,然后再通过界面配置决定分析哪些数据,但它是标准化采集,如果需要设置自定义的采集方式仍需要代码埋点助力。这种方案的代表产品有 GrowingIO、数极客、百度统计等。“个数”为什么会选用可视化埋点?当下移动互联网正处于高速发展且发展形势瞬息万变的阶段中,开发者需要及时根据大数据的分析、反馈,对业务功能等做出调整,在传统的操作模式中,如果想要了解不同节点的数据,就要修改相应代码里面的埋点,然后测试发布,之后再在应用商店审核、上线,整个周期可能长达几个星期,这显然无法满足业务的需求。所以,“个数”采用的“可视化埋点”技术就是为了帮助开发者解决这个问题的。“个数”的可视化埋点灵活、方便,不需对数据追踪点添加任何代码,使用者只需要通过设备连接管理台,对页面可埋点的元素圈圈点点,即可添加随时生效的界面追踪点,同时在数据采集模式及数据分析能力上,“个数”能够提供给开发者们准确的、有效的数据。可视化埋点主要具有以下特性:1、零代码,无需代码,节省成本2、免更新,新增便捷,无需升级3、易测试,圈选测试,实时呈现换而言之,可视化埋点不仅可以节约企业成本,还可以提高开发人员和运营人员的工作效率。行为预测“个数”的行为预测主要包括流失预测、卸载预测、付费预测等,它的原理是基于 App 历史行为数据构建算法模型预测用户关键行为,从而帮助开发者达到用户精细化运营和全生命周期管理的目的。在这里需要注意的是,“个数”的行为预测与电商平台常用的个性化推荐不同,后者主要是基于用户近期的行为,如浏览记录、购买记录而分析出用户可能需要的东西,而“个数”是基于 App 各渠道卸载数、卸载趋势等指标的综合分析,更多的是对人群的聚类分析,而非仅仅基于个人的行为。行为预测的步骤据“个推”大数据科学家朱金星介绍,“个数”的行为预测主要分为以下几个步骤:1、找样本,主要从历史数据库中抽取;2、特征抽取,将用户与数据库打通,做匹配;3、特征筛选,保留相关性高的或有价值的特征;4、模型训练,将保留下来的特征放到模型中训练,在模型的选用上,“个数”主要用了逻辑回归,逻辑回归的模型相对深度学习等其他模型来说,简单一些,而且在特征筛选上相对好处理,得到的结果好解释,也相对稳定。5、参数优化,根据效果进行调整,如果结果不理想,即可返回调整参数重新走一次以上流程。实例分析下面我们以付费预测为例,为大家梳理一下具体的实现过程。个数付费预测的流程主要包括以下几点:1、目标问题分解明确需要进行预测的问题即付费预测,以及未来一段时间的跨度。2、分析样本数据(1)提取出所有用户的历史付费记录;(2)分析付费记录,了解付费用户的构成,比如年龄层次、性别、购买力和消费的产品类别等;(3)提取非付费用户的历史数据,这里可以根据产品的需求,添加条件、或无条件地进行提取,比如提取活跃并且非付费用户,或者不加条件地直接进行提取;(4)分析非付费用户的构成。3、构建模型的特征(1)原始的数据可能能够直接作为特征使用;(2)有些数据在变换后,才会有更好的使用效果,比如年龄,可以变换成少年、中年、老年等特征;(3)交叉特征的生成,比如“中年”和“女性”两种特征,就可以合并为一个特征进行使用。4、计算特征的相关性(1)计算特征饱和度,进行饱和度过滤;(2)计算特征 IV、卡方等指标,用以进行特征相关性的过滤。5、选用逻辑回归进行建模(1)选择适当的参数进行建模;(2)模型训练好后,统计模型的精确度、召回率、AUC 等指标,来评价模型;(3)如果觉得模型的表现可以接受,就可以在验证集上做验证,验证通过后,进行模型保存和预测。6、预测加载上述保存的模型,并加载预测数据,进行预测。7、监控最后,运营人员还需要对每次预测的结果进行关键指标监控,及时发现并解决出现的问题,防止出现意外情况,导致预测无效或预测结果出现偏差。其他场景如流失预测、卸载预测等,在流程上与付费预测类似,所以在这里就不再一一介绍了。有了精准的行为预测,运营者则可以将运营目标进行拆分、细化,具体到每个场景、每个流程,针对不同用户采取不同的推广渠道、运营策略。例如基于流失预测,运营者能够提前洞察到用户流失行为,提早进行干预,通过个性化内容推荐、消息推送等运营手段对即将流失的用户进行挽留,从而降低流失率。总的来说,在大数据行为预测的帮助下,运营者能够更及时、更全面地了解用户,从而达到精细化运营的目的。关于未来接下来“个数”还将在商品推荐等领域做更多的探索,例如开发精准的推荐技术等,也会不断挖掘大数据的潜力,结合反馈的数据做进一步的优化,围绕客户提供的样本数据做更深入的训练学习等,为开发者提供更全面的大数据服务,大家敬请期待。

January 4, 2019 · 1 min · jiezi

前戏,啥是像素?

我们说的分辨率,比如iphone4的分辨率是 960 * 640 —这个是单位是点,标识设备屏幕上有多少显示单元,每个显示单元,可以理解一个个物理的发光二极管iphone4的尺寸是3.5in,说的是物理的尺寸,对角线的长度,对应我们常说的物理单位,cm,m这种。思考一下,是不是我的分辨率越高,就显示的越清晰?NO, 分辨率高未必清晰,我都见过那种很大led广告屏,分辨率高啊,几千几万,但是仍然能看到明显的颗粒度。在仔细一想,其实清晰不清晰,主要看的是单位密度(ppi)而不是设备总像素的多少。是不是单位密度越高,显示的细节越清晰啊。有没有发现,所有的这些都是和具体的设备挂钩的。谈像素谈尺寸,都是离不开具体的设备。但是有个问题,为啥又引入dp的概念,独立设备像素呢。物理像素对应设备像素不是挺好?好吗?对于开发人员来说,设备千千万。分辨率千差万别。你想一下,如果同样3.5寸的屏幕上,我有个列表宽度都是100%,高度我设置 20你会发现,分辨率高的屏幕上,高度特别小,宽度特别长。反倒是分辨率高的显示的不清晰(跟蚂蚁一样,密度太高,物理上看起来就小的很)这可咋整啊。设备厂商也郁闷了,草尼玛,老子辛辛苦苦提高分辨率,出力不讨好啊。有啥办法,看起来物理上的高度和宽度都一致,但是更清晰的。有没有什么办法解决啊!于是大家一合计,你是ppi高吗?那我定义一个单位,这就引出的独立设备像素,顾名思义,就是独立于设备的像素。我就定义一个逻辑单位,dpdp有多大呢,dp就有小指头的上方的一小块这么大(笑笑)。你们呢都,尽量往上靠,高的你就在设备层面转化物理像素的时候,放大一下。低的呢,你就缩小一点。这样看起来,大家是不是差不多大了。对于开发人员来说,只要设置的两个屏幕逻辑像素相同,它们的显示效果就是相同的。是不是很爽。大家一总结,发现把设备按照ppi分一下,大概是这么个比例:ldpi [0.75倍]mdpi [1倍]hdpi [1.5倍]xhdpi [2倍]xxhdpi [3倍]xxxhdpi [4倍]不难发现,真正决定显示效果的,是逻辑像素尺寸(独立设备像素)。有人问了,我一个前端懂这些有卵子用。移动端页面的绝对单位就是px啊,我设置dp也不支持啊(草!!!为啥不支持dp?)px和dp有啥关系?你想啊,浏览器也是设备上的应用,也是按照设备的缩放比缩放的。具体可以对照一下, 缩放比:1dp=1px(mdpi、iPhone 3gs)1dp=1.5px(hdpi)1dp=2px(xhdpi、iPhone 4s/5/6)1dp=3px(xxhdpi、iPhone 6)1dp=4px(xxxhdpi)你再想想,不支持dp也行啊,你不是不支持吗?老子自己造,自己的轮船自己造(哈哈!笑)既然我都知道缩放比了,老子自己换算一下不就ok了。嗯,对了,就你最聪明!!哈哈,所以就有了rem的解决方案。ps: 如何和设计沟通?单位决定了我们的思考方式。在设计和开发过程中,应该尽量使用逻辑像素尺寸来思考界面。设计Android应用时,有的设计师喜欢把画布设为1080×1920,有的喜欢设成720×1280。给出的界面元素尺寸就不统一了。Android的最小点击区域尺寸是48x48dp,这就意味着在xhdpi的设备上,按钮尺寸至少是96x96px。而在xxhdpi设备上,则是144x144px。无论画布设成多大,我们设计的是基准倍率的界面样式,而且开发人员需要的单位都是逻辑像素。所以为了保证准确高效的沟通,双方要以逻辑像素尺寸来描述和理解界面。

January 4, 2019 · 1 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

解密!如何让别人不由自主的答应你的要求

北京历途科技有限公司是一家专注于人工智能与机器人研发的高新技术企业,经过十几年的技术积累,现已自主研发出专业、高效、安全的高楼外墙清洗机器人,填补了国际外墙清洁市场智能化产品空白。公司以"人人皆创客,完美做产品"为发展理念,打造具有颠覆性的科技产品。元旦小长假已经过去了,小伙伴们的心是不是已经都回到了工作上呢?新的一年新的征程,这是2019年的第一个周,相信在元旦零点的时候好多人都和小编一样,在这个最美好的时间里许下了最真诚的心愿,不管是为自己还是为别人,不管是工作的还是生活的,希望在2019年里都能实现。那么许下的愿望如何才能实现呢?今天,小编给大家推荐一本有“影响力教父”之称的美国作家罗伯特.西奥迪尼的书《影响力》。虽然这本书看着好像都是在讲营销人员的一些技巧和陷阱,但是当我们读完之后就会发现它其实是为我们解释了“为什么有些人极具说服力,而我们总是不由自主地答应他们的要求”这样一个事实。当我们也可以拥有这种让人顺从的能力之后,我们的愿望又何愁不能实现呢?所以说这本书不仅对营销人员重要,对我们每个人也同样重要。如何才能让别人不由自主的顺从自己?小编送你6大心理秘籍。1、互惠中国有一句俗语“吃人家的嘴软,拿人家的手短”。互惠原理其实就和这句俗语不谋而合。如果人家给了我们某种好处,我们在潜意识里就会找机会以同等价值或其喜欢的礼物回报他人的恩惠,这就是互惠原理的影响力。这种负债感会引导我们对此不能无动于衷,否则就会被贴上忘恩负义的标签,因为人们对那些只知索取不知回报的人是由衷厌恶的。所以通常情况下,人们在有负债感时会比没有负债感时更容易答应别人的请求。同理,如果想增加自己的影响力,互惠原理可以为我们增加很多优势。这又正好和另一句俗语不谋而合了“吃亏是福”,也就是平时的工作和生活中,不计回报的多付出、多帮助别人,当你有需要帮助的时候,别人就会义无反顾的回报你之前的恩惠。2、承诺和一致莱昂纳多.达.芬奇说过“在开始的时候拒绝总比在最后拒绝容易得多。”承诺和一致原理认为:一旦我们做出了某个决定,或选择了某种立场,就会面对来自个人和外部的压力迫使我们的言行与它保持一致。为什么我们会有如此大的动力去保持一致呢?因为在我们的潜意识里我们会认为做事前后一致的人就应该受到尊重,而那些前后不一的人则会被人们认为是一种不好的品行。所以如果我们想要增加自己的影响力,就应该要尽量做到让别人保持前后一致。比如在销售东西的时候多问几个让客户可以打消退货念头的问题;比如某个人在做出承诺的时候可以尽量让他有一个书面说明。因为书面声明很容易被公之于众,而且比口头承诺需要付出的努力更多,所以书面说明的影响力更大。3、社会认同社会认同实际上就是从众行为。只不过这个从众行为不是单一的一个人或是一小部分人的从众行为,而是大规模的社会从众行为。社会认同原理认为:我们进行是非判断的标准之一就是看别人是怎么想的,尤其是当我们要决定什么是正确的行为时。因为根据大众的经验去做事,往往可以使我们少犯很多错误。所以它为我们的思考和行动提供了一条捷径。正如为什么广告商总喜欢告诉我们哪种商品的销量最高一样。因为当我们知道这件商品的销量高的时候,我们便会认为既然大部分的人都在买的东西,那么质量一定不错。我们参照别人的行为来决定自己的行为。其实这并不是毫无道理的。因为多数人都去做的事情往往都是正确的事情。所以当我们想让别人认同我们的时候,我们可以尽量做一些在社会上都是被认可的事情,这样别人的顾虑便会减少,社会认同的影响力也会促使他认同我们的想法。4、喜好人们总是愿意答应自己认识和喜爱的人提出的要求,这应该是很自然的事,没有谁会对此感到惊讶。这条原理也常常被一些想要我们答应他们要求的陌生人所使用。他们可能和我们的穿着,说话语气很相似,他们可能和我们有着一样的兴趣爱好。这时我们的喜好心理便起到作用,因为我们总是更容易接受和自己有共同爱好的人,也更喜欢和自己有相似度的人做朋友。这时,我们便可以借此来提高自己的影响力,我们可以通过抓住他人的喜好,或者他喜欢的人的喜好来提高自己的影响力。但是这里有一条原则就是我们不能通过这种方式去做一些违背法律,违背道德的事。5、权威权威所具有的强大力量会影响我们的行为,即使是具有独立思考能力的成年人也会为了服从权威的命令而做出一些让人意想不到的事来。因为我们一出生,便被告知服从权威是应该的,违抗权威则是错误的。这个信息伴随着我们一生,所以当一些有权威的人说出一些话,或者让我们去干一些事的时候,我们便会义无反顾的去服从。所以当我们的实际地位,或者在人们心里的地位有了一定的分量的时候,他们便会义无反顾的去按我们的要求做。从而使我们的影响力得到提升。6、短缺“机会越少,价值就越高” 的短缺原理会对我们的行为造成全面的影响,害怕失去某种东西的想法比希望得到同等价值东西的想法对人们的激励作用更大。近几年的房价不断攀升,除了人为炒高房价的原因之外,也因为有人宣称我国的土地将出现长期短缺。在我们的潜意识里总会认为“物以稀为贵”,正因为这个原因激起了人们购买的欲望。我们总会觉得难以得到的东西比容易得到的东西更好,所以我们便会根据获得东西的难易程度来判断质量的高低。如果我们想让别人同意自己的看法或者答应自己的请求,我们就尽量让别人觉得这个想法是多么的来之不易,是集合了多少人的智慧,是多少人都无法想象到的。当我们把这些都阐述明白的时候,相信没有几个人可以对这样的想法拒之门外。当你读过这本书之后,你一定能有所感悟,运用这6大心理秘籍就能让自己比以前更具影响力。2019年刚刚开端,让我们一起加油,这一年大展宏图,走在创新的路上,点燃灵感的火花,收获成功的喜悦,离梦想更近一步。想获得《影响力》电子版书籍的小伙伴们,关注公众号,回复“影响力”就可收到啦,感谢您的阅读。

January 4, 2019 · 1 min · jiezi

概念详述:一对多直播与多对多互动直播该如何区分?

对于刚接触音视频技术的开发者而言,理清这四个概念需要一些时间,让我们通过一组简单的示意图以及关键词来了解这几个概念:直播:(一对多,RTMP/HLS/HTTP-FLV,CDN)直播是一种非常典型的流媒体系统,通常会分为推流端(Pusher)、拉流端(或者叫播放端,Player)以及直播流媒体中心(直播源站),通常会使用CDN进行直播的分发,因此大部分情况下使用的是通用标准的协议,如RTMP,而经过CDN分发后,播放时一般可以选择RTMP、HTTP-FLV或HLS(H5支持)等方式。直播的特点是只有一个推流端,以及多个的观看端。实时音视频:(双人/多人通话,UDP私有协议,低延时)实时音视频(Real-Time Communication, RTC)主要应用场景是音视频通话,技术关注点是低延时通信,因而使用基于UDP的私有协议,其延迟可低于100ms,适用于双人通话或是多人群组群话,典型的场景就是QQ电话、微信电话。 腾讯云实时音视频(TRTC)覆盖各平台,除了iOS/Android/Windows之后,还支持小程序以及 WebRTC 互通,并且支持通过云端混流的方式将画面旁路直播出去。当业务对延迟敏感,通话场景要求比较高,或是需要小程序或者 H5 场景下的双人或多人音视频通话可以选择实时音视频 TRTC。互动直播:(连麦,二对多/多对多,私有协议+标准协议,DC/OC+CDN)互动直播是在实时音视频的基础上,将实时音视频某个房间中的画面经云端混流后,通过旁路直播的方式直播出来。因此,互动直播主播与连麦者之间延迟与实时音视频一致,而主播/连麦者与普通观众之间的延时则与普通直播相同。旁路直播(关键词:云端混流,转推,CDN)将主/副播实时音视频通话时的整个房间的画面复制一份到云端进行云端混流,并将混流后的画面推流给腾讯云直播系统的工作方式。 因为混流后的视频数据流和主/副播通话房间实际上并不是同一路流,而是在另外平行的一路,因而称为旁路,即不在主路。云端录制时,录制的流也是通过旁路的方式从流媒体中心引出,存到COS中。

January 3, 2019 · 1 min · jiezi

PWA--未来式app

本文是PWA科普文,不涉及技术,望大佬勿喷。什么是PWA应用PWA(Progressive Web Apps 的简称,译作渐进式 Web App),是 Google 在 2015 年推出的一个项目,旨在将 Web 网页服务具备类似原生 Apps 的使用体验。PWA应用兼有网页应用和原生app的优点,能给用户带来原生应用一样的体验,同时又能避免原生应用体积过大,滥用权限,频繁更新等问题。PWA应用通过网页加载,同时也能使用service worker实现离线存储和使用。用户无需像原生app那样下载安装PWA,只需打开相应网页即可通过浏览器一键添加到桌面。下次可通过桌面图标进入应用,操作逻辑和原生app一样,只是不用下载,安装。同时PWA应用也是跨平台的,无论是在iOS,安卓还是windows phone下都有一致的用户体验。如何添加PWA应用 iOS: 使用Safari打开相应app网页,点击分享按钮,选择添加到主屏幕即可。 安卓:目前只有chrome(谷歌)浏览器完全支持PWA,点击右上角三个点的按钮,然后选择添加到主屏幕即可。 哪里能找到PWA应用 国外有很多PWA应用商店,这里推荐一个https://pwa.rocks/ 国内有https://www.pwaappstore.cn ,需要使用手机打开这个站点,电脑打开是404

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

我的2018年终总结

2018年即将结束,想一想自己也应该写一个年终总结,来回顾一下这一年的经历,而且去年就没有写,今年再不写也太不应该了。打开笔记大概翻了一下年初自己定的年度计划,大概只完成了60%左右,只能说还凑合吧。作为一个 iOS Developer,今年的总结,我先自我总结一下,然后再聊一下 iOS 开发的形势。我的总结我自己的计划表,就不拿出来献丑了。大概总结一下吧,我的 2018 的计划,大概分为四个部分。技术进步 ❎这个包括了 iOS、前端两个部分,整体来说完成的不是很满意,有不少具体的学习目标都没有达成。顺利完成 ionic 那本书 ✅写书真的是一件很痛苦的事,每天赶稿子,很累很累。好在是完成了。上架自己的 iOS 应用 ❎买了自己的开发者账号,打算自己设计、开发一个 iOS App。这个经历十分坎坷,当时我开发了一半左右,电脑坏了重装系统后代码全没了,花了一个月才把之前的代码全赶回来。我一下子有点能理解稿子被偷的谈迁得心情了,虽然我这个损失还没有那么大。但是好不容易完成后,被拒了三次!!!后面因为要写书,一直就搁置了。日语等级考试 N3 ✅去参加了 12 月的考试,得明年 1 月底才能出成绩,感觉应该能过吧。学日语我没报过班,我觉得作为一个程序员,自学能力必须要有,所以完全是自己看视频+教材自学过来的。不再受关注的 iOS 开发(正文)2013 年的时候,移动端开发相当火热,在那个时候的 iOS 开发能写个 UITableView 就能拿到不错的工资。但是所有事物都是盛极必衰的,今年 iOS 的行情不是很好,在简书、CocoaChina 等论坛经常看到寒冬、裁员等标题,所以我在今天还特地上了智联、Boos直聘等网站,统计了一下我所在城市招聘岗位的数量。大概搜索了一下,Android 的招聘数量大概是 iOS 的 2.5~3 倍左右,前端、Java 等岗位比安卓又多一些。为什么岗位变少通过招聘网站搜索的结果,我来大概分析一下 iOS 岗位变少的原因。大环境整体不好。资本寒冬,今年斗鱼、知乎、滴滴等众多公司都有裁员的信息。替代技术的出现。很多小公司招聘要求掌握 RN、ionic、Weex 等技术,还有新出正式版的 Flutter,这些技术也可以开发 App。iPhone 市场占有率逐步下滑。用户少了,有一些公司开发的软件可能直接不做 iOS 版了。我们该怎么做下面分享一个解决方案,不一定适合每一个人,仅供参考。健身没错,是健身。自从每天锻炼身体后,感觉自己更加精力充沛了,再也不是一到家就不想动的状态。有了更多的时间,不管是用来学习还是做别的都挺好的。深入学习计算机基础知识数据结构、操作系统、编译原理、计算机网络这些基础知识属于内功,会永远伴随着你。另一方面,对它们的掌握程度,也决定了你的上限。学习其他语言以目前 iOS 的发展情况来看,学习其他语言未尝不可。如果你的 iOS 技术很好,转其他的语言也是很快的。不管怎么说,有过硬的技术心里才踏实。最后很多程序员每天都在面对焦虑,技术的不断更迭,导致不得不跟着学习新的技术。本来学习就是一件违反人性的事情,需要消耗很多的精力,如果所在的公司还需要频繁加班,拖着疲惫的身体回到家里,基本就没有学习动力了。不管怎么样,身体才是革命的本钱。今年也听说过一些程序员猝死的新闻,所以在熬夜敲代码、学习的同时,也请多注意自己的身体健康。就到这里吧,感谢阅读。❤️

January 1, 2019 · 1 min · jiezi

通过视图控制器容器和子视图控制器避免庞大的视图控制器

首发于:【译】通过视图控制器容器和子视图控制器避免庞大的视图控制器通过视图控制器容器和子视图控制器避免庞大的视图控制器视图控制器容器和子视图控制器图解View Controller 是一个提供基本构建块的组件,在 iOS 开发中我们以它为基础构建应用。在 Apple MVC 世界中,它作为 View 和 Model 的中间人,在两者之间充当协调者的角色。它以观察者控制器开始,响应模型更改、更新视图、使用目标操作从视图中接受用户交互、然后更新模型。Apple MVC 图解(Apple 公司提供)作为一名 iOS 开发者,很多次我们将面临处理庞大的 View Controller 问题,即便我们使用了像 MVVM、MVP 或 VIPER 这样的架构。某些时刻,View Controller 在一个屏幕上承担了太多职责。这违反了 SRP(单一职责原则),在模块之间形成了强度耦合,并使得重用和测试每个组件变得异常困难。我们可以将下面的应用截图作为示例。你可以看到在一个屏幕上至少存在 3 种职责:显示电影列表;显示可以选择应用于电影列表的过滤列表;清除所选过滤器的选项。如果我们准备使用单一的 View Controller 来构建此屏幕,由于它在一个 view controller 中承担了过多职责,因此可以保证这个 view controller 将变得非常庞大和臃肿。我们如何解决这个问题呢?其中一个解决方案是使用 View Controller 容器和子 View Controller。以下是使用该方案的好处:将电影列表封装到 MovieListViewController 中,它只负责显示电影列表并对 Movie 模型中的更改做出响应。如果我们只想显示没有过滤器的电影列表,我们也可以在另一个屏幕中重用它。将过滤器中的列表和选择逻辑封装到 FilterListViewController 中,它单独负责显示和过滤器的选择。当用户选择和取消选择时,我们可以使用委托与父 View Controller 进行通信。将主 View Controller 缩减为一个 ContainerViewController,它只负责将选中的过滤器从过滤列表应用到 MovieListViewController 中的 Movie 模型。它还设置布局并将子 view controller 添加到容器视图中。你可以在下面的 GitHub 代码仓库中查看完整的项目源代码。alfianlosari/Filter-MVC-iOS使用 Storyboard 来布置 View Controller使用 Storyboard 来布置 View ControllerContainerViewController:View Controller 容器提供了 2 个容器视图,用于将子 View Controller 嵌入到水平 UIStackView 中。它还提供了单个 UIButton 来清空所选的过滤器。它还嵌入在充当初始 View Controller 的 UINavigationController 中。FilterListMovieController:它是 UITableViewController 的子类,具有分类样式和一个用来显示过滤器名称的标准单元格。它还分配了 Storyboard ID,因此可以通过编程的方式在 ContainerViewController 中对它进行实例化。MovieListViewController:它是 UITableViewController 的子类,具有 Plain 样式和一个用来显示 Movie 属性的小标题单元格。它还跟 FilterListViewController 一样分配了 Storyboard ID。电影列表 View Controller此 view controller 负责显示作为实例公开属性的 Movie 模型列表。我们使用 Swift 的 didSet 属性观察器来响应模型的更改,然后重新加载 UITableView。单元格使用默认小标题样式 UITableViewCellStyle 来显示电影的标题、持续时间、评级和流派。import UIKitstruct Movie { let title: String let genre: String let duration: TimeInterval let rating: Float}class MovieListViewController: UITableViewController { var movies = Movie { didSet { tableView.reloadData() } } let formatter: DateComponentsFormatter = { let formatter = DateComponentsFormatter() formatter.allowedUnits = [.hour, .minute] formatter.unitsStyle = .abbreviated formatter.maximumUnitCount = 1 return formatter }() override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return movies.count } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: “Cell”, for: indexPath) let movie = movies[indexPath.row] cell.textLabel?.text = movie.title cell.detailTextLabel?.text = “(formatter.string(from: movie.duration) ?? “”), (movie.genre.capitalized), rating: (movie.rating)” return cell }}过滤器列表 View Controller过滤器列表在 3 个单独的部分中显示 MovieFilter 枚举:流派、评级和持续时间。MovieFilter 枚举本身符合 Hashable 协议,因此可以使用每个枚举及其属性的哈希值存储在唯一集合中。过滤器的选择存储在包含 MovieFilter 的 Set 的实例属性下。要与其他对象通信,通过 FilterListControllerDelegate 使用委托模式,委托有三个方法需要实现:选择一个过滤器。取消选择一个过滤器。清空所有已选择过滤器。import UIKitenum MovieFilter: Hashable { case genre(code: String, name: String) case duration(duration: TimeInterval, name: String) case rating(value: Float, name: String) var hashValue: Int { switch self { case .genre(let code, let name): return “(code)-(name)".hashValue case .rating(let value, let name): return “(value)-(name)".hashValue case .duration(let duration, let name): return “(duration)-(name)".hashValue } }}protocol FilterListViewControllerDelegate: class { func filterListViewController(_ controller: FilterListViewController, didSelect filter: MovieFilter) func filterListViewController(_ controller: FilterListViewController, didDeselect filter: MovieFilter) func filterListViewControllerDidClearFilters(controller: FilterListViewController)}class FilterListViewController: UITableViewController { let filters = MovieFilter.defaultFilters weak var delegate: FilterListViewControllerDelegate? var selectedFilters: Set<MovieFilter> = [] override func viewDidLoad() { super.viewDidLoad() } func clearFilter() { selectedFilters.removeAll() delegate?.filterListViewControllerDidClearFilters(controller: self) tableView.reloadData() } override func numberOfSections(in tableView: UITableView) -> Int { return filters.count } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return filters[section].filters.count } override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { return filters[section].title } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { tableView.deselectRow(at: indexPath, animated: true) let filter = filters[indexPath.section].filters[indexPath.row] if selectedFilters.contains(filter) { selectedFilters.remove(filter) delegate?.filterListViewController(self, didDeselect: filter) } else { selectedFilters.insert(filter) delegate?.filterListViewController(self, didSelect: filter) } tableView.reloadRows(at: [indexPath], with: .automatic) } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: “Cell”, for: indexPath) let filter = filters[indexPath.section].filters[indexPath.row] switch filter { case .genre(, let name): cell.textLabel?.text = name case .rating(, let name): cell.textLabel?.text = name case .duration(, let name): cell.textLabel?.text = name } if selectedFilters.contains(filter) { cell.accessoryType = .checkmark } else { cell.accessoryType = .none } return cell }}在容器 View Controller 中集成在 ContainerViewController 中,我们有以下几个实例属性:FilterListContainerView 和 MovieListContainerView: 用于添加子 view controller 的容器视图。FilterListViewController 和 MovieListViewController:使用 Storyboard ID 实例化的影片列表和筛选器列表 view controller 的引用。movie:使用默认硬编码的电影实例的 Movie 数组。当 viewDidLoad 被调用时,我们调用该方法来设置子 View Controller。以下是它要执行的几项任务:使用 Storyboard ID 实例化 FilterListViewController 和 MovieListViewController;将它们分配给实例属性;将 MovieListViewController 分配给 movies 数组;将 ContainerViewController 指定为 FilterListViewController 的委托,以便它可以响应过滤器选择;设置子视图框架并使用扩展帮助方法将它们添加为子 View Controller。对于 FilterListViewControllerDelegate 的实现,当选择或取消选择过滤器时,将针对每个类型、评级和持续时间过滤默认的电影数据。然后,过滤器的结果将分配给 MovieListViewController 的 movies 属性。要取消选择所有过滤器,它只会分配默认的电影数据。import UIKitclass ContainerViewController: UIViewController { @IBOutlet weak var filterListContainerView: UIView! @IBOutlet weak var movieListContainerView: UIView! var filterListVC: FilterListViewController! var movieListVC: MovieListViewController! let movies = Movie.defaultMovies override func viewDidLoad() { super.viewDidLoad() setupChildViewControllers() } private func setupChildViewControllers() { let storyboard = UIStoryboard(name: “Main”, bundle: nil) let filterListVC = storyboard.instantiateViewController(withIdentifier: “FilterListViewController”) as! FilterListViewController addChild(childController: filterListVC, to: filterListContainerView) self.filterListVC = filterListVC self.filterListVC.delegate = self let movieListVC = storyboard.instantiateViewController(withIdentifier: “MovieListViewController”) as! MovieListViewController movieListVC.movies = movies addChild(childController: movieListVC, to: movieListContainerView) self.movieListVC = movieListVC } @IBAction func clearFilterTapped( sender: Any) { filterListVC.clearFilter() } private func filterMovies(moviesFilter: [MovieFilter]) { movieListVC.movies = movies .filter(with: moviesFilter.genreFilters) .filter(with: moviesFilter.ratingFilters) .filter(with: moviesFilter.durationFilters) }}extension ContainerViewController: FilterListViewControllerDelegate { func filterListViewController(_ controller: FilterListViewController, didSelect filter: MovieFilter) { filterMovies(moviesFilter: Array(controller.selectedFilters)) } func filterListViewController(_ controller: FilterListViewController, didDeselect filter: MovieFilter) { filterMovies(moviesFilter: Array(controller.selectedFilters)) } func filterListViewControllerDidClearFilters(controller: FilterListViewController) { movieListVC.movies = Movie.defaultMovies }}结论通过研究示例项目。我们可以看到在我们的应用中使用 View Controller 容器和子 View Controller 的好处。我们可以将单个 View Controller 的职责划分为单独的 View Controller,它们只具有单一职责(SRP)。我们还需要确保子 View Controller 对其父级没有任何依赖。为了让子 View Controller 与父级进行通信,我们可以使用委托模式。该方法还提供了模块松耦合的优点,这可以为每个组件带来更好的可重用性和可测试性。随着我们的应用变得更大、更复杂,该方法确实有助于我们扩展它。让我们继续学习????,祝你圣诞快乐????,新年快乐????!继续使用 Swift 和 Cocoa !!????在社交平台上关注我们:Facebook: facebook.com/AppCodamobile/Twitter: twitter.com/AppCodaMobileInstagram: instagram.com/AppCodadotcom ...

December 29, 2018 · 4 min · jiezi

好记性不如烂笔头,极光向你发出征文邀请函

由极光 1举办的征文大赛 ✍️️ ——「我和极光的那些事儿」第三届如约而至!过完双十一和双十二,是不是特想剁掉那只买买买的手?千万别 手觉得 ta 还能抢救一下 留着 ta 参赛写文章,赢奖品给你回回血 我们准备了 Filco、Kindle、电动牙刷等诸多大奖,等你带回家 参赛规则:时间:征文截止:2019.01.21公布榜单:2019.01.25标题:极光征文 | ✘✘✘(文章正题)例:极光征文 | 如何用 IMUI 构建你的聊天界面内容:和「极光」有关的文章数据分析、技术、运营、产品、设计、读后感类文章皆可,不知道怎么下笔可以参考文末的写作思路字数 500 字以上、排版美观(建议使用 Markdown 格式)在文末附上「本文为极光征文参赛文章」评定:由极光官方综合评定评定指标:文章质量(重要)、阅读量、点赞数、评论数投稿方式:在「简书」平台发布参赛文章投稿至「极光征文」专题奖品:一等奖:价值 1000 元的 Filco 机械键盘 ,共计 1 名二等奖:价值 600 元的 Kindle 阅读器 ,共计 2 名三等奖:价值 300 元的 Philips 电动牙刷 ,共计 3 名优胜奖:价值 150 元的 极光背包,共计 5 名参与奖 1 :极光减压马克杯(含一套彩笔);参与奖 2 :极光多款周边礼品任选其二。注:参与奖任选其一,参与即有。参与征文的小伙伴均可参与一次抽奖,极光精美周边随心抽。写作思路:一、围绕极光开发者服务和开源项目极光开发者服务涵盖「推送、统计、IM、短信、认证、分享」六大产品,提供了 Cordova、React、Flutter 等丰富的开源插件,还开发了全平台支持的即时通讯 UI 库 「IMUI」。这么多产品,肯定有一款与你产生过一些不得不说的故事,如果不巧还没有,那么我希望现在就是我们故事的开端。可以围绕极光产品,说一说你的接入过程、使用心得、运营趣事,如果极光服务有为你的产品锦上添花或雪中送炭,求夸奖.gif二、围绕极光大数据报告,极光日报大数据报告:可靠、有影响力的数据报告,结合大样本算法开展数据挖掘和统计分析,数据源于极光云服务平台和极光 iAPP 长期对行业及各类 App 的监测采集极光日报:国外技术文章导读,内容涉及硅谷、科技、编程、设计、人工智能等领域,在知乎专栏和简书文集同步连载可以围绕一篇极光文章写读后感,或运用数据做现实问题分析,回忆一下阅读报告对你的工作与生活是否带来了什么帮助,写下你与极光报告的故事一些有趣的、有价值的文章推荐,可能对你的写作有帮助:年年双十一,今年哪些 App 和你一起过 | 2018 年双 11 专题研究报告生活不易,拒绝油腻 | 2018 年 10 月熟男群体研究报告华为销量领军,iPhone 忠诚度持续下跌 | 2018 年 Q3 智能手机行业研究报告班班相报何时了 | 2018 年 K12 教育用户群体研究报告如何在 2 分钟内入睡(二战时期美国飞行员训练法)你想学的一切,只需要这一个网站就够了我们在大学图书馆发现了三本有毒的书互动群:不知道怎么写?写了一半想放弃?想得到文章修改意见?为此我们建了个交流群扫码添加极小光微信拉你入「极光征文交流群」,激发你的灵感,还有随机抽奖给你写作动力声明:版权归原作者所有,极光拥有转载权本活动最终解释权归极光大数据所有 ...

December 29, 2018 · 1 min · jiezi

iOS 覆盖率检测原理与增量代码测试覆盖率工具实现

背景对苹果开发者而言,由于平台审核周期较长,客户端代码导致的线上问题影响时间往往比较久。如果在开发、测试阶段能够提前暴露问题,就有助于避免线上事故的发生。代码覆盖率检测正是帮助开发、测试同学提前发现问题,保证代码质量的好帮手。对于开发者而言,代码覆盖率可以反馈两方面信息:自测的充分程度。代码设计的冗余程度。尽管代码覆盖率对代码质量有着上述好处,但在 iOS 开发中却使用的不多。我们调研了市场上常用的 iOS 覆盖率检测工具,这些工具主要存在以下四个问题:第三方工具有时生成的检测报告文件会出错甚至会失败,开发者对覆盖率生成原理不了解,遇到这类问题容易弃用工具。第三方工具每次展示全量的覆盖率报告,会分散开发者的很多精力在未修改部分。而在绝大多数情况下,开发者的关注重点在本次新增和修改的部分。Xcode 自带的覆盖率检测只适用于单元测试场景,由于需求变更频繁,业务团队开发单元测试的成本很高。已有工具很难和现有开发流程结合起来,需要额外进行测试,运行覆盖率脚本才能获取报告文件。为了解决上述问题,我们深入调研了覆盖率报告的生成逻辑,并结合团队的开发流程,开发了一套嵌入在代码提交流程中、基于单次代码提交(git commit)生成报告、对开发者透明的增量代码测试覆盖率工具。开发者只需要正常开发,通过模拟器测试开发代码,commit 本次代码(commit 和测试顺序可交换),推送(git push)到远端,就可以在本地看到这次提交代码的详细覆盖率报告了。本文分为两部分,先从介绍通用覆盖率检测的原理出发,让读者对覆盖率的收集、解析有直观的认识。之后介绍我们增量代码测试覆盖率工具的实现。覆盖率检测原理生成覆盖率报告,首先需要在 Xcode 中配置编译选项,编译后会为每个可执行文件生成对应的 .gcno 文件;之后在代码中调用覆盖率分发函数,会生成对应的 .gcda 文件。其中,.gcno 包含了代码计数器和源码的映射关系, .gcda 记录了每段代码具体的执行次数。覆盖率解析工具需要结合这两个文件给出最后的检测报表。接下来先看看 .gcno 的生成逻辑。.gcno利用 Clang 分别生成源文件的 AST 和 IR 文件,对比发现,AST 中不存在计数指令,而 IR 中存在用来记录执行次数的代码。搜索 LLVM 源码可以找到覆盖率映射关系生成源码。覆盖率映射关系生成源码是 LLVM 的一个 Pass,(下文简称 GCOVPass)用来向 IR 中插入计数代码并生成 .gcno 文件(关联计数指令和源文件)。下面分别介绍IR插桩逻辑和 .gcno 文件结构。IR 插桩逻辑代码行是否执行到,需要在运行中统计,这就需要对代码本身做一些修改,LLVM 通过修改 IR 插入了计数代码,因此我们不需要改动任何源文件,仅需在编译阶段增加编译器选项,就能实现覆盖率检测了。从编译器角度看,基本块(Basic Block,下文简称 BB)是代码执行的基本单元,LLVM 基于 BB 进行覆盖率计数指令的插入,BB 的特点是:只有一个入口。只有一个出口。只要基本块中第一条指令被执行,那么基本块内所有指令都会顺序执行一次。覆盖率计数指令的插入会进行两次循环,外层循环遍历编译单元中的函数,内层循环遍历函数的基本块。函数遍历仅用来向 .gcno 中写入函数位置信息,这里不再赘述。一个函数中基本块的插桩方法如下:统计所有 BB 的后继数 n,创建和后继数大小相同的数组 ctr[n]。以后继数编号为序号将执行次数依次记录在 ctr[i] 位置,对于多后继情况根据条件判断插入。举个例子,下面是一段猜数字的游戏代码,当玩家猜中了我们预设的数字10的时候会输出Bingo,否则输出You guessed wrong!。这段代码的控制流程图如图1所示。- (void)guessNumberGame:(NSInteger)guessNumber{ NSLog(@“Welcome to the game”); if (guessNumber == 10) { NSLog(@“Bingo!”); } else { NSLog(@“You guess is wrong!”); }}例1 猜数字游戏 这段代码如果开启了覆盖率检测,会生成一个长度为 6 的 64 位数组,对照插桩位置,方括号中标记了桩点序号,图 1 中代码前数字为所在行数。图 1 桩点位置.gcno计数符号和文件位置关联.gcno 是用来保存计数插桩位置和源文件之间关系的文件。GCOVPass 在通过两层循环插入计数指令的同时,会将文件及 BB 的信息写入 .gcno 文件。写入步骤如下:创建 .gcno 文件,写入 Magic number(oncg+version)。随着函数遍历写入文件地址、函数名和函数在源文件中的起止行数(标记文件名,函数在源文件对应行数)。随着 BB 遍历,写入 BB 编号、BB 起止范围、BB 的后继节点编号(标记基本块跳转关系)。写入函数中BB对应行号信息(标注基本块与源码行数关系)。从上面的写入步骤可以看出,.gcno 文件结构由四部分组成:文件结构函数结构BB 结构BB 行结构通过这四部分结构可以完全还原插桩代码和源码的关联,我们以 BB 结构 / BB 行结构为例,给出结构图 2 (a) BB 结构,(b) BB 行信息结构,在本章末尾覆盖率解析部分,我们利用这个结构图还原代码执行次数(每行等高格代表 64bit):图2 BB 结构和 BB 行信息结构.gcda入口函数关于 .gcda 的生成逻辑,可参考覆盖率数据分发源码。这个文件中包含了 __gcov_flush() 函数,这个函数正是分发逻辑的入口。接下来看看 __gcov_flush() 如何生成 .gcda 文件。通过阅读代码和调试,我们发现在二进制代码加载时,调用了llvm_gcov_init(writeout_fn wfn, flush_fn ffn)函数,传入了_llvm_gcov_writeout(写 gcov 文件),_llvm_gcov_flush(gcov 节点分发)两个函数,并且根据调用顺序,分别建立了以文件为节点的链表结构。(flush_fn_node * ,writeout_fn_node * )__gcov_flush() 代码如下所示,当我们手动调用__gcov_flush() 进行覆盖率分发时,会遍历flush_fn_node *这个链表(即遍历所有文件节点),并调用分发函数_llvm_gcov_flush(curr->fn 正是__llvm_gcov_flush函数类型)。void __gcov_flush() { struct flush_fn_node *curr = flush_fn_head; while (curr) { curr->fn(); curr = curr->next; }}具体的分发逻辑观察__llvm_gcov_flush 的 IR 代码,可以看到:图3 __llvm_gcov_flush 代码示例__llvm_gcov_flush 先调用了__llvm_gcov_writeout,来向 .gcda 写入覆盖率信息。最后将计数数组清零__llvm_gcov_ctr.xx。而__llvm_gcov_writeout逻辑为:生成对应源文件的 .gcda 文件,写入 Magic number。循环执行llvm_gcda_emit_function: 向 .gcda 文件写入函数信息。llvm_gcda_emit_arcs: 向 .gcda 文件写入BB执行信息,如果已经存在 .gcda 文件,会和之前的执行次数进行合并。调用llvm_gcda_summary_info,写入校验信息。调用llvm_gcda_end_file,写结束符。感兴趣的同学可以自己生成 IR 文件查看更多细节,这里不再赘述。.gcda 的文件/函数结构和 .gcno 基本一致,这里不再赘述,统计插桩信息结构如图 4 所示。定制化的输出也可以通过修改上述函数完成。我们的增量代码测试覆盖率工具解决代码 BB 结构变动后合并到已有 .gcda 文件不兼容的问题,也是修改上述函数实现的。图4 计数桩输出结构覆盖率解析在了解了如上所述 .gcno ,.gcda 生成逻辑与文件结构之后,我们以例 1 中的代码为例,来阐述解析算法的实现。例 1 中基本块 B0,B1 对应的 .gcno 文件结构如下图所示,从图中可以看出,BB 的主结构完全记录了基本块之间的跳转关系。图5 B0,B1 对应跳转信息B0,B1 的行信息在 .gcno 中表示如下图所示,B0 块因为是入口块,只有一行,对应行号可以从 B1 结构中获取,而 B1 有两行代码,会依次把行号写入 .gcno 文件。图6 B0,B1 对应行信息在输入数字 100 的情况下,生成的 .gcda 文件如下:图7 输入 100 得到的 .gcda 文件通过控制流程图中节点出边的执行次数可以计算出 BB 的执行次数,核心算法为计算这个 BB 的所有出边的执行次数,不存在出边的情况下计算所有入边的执行次数(具体实现可以参考 gcov 工具源码),对于 B0 来说,即看 index=0 的执行次数。而 B1 的执行次数即 index=1,2 的执行次数的和,对照上图中 .gcda 文件可以推断出,B0 的执行次数为 ctr[0]=1,B1 的执行次数是 ctr[1]+ctr[2]=1, B2 的执行次数是 ctr[3]=0,B4 的执行次数为 ctr[4]=1,B5 的执行次数为 ctr[5]=1。经过上述解析,最终生成的 HTML 如下图所示(利用 lcov):图8 覆盖率检测报告以上是 Clang 生成覆盖率信息和解析的过程,下面介绍美团到店餐饮 iOS 团队基于以上原理做的增量代码测试覆盖率工具。增量代码覆盖率检测原理方案权衡由于 gcov 工具(和前面的 .gcov 文件区分,gcov 是覆盖率报告生成工具)生成的覆盖率检测报告可读性不佳,如图 9 所示。我们做的增量代码测试覆盖率工具是基于 lcov 的扩展,报告展示如上节末尾图 8 所示。图9 gcov 输出,行前数字代表执行次数,#### 代表没执行比 gcov 直接生成报告多了一步,lcov 的处理流程是将 .gcno 和 .gcda 文件解析成一个以 .info 结尾的中间文件(这个文件已经包含全部覆盖率信息了),之后通过覆盖率报告生成工具生成可读性比较好的 HTML 报告。结合前两章内容和覆盖率报告生成步骤,覆盖率生成流程如下图所示。考虑到增量代码覆盖率检测中代码增量部分需要通过 Git 获取,比较自然的想法是用 git diff 的信息去过滤覆盖率的内容。根据过滤点的不同,存在以下两套方案:通过 GCOVPass 过滤,只对修改的代码进行插桩,每次修改后需重新插桩。通过 .info 过滤,一次性为所有代码插桩,获取全部覆盖率信息,过滤覆盖率信息。图10 覆盖率生成流程分析这两个方案,第一个方案需要自定义 LLVM 的 Pass,进而会引入以下两个问题:只能使用开源 Clang 进行编译,不利于接入正常的开发流程。每次重新插桩会丢失之前的覆盖率信息,多次运行只能得到最后一次的结果。而第二个方案相对更加轻量,只需要过滤中间格式文件,不仅可以解决我们在文章开头提到的问题,也可以避免上述问题:可以很方便地加入到平常代码的开发流程中,甚至对开发者透明。未修改文件的覆盖率可以叠加(有修改的那些控制流程图结构可能变化,无法叠加)。因此我们实际开发选定的过滤点是在 .info 。在选定了方案 2 之后,我们对中间文件 .info 进行了一系列调研,确定了文件基本格式(函数/代码行覆盖率对应的文件的表示),这里不再赘述,具体可以参考 .info 生成文档。增量代码测试覆盖率工具的实现前一节是实现增量代码覆盖率检测的基本方案选择,为了更好地接入现有开发流程,我们做了以下几方面的优化。降低使用成本在接入方面,接入增量代码测试覆盖率工具只需一次接入配置,同步到代码仓库后,团队中成员无需配置即可使用,降低了接入成本。在使用方面,考虑到插桩在编译时进行,对全部代码进行插桩会很大程度降低编译速度,我们通过解析 Podfile(iOS 开发中较为常用的包管理工具 CocoaPods 的依赖描述文件),只对 Podfile 中使用本地代码的仓库进行插桩(可配置指定仓库),降低了团队的开发成本。对开发者透明接入增量代码测试覆盖率工具后,开发者无需特殊操作,也不需要对工程做任何其他修改,正常的 git commit 代码,git push 到远端就会自动生成并上传这次 commit 的覆盖率信息了。为了做到这一点,我们在接入 Pod 的过程中,自动部署了 Git 的 pre-push 脚本。熟悉 Git 的同学知道,Git 的 hooks 是开发者的本地脚本,不会被纳入版本控制,如何通过一次配置就让这个仓库的所有使用成员都能开启,是做好这件事的一个难点。我们考虑到 Pod 本身会被纳入版本控制,因此利用了 CocoaPods 的一个属性 script_phase,增加了 Pod 编译后脚本,来帮助我们把 pre-push 插入到本地仓库。利用 script_phase 插入还带来了另外一个好处,我们可以直接获取到工程的缓存文件,也避免了 .gcno / .gcda 文件获取的不确定性。整个流程如下:图11 pre-push 分发流程覆盖率累计在实现了覆盖率的过滤后,我们在实际开发中遇到了另外一个问题:修改分支/循环结构后生成的 .gcda 文件无法和之前的合并。 在这种情况下,__gcov_flush会直接返回,不再写入 .gcda 文件了导致覆盖率检测失败,这也是市面上已有工具的通用问题。而这个问题在开发过程中很常见,比如我们给例 1 中的游戏增加一些提示,当输入比预设数字大时,我们就提示出来,反之亦然。- (void)guessNumberGame:(NSInteger)guessNumber{ NSInteger targetNumber = 10; NSLog(@“Welcome to the game”); if (guessNumber == targetNumber) { NSLog(@“Bingo!”); } else if (guessNumber > targetNumber) { NSLog(@“Input number is larger than the given target!”); } else { NSLog(@“Input number is smaller than the given target!”); }}这个问题困扰了我们很久,也推动了对覆盖率检测原理的调研。结合前面覆盖率检测的原理可以知道,不能合并的原因是生成的控制流程图比原来多了两条边( .gcno 和旧的 .gcda 也不能匹配了),反映在 .gcda 上就是数组多了两个数据。考虑到代码变动后,原有的覆盖率信息已经没有意义了,当发生边数不一致的时候,我们会删除掉旧的 .gcda 文件,只保留最新 .gcda 文件(有变动情况下 .gcno 会重新生成)。如下图所示:图12 覆盖率冲突解决算法整体流程图结合上述流程,我们的增量代码测试覆盖率工具的整体流程如图 13 所示。开发者只需进行接入配置,再次运行时,工程中那些作为本地仓库进行开发的代码库会被自动插桩,并在 .git 目录插入 hooks 信息;当开发者使用模拟器进行需求自测时,插桩统计结果会被自动分发出去;在代码被推到远端前,会根据插桩统计结果,生成仅包含本次代码修改的详细增量代码测试覆盖率报告,以及向远端推送覆盖率信息;同时如果测试覆盖率小于 80% 会强制拒绝提交(可配置关闭,百分比可自定义),保证只有经过充分自测的代码才能提交到远端。图13 增量代码测试覆盖率生成流程图总结以上是我们在代码开发质量方面做的一些积累和探索。通过对覆盖率生成、解析逻辑的探究,我们揭开了覆盖率检测的神秘面纱。开发阶段的增量代码覆盖率检测,可以帮助开发者聚焦变动代码的逻辑缺陷,从而更好地避免线上问题。作者介绍丁京,iOS 高级开发工程师。2015 年 2 月校招加入美团到店餐饮事业群,目前负责大众点评 App 美食频道的开发维护。王颖,iOS 开发工程师。2017 年 3 月校招加入美团到店餐饮事业群,目前参与大众点评 App 美食频道的开发维护。招聘信息到店餐饮技术部交易与信息技术中心,负责点评美食用户端业务,服务于数以亿计用户,通过更好的榜单、真实的评价和完善的信息为用户提供更好的决策支持,致力于提升用户体验;同时承载所有餐饮商户端线上流量,为餐饮商户提供多种营销工具,提升餐饮商户营销效率,最终达到让用户“Eat Better、Live Better”的美好愿景!我们的团队包含且不限于 Android、iOS、FE、Java、PHP 等技术方向,已完备覆盖前后端技术栈。只要你来,就能点亮全栈开发技能树。诚挚欢迎投递简历至 wangkang@meituan.com。参考资料覆盖率数据分发源码覆盖率映射关系生成源码基本块介绍gcov 工具源码覆盖率报告生成工具 .info 生成文档 ...

December 28, 2018 · 3 min · jiezi

感慨!2018的历途居然做了这些

北京历途科技有限公司是一家专注于人工智能与机器人研发的高新技术企业,经过十几年的技术积累,现已自主研发出专业、高效、安全的高楼外墙清洗机器人,填补了国际外墙清洁市场智能化产品空白。公司以"人人皆创客,完美做产品"为发展理念,打造具有颠覆性的科技产品。看花开花落,云卷云舒。悄悄的,2018已经余额不足,2019正在向我们招手。就在前几天,红帽子、白胡子的老爷爷也如约而至。在我们的床头为我们每个人许下了最美好的祝愿。回顾过去的一年,虽然历途在前进的道路上没能一帆风顺,也遇到了不少的困难,但是历途人以“人人皆创客,完美做产品”的理念,一步一个脚印的实现着自己的梦想,同时也为全球的高楼外墙清洗行业突破着一个又个的难关。接下来,就让我们一起看一下历途在2018到底做了些什么?1、2018年4月,企业文化建设开始落地俗话说“没有规矩,不成方圆”,一个没有企业文化的公司就好像一个没有灵魂的人,寸步难行。企业文化是“企业的粘合剂”,可以把所有的员工紧紧地粘合在一起,使员工明确目的、步调一致。从2018年4月开始,历途的企业文化建设开始落地。比如每月读一本好书、每月举办一场生日趴、每年组织两次团队建设、给员工的妈妈准备母亲节礼物、员工共同回忆小时候和父母一起过的端午节等活动,温暖员工及其家人们。这些活动无不是在为我们营造更好的工作氛围,提高团队的凝聚力。它在无形中发挥了导向的作用,让员工自发的去遵守公司的规章制度,从而把公司和个人的意愿和远景统一起来,促使公司的发展壮大。2、2018年6月,第六代机器人开始在多栋楼宇进行清洗应用从以前的视频里我们可以看到第六代机器人共清洗了三种典型外立面。第一种是玻璃与玻璃之间凹槽宽2厘米的纯玻璃幕楼宇;第二种是帽檐30厘米的麻面石材+玻璃幕楼宇;第三种是窗台25厘米的铝板+麻面石材+玻璃窗户的楼宇 。机器人在骄阳似火的天气情况下依然可以正常工作,足以证明机器人耐高温的特性。再来看看机器人的清洁速度也是相当的溜,人工与之相比简直就是小巫见大巫。清洁效果更是没得说,一闪一闪亮晶晶的楼宇好像也在炫耀着自己洁净的身躯。第六代历途机器人显然已经在整体结构和程序方案上取得了巨大的成功。3、2018年8月,历途被评定为高新技术企业历途机器人拥有5项发明专利和20余项实用新型专利今年同时被评定为国家高新技术企业和中关村高新技术企业。产品主要功能包括三维智能空间建模、工作表面识别、高效高空清洁作业、建筑物外表面自适应、抗横风与姿态调整以及故障回收等几个部分。机器人的多传感器协同和智能控制算法使动作更加柔性和稳定,清洗效果更完美。4、2018年10月,第七代高楼外墙清洗机器人多台联调测试10月,一个举国欢庆的月份,在这个欢快的时候第七代历途高楼外墙清洗机器人也紧锣密鼓的开始了多台联调测试。高耸挺立的大楼上攀爬着多台机器人,远远望去,就好像一只只健硕的壁虎稳稳的爬在了墙面上,时而齐头并进,时而一上一下,所过之处无不焕然一新。第七代高楼外墙清洗机器人多台联调测试的成功,意味着我们又离自己的目标近了一步。5、2018年12月,意向客户已遍布全国23个省55个市。截止到2018年12月我们遍布全国的意向客户已经超过了23个省,55个城市。这也就意味着历途机器人在全国范围的覆盖率已经达到了60%以上,这是一个多么惊人的数字。从去年的15个省21个市到如今的成就,相信每一个人都能看出来,我们在不断的进步,我们的产品在不断的完善。在这里请允许小编代表历途公司由衷的对各位合作伙伴们表示深深的感谢,感谢您们对历途机器人一如既往的支持与信任,感谢您们长时间的陪伴,是您们的鼓励和支持推动着我们勇往直前!2018已近尾声,2019的钟声悄然响起,在新的一年里,历途将继续秉持着“人人皆创客,完美做产品”的理念为广大的人民群众服务,为社会提供更好的产品,为我们中国乃至世界填补高楼外墙清洗市场的空白。元旦来临之际历途所有小伙伴祝愿大家在新的一年“万事如意展宏图,心想事成兴伟业”,2019我们来了!

December 27, 2018 · 1 min · jiezi

处理H5页面中的IOS键盘回收后留下空白

最近被ios搞的头疼,很多android中正常的h5页面在ios中都有问题。场景如下:h5页面中间有个输入框和登录按钮,当输入完毕点击登录按钮的时候,ios键盘会收起,但是部分ios键盘收起的时候回有残留的灰色空白,如图所示(微信浏览器中):!!!如果想要收起灰色空白,需要在点击按钮的时候,或者input输入框失去焦点的时候,调用window.scroll(0,0)方法。PS:已测试blur()方法不行

December 26, 2018 · 1 min · jiezi

浅析企业移动化诉求与开发者之间的矛盾

一个时代的进步与发展往往会衍生出新的问题,进而反复循环,使人类文明不断地迭代与升级。步入移动互联网时代,新技术、新产品的出现总能打破行业想象,同样也带来了很多无法调节且不断重复的问题,其中尤以企业移动化需求与技术实现间的矛盾最为突出。【开发者的负担】移动互联网发展速度之快变化之大往往令人瞠目结舌,这样的行业发展特点对开发者和企业而言各有利弊。屏幕碎片化是开发者最头痛的问题之一。据谷歌最新统计,全球范围内各种各样的分辨率设备已经多达1000种。这就直接导致开发者在开发app的过程当中会遇到各种各样的屏幕,开发者需要不停的去为这些屏幕做适配。第二是硬件的参差不齐。手机厂商出于成本考虑,会或多或少的对硬件标准配置进行阉割或者降级,继而影响app的运行效果或者功能。例如有的设备厂商为了节约生产成本会选择把硬件的GPS模块拿掉,如果你的app需要GPS功能又刚好运行在这样的设备上时,就会出现无法获取地理位置的问题,在没得到设备厂商确认之前,开发者需要花大量的时间去定位原因。第三是手机厂商的泛滥。早些年国内市场中能做手机的厂商屈指可数,开发者要做的适配范围很小。但是随着移动互联网的发展,手机厂商的数量增长迅猛,根据2017年的统计数据,仅国内就已经超过200家。手机厂商生产手机过程当中,通常会对标准操作系统有各式各样的定制、差异化的改造,这是app兼容问题的罪魁祸首,开发者需要持续跟踪这些变化,并做相应的适配。除此之外,手机系统版本的频繁迭代更新也给开发者造成严重困扰。以安卓为例,从10年前的1.5、1.6版本,到即将上市的9.0版本,在如此多版本中,每一个版本的API级别系统特性、功能变化等都有可能导致app闪退、功能失效等各种各样的兼容问题,而这些都需要开发者花大量时间去一一解决。这些情况夹杂在一起,无疑在不断加重开发者的工作负担。【企业主的现实】对于一家企业而言,如果希望在移动互联网方向上布局,去开发一款app,首先要面对的问题是成本问题,我们来算一笔关于搭建一个基础开发团队的账。首先这个团队至少需要一个IOS和一个安卓开发者;其次至少需要一个项目经理统筹全局;第三,你的想法要落地、实现,必须至少一个产品经理做保障;此外还包括UI设计、测试、服务器端开发人员;如果有网站,那么还需要前端开发者;当然如果你想赶个时髦,肯定还需要一个小程序开发者。简而言之,一家企业想要在移动互联网布局,首先需要搭建一个至少10个人的开发团队。而当企业有了10个人的开发团队,真正开始去做一个app又需要花费多少钱呢?从它开始实施到完整上线,或得到用户认可,差不多需要100万!虽然成本可能是企业开发app所要面对的第一个问题,但并不是企业所要面对的核心问题。从我们长期实践过程中得出来的经验表明,项目能否按时上线,才是企业开发app的核心问题。企业希望app低成本快速实现,按时上线;而开发者因为把大量的精力耗费在不同平台的实现以及解决各种兼容适配问题上,导致项目周期延长、成本增加、无法如期上线。这个问题,是移动互联网发展这么多年来,开发者与企业诉求之间最主要的矛盾点。换句话说,app开发技术这么多年来的发展进步,其根本是为了解决开发者与企业诉求之间这个矛盾而不断演变的,跨平台技术正是这个过程的结晶。通过跨平台技术,可以消除不同平台之间的差异,开发者能够通过一次编码,编译出多个平台app安装包,实现产品在不同平台上线同时满足需求。传统app开发模式下需要4个人的工作,现在只需1个人即可完成,不但大大降低整个企业app开发的成本,也能保证项目如期的上线。移动互联网行业的发展特性决定了跨平台技术在行业中的火爆发展和受重视程度,APICloud企业互联网化生态平台具备天然的跨平台能力,跟跨平台技术出现的初衷一样,都是为了保证app如期上线,开发迭代快速简单且成本低。而这也是APICloud在短短四年得到行业认可快速发展的重要原因,站在客户和行业的角度去思考问题,是APICloud一贯的宗旨和习惯。

December 25, 2018 · 1 min · jiezi

基于大数据的用户行为预测

随着智能手机的普及和APP形态的愈发丰富,移动设备的应用安装量急剧上升。用户在每天使用这些APP的过程中,也会产生大量的线上和线下行为数据。这些数据反映了用户的兴趣与需求,如果能够被深入挖掘并且合理利用,可以指导用户的运营。若能提前预测用户下一步的行为,甚至提前得知用户卸载、流失的可能性,则能更好地指导产品的优化以及用户的精细化运营。大数据服务商个推旗下的应用统计产品“个数”,可以从用户属性、使用行为、行业对比等多指标多维度对APP进行全面统计分析。除了基础统计、渠道统计、埋点统计等功能外,个数的一大特色能力是——可基于大数据进行用户行为预测,帮助运营者预测用户流失、卸载、付费的可能性,从而助力APP的精细化运营以及全生命周期管理。开发者在实践的过程中,基于大数据进行用户行为预测会有两大难点:第一,开发者需要使用多种手段对目标问题进行分解;第二,数据在特定的问题上会有不同的表现。“个数”利用数据分析建模,对用户行为进行预测的大概流程包括以下几点:1、目标问题分解(1)明确需要进行预测的问题;(2)明确未来一段时间的跨度。2、分析样本数据(1)提取出所有用户的历史付费记录,这些付费记录可能仅占所有记录的千分之几,数据量会非常小;(2)分析付费记录,了解付费用户的构成,比如年龄层次、性别、购买力和消费的产品类别等;(3)提取非付费用户的历史数据,这里可以根据产品的需求,添加条件、或无条件地进行提取,比如提取活跃并且非付费用户,或者不加条件地直接进行提取;(4)分析非付费用户的构成。3、构建模型的特征(1)原始的数据可能能够直接作为特征使用;(2)有些数据在变换后,才会有更好的使用效果,比如年龄,可以变换成少年、中年、老年等特征;(3)交叉特征的生成,比如“中年”和“女性”两种特征,就可以合并为一个特征进行使用。4、计算特征的相关性(1)计算特征饱和度,进行饱和度过滤;(2)计算特征IV、卡方等指标,用以进行特征相关性的过滤。5、选用相关的模型进行建模(1)选择适当的参数进行建模;(2)模型训练好后,统计模型的精确度、召回率、AUC等指标,来评价模型;(3)如果觉得模型的表现可以接受,就可以在验证集上做验证,验证通过后,进行模型保存和预测。6、预测加载上述保存的模型,并加载预测数据,进行预测。7、监控最后,运营人员还需要对每次预测的结果进行关键指标监控,及时发现并解决出现的问题,防止出现意外情况,导致预测无效或预测结果出现偏差。以上就是“个数”对用户行为进行预测的整体流程。总的来说,分析和建模的关键在于大数据的收集和对大数据细节的处理。在进行用户行为预测的整个过程中,可供技术人员选择的方法和模型都有很多,而对于实际的应用者来说,没有最好的选择,只有更合适的选择。

December 25, 2018 · 1 min · jiezi

Flutter-WeChat -- 学习Flutter期间仿做的一个微信App

Flutter-WeChat – 学习Flutter期间仿做的一个微信App全都是app静态布局与部分页面交互,没有调用后台接口(个人学习作品,侵权必删)还在学习中,后续会更新页面与交互…有问题还望各位指出…谢谢!!!github地址

December 25, 2018 · 1 min · jiezi

优化状态栏沉浸式效果

沉浸式状态栏是让开发者尤其是Android开发者很头疼的问题,耗费开发者很多精力去校验代码在各个系统版本、各个机型上是否有效,今天这篇教程就跟大家分享优化初始化状态栏沉浸式效果的方法。使用APICloud时,参照社区源码,初始化状态栏沉浸式,像这样去编写:apiready = function() { var header = $api.byId(‘header’); $api.fixStatusBar(header);}有的开发者可能会遇到在Android机器上,导航栏有卡顿效果,仔细查找原因,打开api.js 找到fixStatusBar方法,你会发现是api.js里面根据手机型号等条件操作dom进行适配,方法内还使用了扩展的api对象获取数据,所以该方法必须在apireader内执行,卡顿效果就是加载api对象的时间,apiready执行变晚。下面这个方法分享给大家,帮助你们解决卡顿问题。初始化程序时,index.html 文件中 apireader 内执行:function initHeaderH(){ $api.setStorage(‘SYSTEMTYPE’,api.systemType); $api.setStorage(‘SYSTEMVERSION’,api.systemVersion); $api.setStorage(‘FULLSCREEN’,api.fullScreen); $api.setStorage(‘IOS7STATUSBARAPPEARANCE’,api.iOS7StatusBarAppearance);}在打开其他window时,不在apiready内调用,提前处理沉浸式效果,可以解决卡顿问题。var header = $api.byId(‘header’);fixStatusBar_API(header);apiready = function() {};写到常用方法内://IOS设置barfunction fixIos7Bar_API(el){ if(!$api.isElement(el)){ console.warn(’$api.fixIos7Bar Function need el param, el param must be DOM Element’); return;} var strDM = $api.getStorage(‘SYSTEMTYPE’); if (strDM == ‘ios’) { var strSV = $api.getStorage(‘SYSTEMVERSION’); var numSV = parseInt(strSV,10); var fullScreen = $api.getStorage(‘FULLSCREEN’); var iOS7StatusBarAppearance = $api.getStorage(‘IOS7STATUSBARAPPEARANCE’); if (numSV >= 7 && fullScreen == ‘false’ && iOS7StatusBarAppearance) { el.style.paddingTop = ‘20px’; }}}function fixStatusBar_API(el){ if(!$api.isElement(el)){ console.warn(’$api.fixStatusBar Function need el param, el param must be DOM Element’); return;} var sysType = $api.getStorage(‘SYSTEMTYPE’); if(sysType == ‘ios’){ fixIos7Bar_API(el);}else if(sysType == ‘android’){ var ver = $api.getStorage(‘SYSTEMVERSION’); ver = parseFloat(ver); if(ver >= 4.4){ el.style.paddingTop = ‘25px’; }}};▌本文作者:APICloud 版主 yuyangzhao ...

December 24, 2018 · 1 min · jiezi