注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

环信FAQ

环信FAQ

集成常见问题及答案
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

【iOS】一个简单的人脸跟踪Demo

1、sessionView - 相机画面的容器Viewself.detector - 脸部特征识别器- (void)viewDidLoad { [super viewDidLoad]; self.sessionView = [[UIView...
继续阅读 »

1、
sessionView - 相机画面的容器View
self.detector - 脸部特征识别器

- (void)viewDidLoad {
[super viewDidLoad];

self.sessionView = [[UIView alloc] initWithFrame:self.view.bounds];
[self.view addSubview:self.sessionView];

self.faceView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"a"]];
self.faceView.frame = CGRectZero;
[self.view addSubview:self.faceView];

self.leftEyeView = [[UIView alloc] init];
self.leftEyeView.alpha = 0.4;
self.leftEyeView.backgroundColor = [UIColor greenColor];
[self.view addSubview:self.leftEyeView];

self.rightEyeView = [[UIView alloc] init];
self.rightEyeView.alpha = 0.4;
self.rightEyeView.backgroundColor = [UIColor yellowColor];
[self.view addSubview:self.rightEyeView];

self.mouthView = [[UIView alloc] init];
self.mouthView.alpha = 0.4;
self.mouthView.backgroundColor = [UIColor redColor];
[self.view addSubview:self.mouthView];

self.context = [CIContext context];
self.detector = [CIDetector detectorOfType:CIDetectorTypeFace context:self.context options:@{CIDetectorAccuracy:CIDetectorAccuracyHigh}];
}

2、点击屏幕任意地方打开相机

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 避免重复打开,首先关闭原先的session
[self.session stopRunning];
self.session = [[AVCaptureSession alloc] init];

// 移除原有的相机画面Layer
[self.layer removeFromSuperlayer];

NSError *error;

// Device
NSArray *devices = [AVCaptureDevice devices];
NSLog(@"devices = %@", devices);
AVCaptureDevice *defaultDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];

// Input
AVCaptureDeviceInput *input = [AVCaptureDeviceInput deviceInputWithDevice:defaultDevice error:&error];
[self.session addInput:input];

// Output
AVCaptureVideoDataOutput *output = [[AVCaptureVideoDataOutput alloc] init];
[output setSampleBufferDelegate:(id)self queue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)];
[self.session addOutput:output];

// 开始捕获相机画面
[self.session startRunning];

// 将相机画面添加到容器View中
self.layer = [AVCaptureVideoPreviewLayer layerWithSession:self.session];
self.layer.frame = self.view.bounds;
[self.sessionView.layer addSublayer:self.layer];
}

3、脸部特征跟踪

// AVCaptureAudioDataOutputSampleBufferDelegate
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
// printf("%s\n", __func__);
// 1、获取当前帧图像
CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
CIImage *image = [[CIImage alloc] initWithCVImageBuffer:imageBuffer];

CGFloat imageW = image.extent.size.width;
CGFloat imageH = image.extent.size.height;

2、对图像进行脸部特征识别
CIFeature *feature = [[self.detector featuresInImage:image] lastObject];
if (feature) {
if (self.leftEyeView.frame.size.width == 0) {
self.leftEyeView.frame = CGRectMake(0, 0, 20, 20);
}
if (self.rightEyeView.frame.size.width == 0) {
self.rightEyeView.frame = CGRectMake(0, 0, 20, 20);
}
if (self.mouthView.frame.size.width == 0) {
self.mouthView.frame = CGRectMake(0, 0, 20, 20);
}
NSLog(@"find");
CIFaceFeature *face = (CIFaceFeature *)feature;
dispatch_async(dispatch_get_main_queue(), ^{
self.faceView.frame = CGRectMake(face.bounds.origin.y / imageW * self.sessionView.frame.size.height,
face.bounds.origin.x / imageH * self.sessionView.frame.size.width,
face.bounds.size.width / imageH * self.sessionView.frame.size.width,
face.bounds.size.height / imageW * self.sessionView.frame.size.height);

self.leftEyeView.center = CGPointMake(face.leftEyePosition.y / imageW * self.sessionView.frame.size.height,
face.leftEyePosition.x / imageH * self.sessionView.frame.size.width);

self.rightEyeView.center = CGPointMake(face.rightEyePosition.y / imageW * self.sessionView.frame.size.height,
face.rightEyePosition.x / imageH * self.sessionView.frame.size.width);

self.mouthView.center = CGPointMake(face.mouthPosition.y / imageW * self.sessionView.frame.size.height,
face.mouthPosition.x / imageH * self.sessionView.frame.size.width);

});
}
}

大功告成
手机记得横过来,home键在右边
Demo地址:https://github.com/MagicBlind/Face-Detector

转自:https://www.jianshu.com/p/db37d32e895e

收起阅读 »

iOS性能优化 — 三、安装包瘦身

瘦身指导原则 总体指导原则为:压缩资源、删除无用/重复资源、删除无用代码、通过编译选项进行优化。 常规瘦身方案 压缩资源项目中资源包括图片、字符串、音视频等资源。由于项目中图片比较多,所以资源压缩一般会从图片入手。在把图片加入到项目中时候需要采用tinypng...
继续阅读 »

瘦身指导原则


总体指导原则为:压缩资源、删除无用/重复资源、删除无用代码、通过编译选项进行优化。


常规瘦身方案


压缩资源
项目中资源包括图片、字符串、音视频等资源。由于项目中图片比较多,所以资源压缩一般会从图片入手。在把图片加入到项目中时候需要采用tinypng或者ImageOptim对图片进行压缩;另外,可以通知设计,对切图进行压缩处理再上传;不需要内嵌到项目中的图片可以改为动态下载。


  • png,jpg,gif可以替换成webp


  • 动画图片可替换为lotties、APNG


  • 小图或表情图可替换为iconFont


  • 大图可替换为svg



删除无用/重复资源
删除无用的资源。项目中主要以删除图片为主:


  • 图片用2x和3x图就可以,不要用1x图。


  • 可以用LSUnusedResources搜索出未使用的图片然后删除之。注意:该软件搜索出来的图片有可能项目中还在用,删除之前需要在工程中先搜索下图片是否有使用再确认是否可以删除。



删除无用代码
删除无用类和库:可以用WBBladesForMac来分析,注意:通过字符串调用的类也会检测为无用类。

非常规瘦身方案
1、Strip :去除不必要的符号信息。
-Strip Linked Product 和 Strip Swift Symbols 设置为 YES,Deployment Postprocessing 设置为 NO,发布代码的时候也需要勾选 Strip Swift Symbols。


  • Strip Debug Symbols During Copy 和 Symbols Hidden by Default 在release下设为YES


  • Dead Code Stripping 设置为 YES


  • 对于动态库,可用strip -x [动态库路径] 去除不必要的符号信息



2、Make Strings Read-Only设为YES。
3、Link-Time Optimization(LTO)release下设为 Incremental。WWDC2016介绍编译时会移除没有被调用的方法和代码,优化程序运行效率。
4、开启BitCode
5、去除异常支持。不能使用@try @catch,包只缩小0.1M,效果不显著。
Enable C++ Exceptions和Enable Objective-C Exceptions设为NO,Other C Flags添加-fno-exceptions
6、不生成debug symbols:不能生成dSYM,效果非常显著。
Generate debug symbols选项 release 设置为NO

脑图借鉴

转自:https://www.jianshu.com/p/369c909c1067

收起阅读 »

iOS内存管理-深入解析自动释放池

主要内容:AutoreleasePool简介AutoreleasePool底层原理Autorelease与NSThread、NSRunLoop的关系AutoreleasePool在主线程上的释放时机AutoreleasePool在子线程上的释放时机Autore...
继续阅读 »

主要内容:

  • AutoreleasePool简介
  • AutoreleasePool底层原理
  • Autorelease与NSThread、NSRunLoop的关系
  • AutoreleasePool在主线程上的释放时机
  • AutoreleasePool在子线程上的释放时机
  • AutoreleasePool需要手动添加的情况
  • 一、Autorelease简介

    iOS开发中的Autorelease机制是为了延时释放对象。自动释放的概念看上去很像ARC,但实际上这更类似于C语言中自动变量的特性。

    自动变量:在超出变量作用域后将被废弃;
    自动释放池:在超出释放池生命周期后,向其管理的对象实例的发送release消息。

    1.1 MRC下使用自动释放池
    在MRC环境中使用自动释放池需要用到NSAutoreleasePool对象,其生命周期就相当于C语言变量的作用域。对于所有调用过autorelease方法的对象,在废弃NSAutoreleasePool对象时,都将调用release实例方法。用源代码表示如下:
    //MRC环境下的测试:
    //第一步:生成并持有释放池NSAutoreleasePool对象;
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    //第二步:调用对象的autorelease实例方法;
    id obj = [[NSObject alloc] init];
    [obj autorelease];

    //第三步:废弃NSAutoreleasePool对象;
    [pool drain]; //向pool管理的所有对象发送消息,相当于[obj release]

    //obi已经释放,再次调用会崩溃(Thread 1: EXC_BAD_ACCESS (code=EXC_I386_GPFLT))
    NSLog(@"打印obj:%@", obj);

    理解NSAutoreleasePool对象的生命周期,如下图所示:


    1.2 ARC下使用自动释放池
    ARC环境不能使用NSAutoreleasePool类也不能调用autorelease方法,代替它们实现对象自动释放的是@autoreleasepool块和__autoreleasing修饰符。比较两种环境下的代码差异如下图:

    如图所示,@autoreleasepool块替换了NSAutoreleasePoool类对象的生成、持有及废弃这一过程。而附有__autoreleasing修饰符的变量替代了autorelease方法,将对象注册到了Autoreleasepool;由于ARC的优化,__autorelease是可以被省略的,所以简化后的ARC代码如下:
    //ARC环境下的测试:
    @autoreleasepool {
    id obj = [[NSObject alloc] init];
    NSLog(@"打印obj:%@", obj);
    }

    显式使用__autoreleasing修饰符的情况非常少见,这是因为ARC的很多情况下,即使是不显式的使用__autoreleasing,也能实现对象被注册到释放池中。主要包括以下几种情况:

  • 编译器会进行优化,检查方法名是否以alloc/new/copy/mutableCopy开始,如果不是则自动将返回对象注册到Autoreleasepool;
  • 访问附有__weak修饰符的变量时,实际上必定要访问注册到Autoreleasepool的对象,即会自动加入Autoreleasepool;
  • id的指针或对象的指针(id*,NSError **),在没有显式地指定修饰符时候,会被默认附加上__autoreleasing修饰符,加入Autoreleasepool

  • 注意:如果编译器版本为LLVM.3.0以上,即使ARC无效@autoreleasepool块也能够使用;如下源码所示:

    //MRC环境下的测试:
    @autoreleasepool{
    id obj = [[NSObject alloc] init];
    [obj autorelease];
    }


    二、AutoRelease原理

    2.1 使用@autoreleasepool{}

    我们在main函数中写入自动释放池相关的测试代码如下:

    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    NSLog(@"Hello, World!");
    }
    return 0;
    }

    为了探究释放池的底层实现,我们在终端使用clang -rewrite-objc + 文件名命令将上述OC代码转化为C++源码:

    int main(int argc, const char * argv[]) {
    /* @autoreleasepool */
    {
    __AtAutoreleasePool __autoreleasepool;
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_3f_crl5bnj956d806cp7d3ctqhm0000gn_T_main_d37e0d_mi_0);
    }//大括号对应释放池的作用域

    return 0;
    }

    在经过编译器clang命令转化后,我们看到的所谓的@autoreleasePool块,其实对应着__AtAutoreleasePool的结构体。

    2.2 分析结构体__AtAutoreleasePool的具体实现

    在源码中找到__AtAutoreleasePool结构体的实现代码,具体如下:

    extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);
    extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);

    struct __AtAutoreleasePool {
    __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
    ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
    void * atautoreleasepoolobj;
    };

    __AtAutoreleasePool结构体包含了:构造函数、析构函数和一个边界对象;
    构造函数内部调用:objc_autoreleasePoolPush()方法,返回边界对象atautoreleasepoolobj
    析构函数内部调用:objc_autoreleasePoolPop()方法,传入边界对象atautoreleasepoolobj

    分析main函数中__autoreleasepool结构体实例的生命周期是这样的:
    __autoreleasepool是一个自动变量,其构造函数是在程序执行到声明这个对象的位置时调用的,而其析构函数则是在程序执行到离开这个对象的作用域时调用。所以,我们可以将上面main函数的代码简化如下:

    int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ {
    void *atautoreleasepoolobj = objc_autoreleasePoolPush();
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_kb_06b822gn59df4d1zt99361xw0000gn_T_main_d39a79_mi_0);
    objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
    return 0;
    }
    2.3 objc_autoreleasePoolPush与objc_autoreleasePoolPop

    进一步观察自动释放池构造函数与析构函数的实现,其实它们都只是对AutoreleasePoolPage对应静态方法pushpop的封装

    void *objc_autoreleasePoolPush(void) {
    return AutoreleasePoolPage::push();
    }

    void objc_autoreleasePoolPop(void *ctxt) {
    AutoreleasePoolPage::pop(ctxt);
    }

    2.4 理解AutoreleasePoolPage
    AutoreleasePoolPage是一个C++中的类,打开Runtime的源码工程,在NSObject.mm文件中可以找到它的定义,摘取其中的关键代码如下:
    //大致在641行代码开始
    class AutoreleasePoolPage {
    # define EMPTY_POOL_PLACEHOLDER ((id*)1) //空池占位
    # define POOL_BOUNDARY nil //边界对象(即哨兵对象)
    static pthread_key_t const key = AUTORELEASE_POOL_KEY;
    static uint8_t const SCRIBBLE = 0xA3; // 0xA3A3A3A3 after releasing
    static size_t const SIZE =
    #if PROTECT_AUTORELEASEPOOL
    PAGE_MAX_SIZE; // must be multiple of vm page size
    #else
    PAGE_MAX_SIZE; // size and alignment, power of 2
    #endif
    static size_t const COUNT = SIZE / sizeof(id);
    magic_t const magic; //校验AutoreleasePagePoolPage结构是否完整
    id *next; //指向新加入的autorelease对象的下一个位置,初始化时指向begin()
    pthread_t const thread; //当前所在线程,AutoreleasePool是和线程一一对应的
    AutoreleasePoolPage * const parent; //指向父节点page,第一个结点的parent值为nil
    AutoreleasePoolPage *child; //指向子节点page,最后一个结点的child值为nil
    uint32_t const depth; //链表深度,节点个数
    uint32_t hiwat; //数据容纳的一个上限
    //......
    };

    其实,每个自动释放池都是是由若干个AutoreleasePoolPage组成的双向链表结构,如下图所示:


    AutoreleasePoolPage中拥有parentchild指针,分别指向上一个和下一个page;当前一个page的空间被占满(每个AutorelePoolPage的大小为4096字节)时,就会新建一个AutorelePoolPage对象并连接到链表中,后来的 Autorelease对象也会添加到新的page中;

    另外,当next== begin()时,表示AutoreleasePoolPage为空;当next == end(),表示AutoreleasePoolPage已满。

    2.5 理解哨兵对象/边界对象(POOL_BOUNDARY)的作用

    AutoreleasePoolPage的源码中,我们很容易找到边界对象(哨兵对象)的定义:

    #define POOL_BOUNDARY nil

    边界对象其实就是nil的别名,而它的作用事实上也就是为了起到一个标识的作用。

    每当自动释放池初始化调用objc_autoreleasePoolPush方法时,总会通过AutoreleasePoolPagepush方法,将POOL_BOUNDARY放到当前page的栈顶,并且返回这个边界对象;

    而在自动释放池释放调用objc_autoreleasePoolPop方法时,又会将边界对象以参数传入,这样自动释放池就会向释放池中对象发送release消息,直至找到第一个边界对象为止。

    2.6 理解objc_autoreleasePoolPush方法
    经过前面的分析,objc_autoreleasePoolPush最终调用的是  AutoreleasePoolPagepush方法,该方法的具体实现如下:
    static inline void *push() {
    return autoreleaseFast(POOL_BOUNDARY);
    }

    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 {
    1. return autoreleaseNoPage(obj);
    }
    }

    //压栈操作:将对象加入AutoreleaseNoPage并移动栈顶的指针
    id *add(id obj) {
    id *ret = next;
    *next = obj;
    next++;
    return ret;
    }

    //当前hotPage已满时调用
    static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) {
    do {
    if (page->child) page = page->child;
    else page = new AutoreleasePoolPage(page);
    } while (page->full());

    setHotPage(page);
    return page->add(obj);
    }

    //当前hotpage不存在时调用
    static id *autoreleaseNoPage(id obj) {
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    setHotPage(page);

    if (obj != POOL_SENTINEL) {
    page->add(POOL_SENTINEL);
    }

    return page->add(obj);
    }

    观察上述代码,每次调用push其实就是创建一个新的AutoreleasePool,在对应的AutoreleasePoolPage中插入一个POOL_BOUNDARY ,并且返回插入的POOL_BOUNDARY 的内存地址。push方法内部调用的是autoreleaseFast方法,并传入边界对象(POOL_BOUNDARY)。hotPage可以理解为当前正在使用的AutoreleasePoolPage

    自动释放池最终都会通过page->add(obj)方法将边界对象添加到释放池中,而这一过程在autoreleaseFast方法中被分为三种情况:

  • 当前page存在且不满,调用page->add(obj)方法将对象添加至page的栈中,即next指向的位置
  • 当前page存在但是已满,调用autoreleaseFullPage初始化一个新的page,调用page->add(obj)方法将对象添加至page的栈中
  • 当前page不存在时,调用autoreleaseNoPage创建一个hotPage,再调用page->add(obj) 方法将对象添加至page的栈中

  • 2.7 objc_autoreleasePoolPop方法

    AutoreleasePool的释放调用的是objc_autoreleasePoolPop方法,此时需要传入边界对象作为参数。这个边界对象正是每次执行objc_autoreleasePoolPush方法返回的对象atautoreleasepoolobj

    同理,我们找到objc_autoreleasePoolPop最终调用的方法,即AutoreleasePoolPagepop方法,该方法的具体实现如下:

    static inline void pop(void *token)   //POOL_BOUNDARY的地址
    {
    AutoreleasePoolPage *page;
    id *stop;

    page = pageForPointer(token); //通过POOL_BOUNDARY找到对应的page
    stop = (id *)token;
    if (DebugPoolAllocation && *stop != POOL_SENTINEL) {
    // This check is not valid with DebugPoolAllocation off
    // after an autorelease with a pool page but no pool in place.
    _objc_fatal("invalid or prematurely-freed autorelease pool %p; ",
    token);
    }

    if (PrintPoolHiwat) printHiwat(); // 记录最高水位标记

    page->releaseUntil(stop); //向栈中的对象发送release消息,直到遇到第一个哨兵对象

    // memory: delete empty children
    // 删除空掉的节点
    if (DebugPoolAllocation && page->empty()) {
    // special case: delete everything during page-per-pool debugging
    AutoreleasePoolPage *parent = page->parent;
    page->kill();
    setHotPage(parent);
    } else if (DebugMissingPools && page->empty() && !page->parent) {
    // special case: delete everything for pop(top)
    // when debugging missing autorelease pools
    page->kill();
    setHotPage(nil);
    }
    else if (page->child) {
    // hysteresis: keep one empty child if page is more than half full
    if (page->lessThanHalfFull()) {
    page->child->kill();
    }
    else if (page->child->child) {
    page->child->child->kill();
    }
    }
    }

    上述代码中,首先根据传入的边界对象地址找到边界对象所处的page;然后选择当前page中最新加入的对象一直向前清理,可以向前跨越若干个page,直到边界所在的位置;清理的方式是向这些对象发送一次release消息,使其引用计数减一;

    另外,清空page对象还会遵循一些原则:

    1.如果当前的page中存放的对象少于一半,则子page全部删除;

    2.如果当前当前的page存放的多余一半(意味着马上将要满),则保留一个子page,节省创建新page的开销;

    2.8 autorelease方法

    上述是对自动释放池整个生命周期的分析,现在我们来理解延时释放对象autorelease方法的实现,首先查看该方法的调用栈:

    - [NSObject autorelease]
    └── id objc_object::rootAutorelease()
    └── id objc_object::rootAutorelease2()
    └── static id AutoreleasePoolPage::autorelease(id obj)
    └── static id AutoreleasePoolPage::autoreleaseFast(id obj)
    ├── id *add(id obj)
    ├── static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
    │ ├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
    │ └── id *add(id obj)
    └── static id *autoreleaseNoPage(id obj)
    ├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
    └── id *add(id obj)

    如上所示,autorelease方法最终也会调用上面提到的 autoreleaseFast方法,将当前对象加到AutoreleasePoolPage中。关于autoreleaseFast的分析这里不再累述,我们主要来考虑一下两次调用的区别:

    autorelease函数和push函数一样,关键代码都是调用autoreleaseFast函数向自动释放池的链表栈中添加一个对象,不过push函数入栈的是一个边界对象,而autorelease函数入栈的是一个具体的Autorelease的对象。

    三、AutoreleasePool与NSThread、NSRunLoop的关系

    由于AppKitUIKit框架的优化,我们很少需要显式的创建一个自动释放池块。这其中就涉及到AutoreleasePoolNSThreadNSRunLoop的关系。

    3.1 RunLoop和NSThread的关系
    RunLoop是用于控制线程生命周期并接收事件进行处理的机制,其实质是一个do-While循环。在苹果文档找到关于NSRunLoop的介绍如下:
    Your application neither creates or explicitly manages NSRunLoop objects. Each NSThread object—including the application’s main thread—has an NSRunLoop object automatically created for it as needed. If you need to access the current thread’s run loop, you do so with the class method currentRunLoop.

    总结RunLoopNSThread(线程)之间的关系如下:
  • RunLoop与线程是一一对应关系,每个线程(包括主线程)都有一个对应的RunLoop对象;其对应关系保存在一个全局的Dictionary里;
  • 主线程的RunLoop默认由系统自动创建并启动;而其他线程在创建时并没有RunLoop,若该线程一直不主动获取,就一直不会有RunLoop
  • 苹果不提供直接创建RunLoop的方法;所谓其他线程Runloop的创建其实是发生在第一次获取的时候,系统判断当前线程没有RunLoop就会自动创建;
  • 当前线程结束时,其对应的Runloop也被销毁;
  • 3.2 RunLoop和AutoreleasePool的关系

    苹果文档中找到两者关系的介绍如下:

    The Application Kit creates an autorelease pool on the main thread at the beginning of every cycle of the event loop, and drains it at the end, thereby releasing any autoreleased objects generated while processing an event.

    如上所述,主线程的NSRunLoop在监测到事件响应开启每一次event loop之前,会自动创建一个autorelease pool,并且会在event loop结束的时候执行drain操作,释放其中的对象。

    3.3 Thread和AutoreleasePool的关系
    苹果文档中找到两者关系的介绍如下:

    Each thread (including the main thread) maintains its own stack of NSAutoreleasePool objects (see Threads). As new pools are created, they get added to the top of the stack. When pools are deallocated, they are removed from the stack. Autoreleased objects are placed into the top autorelease pool for the current thread. When a thread terminates, it automatically drains all of the autorelease pools associated with itself.

    如上所述, 包括主线程在内的所有线程都维护有它自己的自动释放池的堆栈结构。新的自动释放池被创建的时候,它们会被添加到栈的顶部,而当池子销毁的时候,会从栈移除。对于当前线程来说,Autoreleased对象会被放到栈顶的自动释放池中。当一个线程线程停止,它会自动释放掉与其关联的所有自动释放池。

    四、AutoreleasePool在主线程上的释放时机

    4.1 理解主线程上的自动释放过程
    分析主线程RunLoop管理自动释放池并释放对象的详细过程,我们在如下Demo中的主线程中设置断点,并执行lldb命令:po [NSRunLoop currentRunLoop],具体效果如下:

    我们看到主线程RunLoop中有两个与自动释放池相关的Observer,它们的 activities分别为0x10xa0这两个十六进制的数,转为二进制分别为110100000,对应CFRunLoopActivity的类型如下:
    /* Run Loop Observer Activities */
    typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0), //0x1,启动Runloop循环
    kCFRunLoopBeforeTimers = (1UL << 1),
    kCFRunLoopBeforeSources = (1UL << 2),
    kCFRunLoopBeforeWaiting = (1UL << 5), //0xa0,即将进入休眠
    kCFRunLoopAfterWaiting = (1UL << 6),
    kCFRunLoopExit = (1UL << 7), //0xa0,退出RunLoop循环
    kCFRunLoopAllActivities = 0x0FFFFFFFU
    };

    结合RunLoop监听的事件类型,分析主线程上自动释放池的使用过程如下:

  • App启动后,苹果在主线程RunLoop里注册了两个Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler();
  • 第一个Observer监视的事件是Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush()创建自动释放池。order = -2147483647(即32位整数最小值)表示其优先级最高,可以保证创建释放池发生在其他所有回调之前;
  • 第二个Observer监视了两个事件BeforeWaiting(准备进入休眠)时调用_objc_autoreleasePoolPop()_objc_autoreleasePoolPush()释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop()来释放自动释放池。order = 2147483647(即32位整数的最大值)表示其优先级最低,保证其释放池子发生在其他所有回调之后;
  • 在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop创建好的AutoreleasePool环绕着,所以不会出现内存泄漏,开发者也不必显示创建AutoreleasePool了;
  • 最后,也可以结合图示理解主线程上自动释放对象的具体流程:


  • 程序启动到加载完成后,主线程对应的RunLoop会停下来等待用户交互
  • 用户的每一次交互都会启动一次运行循环,来处理用户所有的点击事件、触摸事件。
  • RunLoop检测到事件后,就会创建自动释放池;
  • 所有的延迟释放对象都会被添加到这个池子中;
  • 在一次完整的运行循环结束之前,会向池中所有对象发送release消息,然后自动释放池被销毁;

  • 4.2 测试主线程上的对象自动释放过程
    下面的代码创建了一个Autorelease对象string,并且通过weakString进行弱引用(不增加引用计数,所以不会影响对象的生命周期),具体如下:
    @interface TestMemoryVC ()
    @property (nonatomic,weak)NSString *weakString;
    @end

    @implementation TestMemoryVC
    - (void)viewDidLoad {
    [super viewDidLoad];
    NSString *string = [NSString stringWithFormat:@"%@",@"WUYUBEICHEN"];
    self.weakString = string;
    }

    - (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    NSLog(@"viewWillAppear:%@", self.weakString);
    }

    - (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    NSLog(@"viewDidAppear:%@", self.weakString);
    }

    @end

    //打印结果:
    //viewWillAppear:WUYUBEICHEN
    //viewDidAppear:(null)
    代码分析:自动变量的string在离开viewDidLoad的作用域后,会依靠当前主线程上的RunLoop迭代自动释放。最终string对象在viewDidAppear方法执行前被释放(RunLoop完成此次迭代)。

    五、AutoreleasePool子线程上的释放时机

    子线程默认不开启RunLoop,那么其中的延时对象该如何释放呢?其实这依然要从ThreadAutoreleasePool的关系来考虑:
    Each thread (including the main thread) maintains its own stack of NSAutoreleasePool objects.
    也就是说,每一个线程都会维护自己的 Autoreleasepool栈,所以子线程虽然默认没有开启RunLoop,但是依然存在AutoreleasePool,在子线程退出的时候会去释放autorelease对象。

    前面讲到过,ARC会根据一些情况进行优化,添加__autoreleasing修饰符,其实这就相当于对需要延时释放的对象调用了autorelease方法。从源码分析的角度来看,如果子线程中没有创建AutoreleasePool ,而一旦产生了Autorelease对象,就会调用autoreleaseNoPage方法自动创建hotpage,并将对象加入到其栈中。所以,一般情况下,子线程中即使我们不手动添加自动释放池,也不会产生内存泄漏。

    六、AutoreleasePool需要手动添加的情况

    尽管ARC已经做了诸多优化,但是有些情况我们必须手动创建AutoreleasePool,而其中的延时对象将在当前释放池的作用域结束时释放。苹果文档中说明了三种情况,我们可能会需要手动添加自动释放池:
    1. 编写的不是基于UI框架的程序,例如命令行工具;
    2. 通过循环方式创建大量临时对象;
    3. 使用非Cocoa程序创建的子线程;

    而在ARC环境下的实际开发中,我们最常遇到的也是第二种情况,以下面的代码为例:

    - (void)viewDidLoad {
    [super viewDidLoad];
    for (int i = 0; i < 1000000; i++) {
    NSObject *obj = [[NSObject alloc] init];
    NSLog(@"打印obj:%@", obj);
    }
    }
    上述代码中,obj因为离开作用域所以会被加入最近一次创建的自动释放池中,而这个释放池就是主线程上的RunLoop管理的;因为for循环在当前线程没有执行完毕,Runloop也就没有完成当前这一次的迭代,所以导致大量对象被延时释放。释放池中的对象将会在viewDidAppear方法执行前就被销毁。在此情况下,我们就有必要通过手动干预的方式及时释放不需要的对象,减少内存消耗;优化的代码如下:
    - (void)viewDidLoad {
    [super viewDidLoad];
    for (int i = 0; i < 1000000; i++) {
    @autoreleasepool{
    NSObject *obj = [[NSObject alloc] init];
    NSLog(@"打印obj:%@", obj);
    }
    }
    }


    摘自作者:梧雨北辰
    原贴链接:https://www.jianshu.com/p/7bd2f85f03dc

    收起阅读 »

    iOS性能优化 — 四、内存泄露检测

    上篇文章为大家讲解了安装包瘦身,这篇文章继续为大家讲解下内存泄露检测。造成内存泄漏原因常见循环引用及解决方案怎么检测循环引用造成内存泄漏原因在用C/C++时,创建对象后未销毁,比如调用malloc后不free、调用new后不delete;调用CoreFound...
    继续阅读 »

    上篇文章为大家讲解了安装包瘦身,这篇文章继续为大家讲解下内存泄露检测。

    • 造成内存泄漏原因

    • 常见循环引用及解决方案

    • 怎么检测循环引用

    造成内存泄漏原因

    • 在用C/C++时,创建对象后未销毁,比如调用malloc后不free、调用new后不delete;

    • 调用CoreFoundation里面的C方法后创建对对象后不释放。比如调用CGImageCreate不调用CGImageRelease;

    • 循环引用。当对象A和对象B互相持有的时候,就会产生循环引用。常见产生循环引用的场景有在VC的cellForRowAtIndexPath方法中cell block引用self。

    常见循环引用及解决方案

    1) 在VC的cellForRowAtIndexPath方法中cell的block直接引用self或者直接以_形式引用属性造成循环引用。

    cell.clickBlock = ^{
    self.name = @"akon";
    };

    cell.clickBlock = ^{
    _name = @"akon";
    };

    解决方案:把self改成weakSelf;

    __weak typeof(self)weakSelf = self;
    cell.clickBlock = ^{
    weakSelf.name = @"akon";
    };

    2)在cell的block中直接引用VC的成员变量造成循环引用。

    //假设 _age为VC的成员变量
    @interface TestVC(){

    int _age;

    }
    cell.clickBlock = ^{
    _age = 18;
    };

    解决方案有两种:

    • 用weak-strong dance

    __weak typeof(self)weakSelf = self;
    cell.clickBlock = ^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    strongSelf->age = 18;
    };
    • 把成员变量改成属性

    //假设 _age为VC的成员变量
    @interface TestVC()

    @property(nonatomic, assign)int age;

    @end

    __weak typeof(self)weakSelf = self;
    cell.clickBlock = ^{
    weakSelf.age = 18;
    };

    3)delegate属性声明为strong,造成循环引用。

    @interface TestView : UIView

    @property(nonatomic, strong)id<TestViewDelegate> delegate;

    @end

    @interface TestVC()<TestViewDelegate>

    @property (nonatomic, strong)TestView* testView;

    @end

    testView.delegate = self; //造成循环引用

    解决方案:delegate声明为weak

    @interface TestView : UIView

    @property(nonatomic, weak)id<TestViewDelegate> delegate;

    @end

    4)在block里面调用super,造成循环引用。

    cell.clickBlock = ^{
    [super goback]; //造成循环应用
    };

    解决方案,封装goback调用

    __weak typeof(self)weakSelf = self;
    cell.clickBlock = ^{
    [weakSelf _callSuperBack];
    };

    - (void) _callSuperBack{
    [self goback];
    }

    5)block声明为strong
    解决方案:声明为copy
    6)NSTimer使用后不invalidate造成循环引用。
    解决方案:

    • NSTimer用完后invalidate;

    • NSTimer分类封装

    *   (NSTimer *)ak_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
    block:(void(^)(void))block
    repeats:(BOOL)repeats{

    return [self scheduledTimerWithTimeInterval:interval
    target:self
    selector:@selector(ak_blockInvoke:)
    userInfo:[block copy]
    repeats:repeats];
    }

    * (void)ak_blockInvoke:(NSTimer*)timer{

    void (^block)(void) = timer.userInfo;
    if (block) {
    block();
    }
    }

    怎么检测循环引用

    • 静态代码分析。 通过Xcode->Product->Anaylze分析结果来处理;

    • 动态分析。用MLeaksFinder或者Instrument进行检测。

    转自:https://www.jianshu.com/p/f06f14800cf7

    收起阅读 »

    Xcode12适配The linked library is missing one or more architectures required by this target问题

    问题升级到Xcode12后,运行Release模式后,会提示以下信息: The linked library 'xxxx.a/Framework' is missing one or more architectures required by this ta...
    继续阅读 »

    问题
    升级到Xcode12后,运行Release模式后,会提示以下信息:

    The linked library 'xxxx.a/Framework' is missing one or more architectures required by this target: armv7.

    又或者


    xxx/Pods/Target Support Files/Pods-xxx/Pods-xxx-frameworks.sh: line 128: ARCHS[@]: unbound variable
    Command PhaseScriptExecution failed with a nonzero exit code

    以上涉及架构问题

    解决方案

    在Target-Build Settings-Excluded Architectures中添加以下代码

    EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64=arm64 arm64e armv7 armv7s armv6 armv8 EXCLUDED_ARCHS=$(inherited) $(EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_$(EFFECTIVE_PLATFORM_SUFFIX)__NATIVE_ARCH_64_BIT_$(NATIVE_ARCH_64_BIT))


    转自:https://www.jianshu.com/p/81741aed39f7


    收起阅读 »

    iOS 使用NSSetUncaughtExceptionHandler收集Crash

    在iOS程序崩溃时,一般我们是用Bugtags、Bugly、友盟等第三方收集崩溃,其实官方提供的NSUncaughtExceptionHandler来收集crash信息。实现方式如下:自定义一个UncaughtExceptionHandler类,在.h中: @...
    继续阅读 »

    在iOS程序崩溃时,一般我们是用Bugtags、Bugly、友盟等第三方收集崩溃,其实官方提供的NSUncaughtExceptionHandler来收集crash信息。实现方式如下:
    自定义一个UncaughtExceptionHandler类,在.h中:

    @interface CustomUncaughtExceptionHandler : NSObject
    + (void)setDefaultHandler;
    + (NSUncaughtExceptionHandler *)getHandler;
    @end

    复制代码
    在.m中实现:

    #import "CustomUncaughtExceptionHandler.h"

    // 沙盒的地址
    NSString * applicationDocumentsDirectory() {
    return [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    }


    // 崩溃时的回调函数
    void UncaughtExceptionHandler(NSException * exception) {
    NSArray * arr = [exception callStackSymbols];
    NSString * reason = [exception reason]; // // 崩溃的原因 可以有崩溃的原因(数组越界,字典nil,调用未知方法...) 崩溃的控制器以及方法
    NSString * name = [exception name];
    NSString * url = [NSString stringWithFormat:@"crash报告\nname:%@\nreason:\n%@\ncallStackSymbols:\n%@",name,reason,[arr componentsJoinedByString:@"\n"]];
    NSString * path = [applicationDocumentsDirectory() stringByAppendingPathComponent:@"crash.txt"];
    // 将一个txt文件写入沙盒
    [url writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:nil];
    }

    @implementation CustomUncaughtExceptionHandler

    + (void)setDefaultHandler {
    NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);
    }

    + (NSUncaughtExceptionHandler *)getHandler {
    return NSGetUncaughtExceptionHandler();
    }

    @end

    复制代码
    这样我们就实现好了一个自定义UncaughtExceptionHandler类,接下来只需要在合适的地方获取crash文件以及传到服务器上去即可,如下所示:

    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.

    //崩溃日志
    [CustomUncaughtExceptionHandler setDefaultHandler];
    //获取崩溃日志,然后发送
    NSString *path = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
    NSString *dataPath = [path stringByAppendingPathComponent:@"crash.txt"];
    NSData *data = [NSData dataWithContentsOfFile:dataPath];
    if (data != nil) {
    //发送崩溃日志
    NSLog(@"crash了:%@",data);
    }
    }

    链接:https://juejin.cn/post/6953142642746064910


    收起阅读 »

    怎么获取到环信老版本的SDK和Demo

    来到环信官网的下载页面:下载-即时通讯云-环信 找到想要下载的sdk,以iOS端为例,右键“SDK+Demo源码”,拷贝链接,然后修改链接里的版本号即可例如:https://download-sdk.oss-cn-beijing.aliyuncs.com/do...
    继续阅读 »

    来到环信官网的下载页面:下载-即时通讯云-环信

    找到想要下载的sdk,以iOS端为例,右键“SDK+Demo源码”,拷贝链接,然后修改链接里的版本号即可
    例如:https://download-sdk.oss-cn-beijing.aliyuncs.com/downloads/iOS_IM_SDK_V3.7.4.zip

    收起阅读 »

    (IM)iOS端离线推送收不到怎么办?

    离线推送收不到,按照下面步骤一步一步进行排查: 0、如果你的app之前可以收到离线推送,突然收不到了,那么先移步苹果开发者中心查看推送证书是否过期。如果过期了,需要重新制作证书,然后到环信管理后台(Console)将旧的删掉再上传新的。过期的一般会被封禁,需要...
    继续阅读 »

    离线推送收不到,按照下面步骤一步一步进行排查:


    0、如果你的app之前可以收到离线推送,突然收不到了,那么先移步苹果开发者中心查看推送证书是否过期。如果过期了,需要重新制作证书,然后到环信管理后台(Console)将旧的删掉再上传新的。过期的一般会被封禁,需要联系环信进行解封操作。


    1、首先已经按照环信的文档集成了离线推送:APNs离线推送


    2、如果是iOS13及以上的系统,那么需要将IM SDK更新到3.6.4或以上版本。
    如果更新后还不行那么退出登录、重启app、再登录试下。
    初始化sdk成功之后打印版本号:
    NSString *ver = [EMClient sharedClient].version;

    3、测试APNs推送的时候,接收方的APP需要是杀死状态,需要用户长连接断开才会发APNs推送;
    **所以直接上划杀死APP测试。**


    4、要确保导出p12时使用的Mac和创建CertificateSigningRequest.certSigningRequest文件的Mac是同一台;导出证书的时候要直接点击导出,不要点击秘钥的内容导出;确认 APP ID 是否带有推送功能;


    5、环信管理后台(Console)上传证书时填写的Bundle ID须与工程中的Bundle ID、推送证书的 APP ID 相同;选择的证书类型须与推送证书的环境一致;导出.p12文件需要设置密码,并在上传管理后台时传入;


    6、工程中初始化SDK那里填的证书名与环信管理后台上传的证书名称必须是相同的;


    7、测试环境测试,需要使用development环境的推送证书,Xcode直接真机运行;
    正式环境测试,需要使用production环境的推送证书,而且要打包,打包时选择Ad Hoc,导出IPA安装到手机上。

    8、APP杀死后可调用“获取单个用户”的rest接口,确认证书名称是否有绑定(正常情况下,登录成功后会绑定上推送证书,绑定后会显示推送证书名称);还需要确认绑定的证书名称和管理后台上传的证书名称是否一致。


    接口文档:获取单个用户
    获取用户信息
    如果没绑定上,那么退出登录、重启app、重新登录再试下。

    如果证书名称不一致,改正过来后重新登录试下。


    9、如果以上都确认无误,可以联系环信排查。需提供以下信息(请勿遗漏,以免反复询问耽误时间):
    appkey、devicetoken、bundle id、证书的.p12文件、证书名称、证书密码、收不到推送的环信id、测试的环境(development or production)、消息id、消息的内容和发送时间

    消息id要在消息发送成功后获取,如图:
    获取消息id
    收起阅读 »

    iOS 唤起APP之Universal Link(通用链接)

    iOS 9之前,一直使用的是URL Schemes技术来从外部对App进行跳转,但是iOS系统中进行URL Schemes跳转的时候如果没有安装App,会提示Cannot open Page的提示,而且当注册有多个scheme相同的时候,目前没有办法区分,但是...
    继续阅读 »

    iOS 9之前,一直使用的是URL Schemes技术来从外部对App进行跳转,但是iOS系统中进行URL Schemes跳转的时候如果没有安装App,会提示Cannot open Page的提示,而且当注册有多个scheme相同的时候,目前没有办法区分,但是从iOS 9起可以使用Universal Links技术进行跳转页面,这是一种体验更加完美的解决方案


    什么是Universal Link(通用链接)
    Universal Link是Apple在iOS 9推出的一种能够方便的通过传统HTTPS链接来启动APP的功能。如果你的应用支持Universal Link,当用户点击一个链接时可以跳转到你的网站并获得无缝重定向到对应的APP,且不需要通过Safari浏览器。如果你的应用不支持的话,则会在Safari中打开该链接

    支持Universal Link(通用链接)
    先决条件:必须有一个支持HTTPS的域名,并且拥有该域名下上传到根目录的权限(为了上传Apple指定文件)

    集成步骤

    1、开发者中心配置
    找到对应的App ID,在Application Services列表里有Associated Domains一条,把它变为Enabled就可以了

    2、工程配置
    targets->Capabilites->Associated Domains,在其中的Domains中填入你想支持的域名,必须以applinks:为前缀,如:applinks:domain

    3、配置指定文件
    创建一个内容为json格式的文件,苹果将会在合适的时候,从我们在项目中填入的域名请求这个文件。这个文件名必须为apple-app-site-association,切记没有后缀名,文件内容大概是这样子:

    {
    “applinks”: {
    “apps”: [],
    “details”: [
    {
    “appID”: “9JA89QQLNQ.com.apple.wwdc”,
    “paths”: [ “/wwdc/news/“, “/videos/wwdc/2015/“]
    },
    {
    “appID”: “ABCD1234.com.apple.wwdc”,
    “paths”: [ ““ ]
    }
    ]
    }
    }
    复制代码appID:组成方式是TeamID.BundleID。如上面的9JA89QQLNQ就是teamId。登陆开发者中心,在Account -> Membership里面可以找到Team ID
    paths:设定你的app支持的路径列表,只有这些指定路径的链接,才能被app所处理。*的写法代表了可识别域名下所有链接

    4、上传该文件
    上传该文件到你的域名所对应的根目录或者.well-known目录下,这是为了苹果能获取到你上传的文件。上传完后,先访问一下,看看是否能够获取到,当你在浏览器中输入这个文件链接后,应该是直接下载apple-app-site-association文件

    5、代码中的相关支持
    当点击某个链接,可以直接进我们的app,但是我们的目的是要能够获取到用户进来的链接,根据链接来展示给用户相应的内容,我们需要在工程里实现AppDelegate对应的方法:


    • (BOOL)application:(UIApplication )application continueUserActivity:(NSUserActivity )userActivity restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler {
      // NSUserActivityTypeBrowsingWeb 由Universal Links唤醒的APP
      if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]){
        NSURL *webpageURL = userActivity.webpageURL;
      NSString *host = webpageURL.host;
      if ([host isEqualToString:@"api.r2games.com.cn"]){
      //进行我们的处理
      NSLog(@"TODO....");
      }else{
      NSLog(@"openurl");
      [[UIApplication sharedApplication] openURL:webpageURL options:nil completionHandler:nil];
      // [[UIApplication sharedApplication] openURL:webpageURL];
      }
      }
      return YES;
      }
      复制代码苹果为了方便开发者,提供了一个网页验证我们编写的这个apple-app-site-association是否合法有效

    Universal Link(通用链接)注意点


    Universal Link跨域
    Universal Link有跨域问题,Universal Link必须要求跨域,如果不跨域,就不会跳转(iOS 9.2之后的改动)
    假如当前网页的域名是A,当前网页发起跳转的域名是B,必须要求B和A是不同域名才会触发Universal Link,如果B和A是相同域名,只会继续在当前WebView里面进行跳转,哪怕你的Universal Link一切正常,根本不会打开App
    Universal Link请求apple-app-site-association时机

    当我们的App在设备上第一次运行时,如果支持Associated Domains功能,那么iOS会自动去GET定义的Domain下的apple-app-site-association文件


    iOS会先请求https://domain.com/.well-known/apple-app-site-association,如果此文件请求不到,再去请求https://domain.com/apple-app-site-association,所以如果想要避免服务器接收过多GET请求,可以直接把apple-app-site-association放在./well-known目录下


    服务器上apple-app-site-association的更新不会让iOS本地的apple-app-site-association同步更新,即iOS只会在App第一次启动时请求一次,以后除非App更新或重新安装,否则不会在每次打开时请求apple-app-site-association


    Universal Link的好处


    之前的Custom URL scheme是自定义的协议,因此在没有安装该app的情况下是无法直接打开的。而Universal Links本身就是一个能够指向web页面或者app内容页的标准web link,因此能够很好的兼容其他情况
    Universal links是从服务器上查询是哪个app需要被打开,因此不存在Custom URL scheme那样名字被抢占、冲突的情况
    Universal links支持从其他app中的UIWebView中跳转到目标app
    提供Universal link给别的app进行app间的交流时,对方并不能够用这个方法去检测你的app是否被安装(之前的custom scheme URL的canOpenURL方法可以)

    作者:72行代码
    链接:https://juejin.cn/post/6844903988526055437
    来源:掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
    收起阅读 »

    iOS Instruments使用

    一、Instruments介绍Instruments 一个很灵活的、强大的工具,是性能分析、动态跟踪 和分析OS X以及iOS代码的测试工具,用它可以极为方便收集关于一个或多个系统进程的性能和行为的数据,并能及时随着时间跟踪而产生的数据,并检查所收集的数据,还...
    继续阅读 »

    一、Instruments介绍

    Instruments 一个很灵活的、强大的工具,是性能分析、动态跟踪 和分析OS X以及iOS代码的测试工具,用它可以极为方便收集关于一个或多个系统进程的性能和行为的数据,并能及时随着时间跟踪而产生的数据,并检查所收集的数据,还可以广泛收集不同类型的数据.也可以追踪程序运行的过程,这样instrument就可以帮助我们了解用户的应用程序和操作系统的行为。

    总结一下instrument能做的事情:

    1. Instruments是用于动态调追踪和分析OS X和iOS的代码的性能分析和测试工具;
    2.Instruments支持多线程的调试;
    3.可以用Instruments去录制和回放,图形用户界面的操作过程
    4.可将录制的图形界面操作和Instruments保存为模板,供以后访问使用。
    instrument还可以:

    1.追踪代码中的(甚至是那些难以复制的)问题;
    2.分析程序的性能;
    3.实现程序的自动化测试;
    4.部分实现程序的压力测试;
    5.执行系统级别的通用问题追踪调试;
    6.使你对程序的内部运行过程更加了解。

     打开方式:
    Xcode -> Open Developer Tool -> Instruments


    其中比较常用的有四种:

    1.Allocations:用来检查内存分配,跟踪过程的匿名虚拟内存和堆的对象提供类名和可选保留/释放历史

    2.Leaks:一般的查看内存使用情况,检查泄漏的内存,并提供了所有活动的分配和泄漏模块的类对象分配统计信息以及内存地址历史记录

    3.Time Profiler:分析代码的执行时间,执行对系统的CPU上运行的进程低负载时间为基础采样

    4.Zombies:检查是否访问了僵尸对象

    其他的:

    Blank:创建一个空的模板,可以从Library库中添加其他模板

    Activity Monitor:显示器处理的CPU、内存和网络使用情况统计

    Automation:用JavaScript语言编写,主要用于分析应用的性能和用户行为,模仿/击发被请求的事件,利用它可以完成对被测应用的简单的UI测试及相关功能测试

    Cocoa Layout:观察约束变化,找出布局代码的问题所在。

    Core Animation:用来检测Core Animation性能的,给我们提供了周期性的FPS,并且考虑到了发生在程序之外的动画,界面滑动FPS可以进行测试

    Core Data:监测读取、缓存未命中、保存等操作,能直观显示是否保存次数远超实际需要

    Energy Diagnostic :用于Xcode下的Instruments来分析手机电量消耗的。(必须是真机才有电量)

    GPU Driver :可以测量GPU的利用率,同样也是一个很好的来判断和GPU相关动画性能的指示器。它同样也提供了类似Core Animtaion那样显示FPS的工具。

    Network:分析应用程序如何使用TCP / IP和UDP / IP连接使用连接仪器。就是检查手机网速的。(这个最好是真机)

    二、Allocations(分配)

    1.内存分类:

    Leaked memory:泄漏的内存,如为对象A申请了内存空间,之后再也没用到A,也没有释放A导致内存泄漏(野指针。。。)

    Abandoned memory:被遗弃的内存,如循环引用,递归不断申请内存而导致的内存泄漏

    Cached memory:缓存的内存

    2.Abandoned memory

    其中内存泄漏我们可以用Leaks,野指针可以用Zombies(僵尸对象),而在这里我们就可以用Allocations来检测Abandoned memory的内存。


    即我们采用Generational Analysis的方法来分析,反复进入退出某一场景,查看内存的分配与释放情况,以定位哪些对象是属于Abandoned Memory的范畴。

    在Allocations工具中,有专门的Generational Analysis设置,如下:


    我们可以在程序运行时,在进入某个模块前标记一个Generation,这样会生成一个快照。然后进入、退出,再标记一个Generation,如下图:


    在详情面板中我们可以看到两个Generation间内存的增长情况,其中就可能存在潜在的被遗弃的对象,如下图:


    其中growth就是我们增长的内存,GenerationA是程序启动到进入该场景增长的内存,GenerationB就是第二次进入该场景所增长的内存,查看子类可以发现有两个管理类造成了Abandoned memory

    3.设置Generations

    使用instrument测试内存泄露 工具 Allocations 测试是否内存泄露 使用标记,可以更省事省力的测试页面是否有内存泄露
    1)设置Generations


    2)选择mark generation


    3)使用方法 在进入测试页面之前,mark一下----->进入页面----->退出----->mark------>进入------->退出------->mark------>进入如此往复5、6次,就可以看到如下结果


    这种情况下是内存有泄露,看到每次的增量都是好几百K或者上M的,都是属于内存有泄露的,这时候就需要检测下代码一般情况

    100K以下都属于正常范围,growth表示距离你上次mark的增量

    三、Leaks(泄漏)

    1.内存溢出和内存泄漏的区别

    内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出

    内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。

    memory leak会最终会导致out of memory!

    在前面的ALLcations里面我们提到过内存泄漏就是应该释放而没有释放的内存。而内存泄漏分为两种:Leaked Memory 和 Abandoned Memory。前面我们讲到了如何找到Abandoned Memory被遗忘的内存,现在我们研究的就是Leaked Memory

    发生的方式来分类,内存泄漏可以分为4类:

    常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。
    偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。
    一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,所以内存泄漏只会发生一次。
    隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。

    影响:从用户使用程序的角度来看,内存泄漏本身不会产生什么危害,作为一般的用户,根本感觉不到内存泄漏的存在。真正有危害的是内存泄漏的堆积,这会最终消耗尽系统所有的内存。从这个角度来说,一次性内存泄漏并没有什么危害,因为它不会堆积,而隐式内存泄漏危害性则非常大,因为较之于常发性和偶发性内存泄漏它更难被检测到。

    下边我们介绍Instruments里面的Leaked的用法,首先打开Leaked,跑起工程来,点击要测试的页面,如果有内存泄漏,会出现下图中的红色的❌。然后按照后边的步骤进行修复即可


    上面的旧版的样式,下面的是新版的样式,基本操作差不多



    在详情面板选中显示的若干条中的一条,双击,会自动跳到内存泄露代码处,然后点击右上角 Xcode 图标进行修改。

    下图是对Leaked页面进一步的理解:


    内存泄漏动态分析技巧:

    1.在 Display Settings 界面建议把 Snapshot Interval (snapʃɒt, 数据快照)间隔时间设置为10秒,勾选Automatic Snapshotting,Leaks 会自动进行内存捕捉分析。(新版本直接在底部修改)

    2.熟练使用 Leaks 后会对内存泄漏判断更准确,在可能导致泄漏的操作里,在你怀疑有内存泄漏的操作前和操作后,可以点击 Snapshot Now 进行手动捕捉。

    3.开始时如果设备性能较好,可以把自动捕捉间隔设置为 5 秒钟。

    4.使用ARC的项目,一般内存泄漏都是 malloc、自定义结构、资源引起的,多注意这些地方进行分析。

    5.开启ARC后,内存泄漏的原因,开启了ARC并不是就不会存在内存问题,苹果有句名言:ARC is only for NSObject。

    注:如果你的项目使用了ARC,随着你的操作,不断开启或关闭视图,内存可能持续上升,但这不一定表示存在内存泄漏,ARC释放的时机是不固定的

    这里对 Display Settings中 的 Call tree 选项做一下说明 [官方user guide翻译]:

    Separate By Thread:线程分离,只有这样才能在调用路径中能够清晰看到占用CPU最大的线程.每个线程应该分开考虑。只有这样你才能揪出那些大量占用CPU的"重"线程,按线程分开做分析,这样更容易揪出那些吃资源的问题线程。特别是对于主线程,它要处理和渲染所有的接口数据,一旦受到阻塞,程序必然卡顿或停止响应。

    Invert Call Tree:从上到下跟踪堆栈信息.这个选项可以快捷的看到方法调用路径最深方法占用CPU耗时(这意味着你看到的表中的方法,将已从第0帧开始取样,这通常你是想要的,只有这样你才能看到CPU中花费时间最深的方法),比如FuncA{FunB{FunC}},勾选后堆栈以C->B->A把调用层级最深的C显示最外面.反向输出调用树。把调用层级最深的方法显示在最上面,更容易找到最耗时的操作。

    Hide Missing Symbols:如果dSYM无法找到你的APP或者调用系统框架的话,那么表中将看到调用方法名只能看到16进制的数值,勾选这个选项则可以隐藏这些符号,便于简化分析数据.

    Hide System Libraries:表示隐藏系统的函数,调用这个就更有用了,勾选后耗时调用路径只会显示app耗时的代码,性能分析普遍我们都比较关系自己代码的耗时而不是系统的.基本是必选项.注意有些代码耗时也会纳入系统层级,可以进行勾选前后前后对执行路径进行比对会非常有用.因为通常你只关心cpu花在自己代码上的时间不是系统上的,隐藏系统库文件。过滤掉各种系统调用,只显示自己的代码调用。隐藏缺失符号。如果 dSYM 文件或其他系统架构缺失,列表中会出现很多奇怪的十六进制的数值,用此选项把这些干扰元素屏蔽掉,让列表回归清爽。

    Show Obj-C Only:只显示oc代码 ,如果你的程序是像OpenGl这样的程序,不要勾选侧向因为他有可能是C++的

    Flatten Recursion:递归函数, 每个堆栈跟踪一个条目,拼合递归。将同一递归函数产生的多条堆栈(因为递归函数会调用自己)合并为一条。

    Top Functions:找到最耗时的函数或方法。 一个函数花费的时间直接在该函数中的总和,以及在函数调用该函数所花费的时间的总时间。因此,如果函数A调用B,那么A的时间报告在A花费的时间加上B.花费的时间,这非常真有用,因为它可以让你每次下到调用堆栈时挑最大的时间数字,归零在你最耗时的方法。

    四、Time Profiler(时间分析器)

    用来检测app中每个方法所用的时间,并且可以排序,并查找出哪些函数占用了大量时间。

    使用Time Profile前有两点需要注意的地方:

    1、一定要使用真机调试

    在开始进行应用程序性能分析的时候,一定要使用真机。因为模拟器运行在Mac上,然而Mac上的CPU往往比iOS设备要快。相反,Mac上的GPU和iOS设备的完全不一样,模拟器不得已要在软件层面(CPU)模拟设备的GPU,这意味着GPU相关的操作在模拟器上运行的更慢,尤其是使用CAEAGLLayer来写一些OpenGL的代码时候,这就导致模拟器性能数据和用户真机使用性能数据相去甚远

    2、应用程序一定要使用发布配置

    在发布环境打包的时候,编译器会引入一系列提高性能的优化,例如去掉调试符号或者移除并重新组织代码。另iOS引入一种"Watch Dog"[看门狗]机制,不同的场景下,“看门狗”会监测应用的性能,如果超出了该场景所规定的运行时间,“看门狗”就会强制终结这个应用的进程。开发者可以crashlog看到对应的日志,但Xcode在调试配置下会禁用"Watch Dog"

    1)界面详情:


    2)详细面板


    主要是看Call Tree和Sample List这两种视图:

    3)调用树


    Running Time:函数运行的时间,这个时间是累积时间

    Self:在栈顶次数

    Symbol Name:被调用函数的符号信息

    4)详情面板更多的信息选项


    5)样本列表


    五、Zombies(僵尸)

    1.概念

    翻译英文:专注于检测过度释放的“僵尸”对象。还提供了数据对象分配的类以及所有活动分配内存地址的历史。

    这里我们可以看到一个词语叫“over-release”,过度释放。我们在项目中见到最多的就是“EXC_BAD_ACCESS”或者是这样的:Thread 1: Program received signal:"EXC_BAD_ACCESS",这就是访问了被释放的内存地址造成的

    过度释放,是对同一个对象释放了过多的次数,其实当引用计数降到0时,对象占用的内存已经被释放掉,此时指向原对象的指针就成了“悬垂指针”,如若再对其进行任何方法的调用,(原则上)都会直接crash(然而由于某些特殊的情况,不会马上crash)。过度释放简单的说就是对release的对象再release,就是过度释放

    我们需要知道这几个概念:

    1、内存泄漏:对象使用完没有释放,导致内存浪费。
    2、僵尸对象:已经被销毁的对象(不能再使用的对象)
    3、野指针:指向僵尸对象(不可用内存)的指针。给野指针发消息会报EXC_BAD_ACCECC错误
    4、空指针:没有指向储存空间的指针(里面存的是nil,也就是0)。在oc中使用空指针调中方法不会报错。

    注意:为了避免野指针错误的常见方法:在对象被销毁之后,将指向对象的指针变为空指针。

    对于过度释放的问题,可以直接使用Zombie,当过度释放发生时会立即停在发生问题的位置,同时结合内存分配释放历史和调用栈,可以发现问题。至于上文提到的不会crash的原因,其实有很多,比如:

    对象内存释放时,所用内存并没有完全被擦除,仍有旧对象部分数据可用
    原内存位置被写入同类或同样结构的数据

    2.原理

    我们将僵尸对象“复活”的目的:僵尸对象就是让已经释放了的对象重新复活,便于调试;是为了让已经释放了的对象在被再次访问时能够输出一些错误信息。其实这里的“复活”并不是真的复活,而是强行不死:这么说吧 相当于 他的RC=0的时候 系统再强行让他RC=1,顺便打上一个标记 zoom,等到你去掉那个沟以后 系统会把带有标记zoom的对象RC=0。

    3.用法

    下边是Instruments里面的Zombies的用法:

    在Launch Configuration中勾选Record reference counts和Enable NSZombie detection。其中Recordreference counts是显示引用计数,Enable NSZombie detection是能够检测僵尸对象。


    这样在程序运行的时候,如果发现僵尸对象它就会弹出一个对话框,点击其中“→”按钮,在屏幕的下方会显示僵尸对象的详细信息,下图可以看到僵尸对象的引用计数变化情况。


    注意:Zombies模版在使用的时候会导致内存的飙升,这是因为所有被释放的对象被僵尸对象取代,并未真的释放掉,在结束Zombies时会释放,这是预知行为,这就意味着instrument里的其它工具和Zombies是不能同时使用的,Zombies会导致其它的数据不准。包括leaks,你也不应该把它加到Zombies模版中,即使这么做了结果也没什么意义。对于iOS应用来说,在用Zombies模版时使用iOS模拟器比真机要好

    另外XCode也提供了手动设置NSZombieEnabled环境变量的方法,不过设置NSZombieEnabled为True后,会导致内存占用的增长,同时会影响Leaks工具的调试,这是因为设置NSZombieEnabled会用僵尸对象来代替已释放对象

    点击Product菜单Edit Scheme打开该页面,然后勾选Enable Zombie Objects复选框:


    最后提醒的是NSZombieEnabled只能在调试的时候使用,千万不要忘记在产品发布的时候去掉,因为NSZombieEnabled不会真正去释放dealloc对象的内存,一直开启的话,该死去的对象会一直存在,后果可想而知,自重!

    六、扩展

    野指针

    C语言: 当我们声明1个指针变量,没有为这个指针变量赋初始值.这个指针变量的值是1个垃圾指 指向1块随机的内存空间。

    OC语言: 指针指向的对象已经被回收掉了.这个指针就叫做野指针.

    僵尸对象

    内存回收的本质.

    申请1块空间,实际上是向系统申请1块别人不再使用的空间.
    释放1块空间,指的是占用的空间不再使用,这个时候系统可以分配给别人去使用.
    在这个个空间分配给别人之前 数据还是存在的.
    OC对象释放以后,表示OC对象占用的空间可以分配给别人.
    但是再分配给别人之前 这个空间仍然存在 对象的数据仍然存在.

    僵尸对象: 1个已经被释放的对象 就叫做僵尸对象.

    使用野指针访问僵尸对象.有的时候会出问题,有的时候不会出问题.

    当野指针指向的僵尸对象所占用的空间还没有分配给别人的时候, - 这个时候其实是可以访问的.
    因为对象的数据还在.
    当野指针指向的对象所占用的空间分配给了别人的时候 这个时候访问就会出问题.
    所以,你不要通过1个野指针去访问1个僵尸对象.
    虽然可以通过野指针去访问已经被释放的对象,但是我们不允许这么做.

    僵尸对象检测.

    默认情况下. Xcode不会去检测指针指向的对象是否为1个僵尸对象. 能访问就访问 不能访问就报错.

    可以开启Xcode的僵尸对象检测.

    那么就会在通过指针访问对象的时候,检测这个对象是否为1个僵尸对象 如果是僵尸对象 就会报错.

    为什么不默认开启僵尸对象检测呢?

    因为一旦开启,每次通过指针访问对象的时候.都会去检查指针指向的对象是否为僵尸对象.
    那么这样的话 就影响效率了.

    如何避免僵尸对象报错.

    当1个指针变为野指针以后. 就把这个指针的值设置为nil

    僵尸对象无法复活.

    当1个对象的引用计数器变为0以后 这个对象就被释放了.
    就无法取操作这个僵尸对象了. 所有对这个对象的操作都是无效的.
    因为一旦对象被回收 对象就是1个僵尸对象 而访问1个僵尸对象 是没有意义.

    摘自:https://blog.csdn.net/weixin_41963895/article/details/107231347

    收起阅读 »

    iOS-事件传递&&响应机制(二)

    如果想让某个view不能处理事件(或者说,事件传递到某个view那里就断了),那么可以通过刚才提到的三种方式。比如,设置其userInteractionEnabled = NO;那么传递下来的事件就会由该view的父控件处理。例如,不想让蓝色的view接收事件...
    继续阅读 »


    如果想让某个view不能处理事件(或者说,事件传递到某个view那里就断了),那么可以通过刚才提到的三种方式。比如,设置其userInteractionEnabled = NO;那么传递下来的事件就会由该view的父控件处理。
    例如,不想让蓝色的view接收事件,那么可以设置蓝色的view的userInteractionEnabled = NO;那么点击黄色的view或者蓝色的view所产生的事件,最终会由橙色的view处理,橙色的view就会成为最合适的view。
    所以,不管视图能不能处理事件,只要点击了视图就都会产生事件,关键在于该事件最终是由谁来处理!也就是说,如果蓝色视图不能处理事件,点击蓝色视图产生的触摸事件不会由被点击的视图(蓝色视图)处理!

    注意:如果设置父控件的透明度或者hidden,会直接影响到子控件的透明度和hidden。如果父控件的透明度为0或者hidden = YES,那么子控件也是不可见的!

    3.3.如何寻找最合适的view

    应用如何找到最合适的控件来处理事件?
    1.首先判断主窗口(keyWindow)自己是否能接受触摸事件

    2.触摸点是否在自己身上

    3.从后往前遍历子控件,重复前面的两个步骤(首先查找数组中最后一个元素)

    4.如果没有符合条件的子控件,那么就认为自己最合适处理

    详述:

    1.主窗口接收到应用程序传递过来的事件后,首先判断自己能否接手触摸事件。如果能,那么在判断触摸点在不在窗口自己身上
    2.如果触摸点也在窗口身上,那么窗口会从后往前遍历自己的子控件(遍历自己的子控件只是为了寻找出来最合适的view)
    3.遍历到每一个子控件后,又会重复上面的两个步骤(传递事件给子控件,1.判断子控件能否接受事件,2.点在不在子控件上)
    4.如此循环遍历子控件,直到找到最合适的view,如果没有更合适的子控件,那么自己就成为最合适的view。
    找到最合适的view后,就会调用该view的touches方法处理具体的事件。所以,只有找到最合适的view,把事件传递给最合适的view后,才会调用touches方法进行接下来的事件处理。找不到最合适的view,就不会调用touches方法进行事件处理。

    注意:之所以会采取从后往前遍历子控件的方式寻找最合适的view只是为了做一些循环优化。因为相比较之下,后添加的view在上面,降低循环次数。

    3.3.1.寻找最合适的view底层剖析

    两个重要的方法:
    hitTest:withEvent:方法
    pointInside方法

    3.3.1.1.hitTest:withEvent:方法

    什么时候调用?

    只要事件一传递给一个控件,这个控件就会调用他自己的hitTest:withEvent:方法

    作用

    寻找并返回最合适的view(能够响应事件的那个最合适的view)

    注 意:不管这个控件能不能处理事件,也不管触摸点在不在这个控件上,事件都会先传递给这个控件,随后再调用hitTest:withEvent:方法

    拦截事件的处理

    1.正因为hitTest:withEvent:方法可以返回最合适的view,所以可以通过重写hitTest:withEvent:方法,返回指定的view作为最合适的view。

    2.不管点击哪里,最合适的view都是hitTest:withEvent:方法中返回的那个view。

    3.通过重写hitTest:withEvent:,就可以拦截事件的传递过程,想让谁处理事件谁就处理事件。

    事件传递给谁,就会调用谁的hitTest:withEvent:方法。
    注 意:如果hitTest:withEvent:方法中返回nil,那么调用该方法的控件本身和其子控件都不是最合适的view,也就是在自己身上没有找到更合适的view。那么最合适的view就是该控件的父控件。

    所以事件的传递顺序是这样的:

     产生触摸事件->UIApplication事件队列->[UIWindow hitTest:withEvent:]->返回更合适的view->[子控件 hitTest:withEvent:]->返回最合适的view

    事件传递给窗口或控件的后,就调用hitTest:withEvent:方法寻找更合适的view。所以是,先传递事件,再根据事件在自己身上找更合适的view。

         不管子控件是不是最合适的view,系统默认都要先把事件传递给子控件,经过子控件调用子控件自己的hitTest:withEvent:方法验证后才知道有没有更合适的view。即便父控件是最合适的view了,子控件的hitTest:withEvent:方法还是会调用,不然怎么知道有没有更合适的!即,如果确定最终父控件是最合适的view,那么该父控件的子控件的hitTest:withEvent:方法也是会被调用的。

    技巧:想让谁成为最合适的view就重写谁自己的父控件的hitTest:withEvent:方法返回指定的子控件,或者重写自己的hitTest:withEvent:方法 return self。但是,建议在父控件的hitTest:withEvent:中返回子控件作为最合适的view!

    原因在于在自己的hitTest:withEvent:方法中返回自己有时候会出现问题。因为会存在这么一种情况:当遍历子控件时,如果触摸点不在子控件A自己身上而是在子控件B身上,还要要求返回子控件A作为最合适的view,采用返回自己的方法可能会导致还没有来得及遍历A自己,就有可能已经遍历了点真正所在的view,也就是B。这就导致了返回的不是自己而是触摸点真正所在的view。所以还是建议在父控件的hitTest:withEvent:中返回子控件作为最合适的view!

    例如:whiteView有redView和greenView两个子控件。redView先添加,greenView后添加。如果要求无论点击那里都要让redView作为最合适的view(把事件交给redView来处理)那么只能在whiteView的hitTest:withEvent:方法中return self.subViews[0];这种情况下在redView的hitTest:withEvent:方法中return self;是不好使的!

    // 这里redView是whiteView的第0个子控件
    #import "redView.h"

    @implementation redView
    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    return self;
    }
    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    NSLog(@"red-touch");
    }@end
    // 或者
    #import "whiteView.h"

    @implementation whiteView
    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    return self.subviews[0];
    }
    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    NSLog(@"white-touch");
    }
    @end

    特殊情况:

    谁都不能处理事件,窗口也不能处理。

    重写window的hitTest:withEvent:方法return nil

    只能有窗口处理事件。

    控制器的view的hitTest:withEvent:方法return nil或者window的hitTest:withEvent:方法return self

    return nil的含义:

    hitTest:withEvent:中return nil的意思是调用当前hitTest:withEvent:方法的view不是合适的view,子控件也不是合适的view。如果同级的兄弟控件也没有合适的view,那么最合适的view就是父控件。

    寻找最合适的view底层剖析之hitTest:withEvent:方法底层做法

    /********************************* hitTest:withEvent:方法底层实现********************************/

    #import "WYWindow.h"
    @implementation WYWindow
    // 什么时候调用:只要事件一传递给一个控件,那么这个控件就会调用自己的这个方法
    // 作用:寻找并返回最合适的view
    // UIApplication -> [UIWindow hitTest:withEvent:]寻找最合适的view告诉系统
    // point:当前手指触摸的点
    // point:是方法调用者坐标系上的点
    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    // 1.判断下窗口能否接收事件
    if (self.userInteractionEnabled == NO || self.hidden == YES || self.alpha <= 0.01) return nil;
    // 2.判断下点在不在窗口上
    // 不在窗口上
    if ([self pointInside:point withEvent:event] == NO) return nil;
    // 3.从后往前遍历子控件数组
    int count = (int)self.subviews.count;
    for (int i = count - 1; i >= 0; i--) {
    // 获取子控件
    UIView *childView = self.subviews[i];
    // 坐标系的转换,把窗口上的点转换为子控件上的点
    // 把自己控件上的点转换成子控件上的点
    CGPoint childP = [self convertPoint:point toView:childView];
    UIView *fitView = [childView hitTest:childP withEvent:event];
    if (fitView) {
    // 如果能找到最合适的view
    return fitView;
    }
    }
    // 4.没有找到更合适的view,也就是没有比自己更合适的view
    return self;
    }
    // 作用:判断下传入过来的点在不在方法调用者的坐标系上
    // point:是方法调用者坐标系上的点
    //- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
    //{
    // return NO;
    //}
    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    NSLog(@"%s",__func__);
    }
    @end

    hit:withEvent:方法底层会调用pointInside:withEvent:方法判断点在不在方法调用者的坐标系上。

    3.3.1.2.pointInside:withEvent:方法

    pointInside:withEvent:方法判断点在不在当前view上(方法调用者的坐标系上)如果返回YES,代表点在方法调用者的坐标系上;返回NO代表点不在方法调用者的坐标系上,那么方法调用者也就不能处理事件。

    3.3.2.练习

    屏幕上现在有一个viewA,viewA有一个subView叫做viewB,要求触摸viewB时,viewB会响应事件,而触摸viewA本身,不会响应该事件。如何实现?

    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event{
    UIView *view = [super hitTest:point withEvent:event];
    if (view == self) {
    return nil;
    }
    return view;
    }

    (四)事件的响应

    4.1.触摸事件处理的整体过程

    1>用户点击屏幕后产生的一个触摸事件,经过一系列的传递过程后,会找到最合适的视图控件来处理这个事件

    2>找到最合适的视图控件后,就会调用控件的touches方法来作具体的事件处理touchesBegan…touchesMoved…touchedEnded…3>这些touches方法的默认做法是将事件顺着响应者链条向上传递(也就是touch方法默认不处理事件,只传递事件),将事件交给上一个响应者进行处理

    4.2.响应者链条示意图

    响应者链条:

    在iOS程序中无论是最后面的UIWindow还是最前面的某个按钮,它们的摆放是有前后关系的,一个控件可以放到另一个控件上面或下面,那么用户点击某个控件时是触发上面的控件还是下面的控件呢,这种先后关系构成一个链条就叫“响应者链”。也可以说,响应者链是由多个响应者对象连接起来的链条。在iOS中响应者链的关系可以用下图表示


    响应者对象:能处理事件的对象,也就是继承自UIResponder的对象
    作用:能很清楚的看见每个响应者之间的联系,并且可以让一个事件多个对象处理。

    如何判断上一个响应者

    1> 如果当前这个view是控制器的view,那么控制器就是上一个响应者

    2> 如果当前这个view不是控制器的view,那么父控件就是上一个响应者

    响应者链的事件传递过程:

    1>如果当前view是控制器的view,那么控制器就是上一个响应者,事件就传递给控制器;如果当前view不是控制器的view,那么父视图就是当前view的上一个响应者,事件就传递给它的父视图

    2>在视图层次结构的最顶级视图,如果也不能处理收到的事件或消息,则其将事件或消息传递给window对象进行处理

    3>如果window对象也不处理,则其将事件或消息传递给UIApplication对象

    4>如果UIApplication也不能处理该事件或消息,则将其丢弃

    事件处理的整个流程总结:

    1.触摸屏幕产生触摸事件后,触摸事件会被添加到由UIApplication管理的事件队列中(即,首先接收到事件的是UIApplication)。

    2.UIApplication会从事件队列中取出最前面的事件,把事件传递给应用程序的主窗口(keyWindow)。

    3.主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件。(至此,第一步已完成)

    4.最合适的view会调用自己的touches方法处理事件

    5.touches默认做法是把事件顺着响应者链条向上抛。

    touches的默认做法:#import "WYView.h"

    @implementation WYView 
    //只要点击控件,就会调用touchBegin,如果没有重写这个方法,自己处理不了触摸事件
    // 上一个响应者可能是父控件
    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    // 默认会把事件传递给上一个响应者,上一个响应者是父控件,交给父控件处理
    [super touchesBegan:touches withEvent:event];
    // 注意不是调用父控件的touches方法,而是调用父类的touches方法
    // super是父类 superview是父控件
    }
    @end

    事件的传递与响应:

    1、当一个事件发生后,事件会从父控件传给子控件,也就是说由UIApplication -> UIWindow -> UIView -> initial view,以上就是事件的传递,也就是寻找最合适的view的过程。

    2、接下来是事件的响应。首先看initial view能否处理这个事件,如果不能则会将事件传递给其上级视图(inital view的superView);如果上级视图仍然无法处理则会继续往上传递;一直传递到视图控制器view controller,首先判断视图控制器的根视图view是否能处理此事件;如果不能则接着判断该视图控制器能否处理此事件,如果还是不能则继续向上传 递;(对于第二个图视图控制器本身还在另一个视图控制器中,则继续交给父视图控制器的根视图,如果根视图不能处理则交给父视图控制器处理);一直到 window,如果window还是不能处理此事件则继续交给application处理,如果最后application还是不能处理此事件则将其丢弃

    3、在事件的响应中,如果某个控件实现了touches...方法,则这个事件将由该控件来接受,如果调用了[super touches….];就会将事件顺着响应者链条往上传递,传递给上一个响应者;接着就会调用上一个响应者的touches….方法

    如何做到一个事件多个对象处理:

    因为系统默认做法是把事件上抛给父控件,所以可以通过重写自己的touches方法和父控件的touches方法来达到一个事件多个对象处理的目的。

    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ 
    // 1.自己先处理事件...
    NSLog(@"do somthing...");
    // 2.再调用系统的默认做法,再把事件交给上一个响应者处理
    [super touchesBegan:touches withEvent:event];
    }

    事件处理的整个流程总结:

    1.触摸屏幕产生触摸事件后,触摸事件会被添加到由UIApplication管理的事件队列中(即,首先接收到事件的是UIApplication)。

    2.UIApplication会从事件队列中取出最前面的事件,把事件传递给应用程序的主窗口(keyWindow)。

    3.主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件。(至此,第一步已完成)

    4.最合适的view会调用自己的touches方法处理事件

    5.touches默认做法是把事件顺着响应者链条向上抛。



    收起阅读 »

    iOS-事件传递&&响应机制(一)

    前言:按照时间顺序,事件的生命周期:  事件的产生和传递(事件如何从父控件传递到子控件并寻找到最合适的view、寻找最合适的view的底层实现、拦截事件的处理)->找到最合适的view后事件的处理(touches方法的重写,也就是事件的响应)重点和难点是:  ...
    继续阅读 »

    前言:

    按照时间顺序,事件的生命周期:
      事件的产生和传递(事件如何从父控件传递到子控件并寻找到最合适的view、寻找最合适的view的底层实现、拦截事件的处理)->找到最合适的view后事件的处理(touches方法的重写,也就是事件的响应)

    重点和难点是:
      1.如何寻找最合适的view
      2.寻找最合适的view的底层实现(hitTest:withEvent:底层实现)

    (一)iOS中的事件

    iOS中的事件可以分为3大类型:
    1.触摸事件

    2.加速计事件

    3.远程控制事件

    这里我们只讨论iOS中的触摸事件。

    1.1.响应者对象(UIResponder)

    学习触摸事件首先要了解一个比较重要的概念-响应者对象(UIResponder)。

    在iOS中不是任何对象都能处理事件,只有继承了UIResponder的对象才能接受并处理事件,我们称之为“响应者对象”。以下都是继承自UIResponder的,所以都能接收并处理事件。

    UIApplication
    UIWindows
    UIViewController
    UIView

    那么为什么继承自UIResponder的类就能够接收并处理事件呢?

    因为UIResponder中提供了以下4个对象方法来处理触摸事件。UIResponder内部提供了以下方法来处理事件触摸事件

    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event;
    - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event;
    - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event;
    - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event;
    加速计事件
    - (void)motionBegan:(UIEventSubtype)motion withEvent:(UIEvent *)event;
    - (void)motionEnded:(UIEventSubtype)motion withEvent:(UIEvent *)event;
    - (void)motionCancelled:(UIEventSubtype)motion withEvent:(UIEvent *)event;
    远程控制事件
    - (void)remoteControlReceivedWithEvent:(UIEvent *)event;

    (二)事件的处理

    下面以UIView为例来说明触摸事件的处理。

    // UIView是UIResponder的子类,可以覆盖下列4个方法处理不同的触摸事件
    // 一根或者多根手指开始触摸view,系统会自动调用view的下面方法
    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
    // 一根或者多根手指在view上移动,系统会自动调用view的下面方法(随着手指的移动,会持续调用该方法)
    - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
    // 一根或者多根手指离开view,系统会自动调用view的下面方法
    - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event
    // 触摸结束前,某个系统事件(例如电话呼入)会打断触摸过程,系统会自动调用view的下面方法
    - (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event
    // 提示:touches中存放的都是UITouch对象

    需要注意的是:以上四个方法是由系统自动调用的,所以可以通过重写该方法来处理一些事件。

    1.如果两根手指同时触摸一个view,那么view只会调用一次touchesBegan:withEvent:方法,touches参数中装着2个UITouch对象

    2.如果这两根手指一前一后分开触摸同一个view,那么view会分别调用2次touchesBegan:withEvent:方法,并且每次调用时的touches参数中只包含一个UITouch对象

    3.重写以上四个方法,如果是处理UIView的触摸事件。必须要自定义UIView子类继承自UIView。因为苹果不开源,没有把UIView的.m文件提 供给我们。我们只能通过子类继承父类,重写子类方法的方式处理UIView的触摸事件(注意:我说的是UIView触摸事件而不是说的 UIViewController的触摸事件)。

    4.如果是处理UIViewController的触摸事件,那么在控制器的.m文件中直接重写那四个方法即可!

    /************************自定义UIView的.h.m文件************************/

    #import 

    @interface WYView : UIView
    @end
    #import "WYView.h"
    @implementation WYView
    // 开始触摸时就会调用一次这个方法
    - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    NSLog(@"摸我干啥!");
    }
    // 手指移动就会调用这个方法
    // 这个方法调用非常频繁
    - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{
    NSLog(@"移动过程中持续调用!");
    }
    // 手指离开屏幕时就会调用一次这个方法
    - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
    NSLog(@"手放开还能继续玩耍!");
    }
    @end

    /**************************控制器的.m文件*************************/

    #import "ViewController.h"
    #import "WYView.h"
    @interface ViewController ()
    @end@implementation ViewController
    - (void)viewDidLoad {
    [super viewDidLoad];
    // 创建自定义view
    WYView *touchView = [[WYView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
    // 背景颜色
    touchView.backgroundColor = [UIColor redColor];
    // 添加到父控件
    [self.view addSubview:touchView];
    }
    @end

    注 意:有人认为,我要是处理控制器的自带的view的事件就不需要自定义UIView子类继承于UIView,因为可以在viewController.m 文件中重写touchBegan:withEvent:方法,但是,我们此处讨论的是处理UIView的触摸事件,而不是处理 UIViewController的触摸事件。你如果是在viewController.m文件中重写touchBegan:withEvent:方法,相当于处理的是viewController的触摸事件,因为viewController也是继承自UIResponder,所以会给人一种错觉。
    所以,还是那句话,想处理UIView的触摸事件,必须自定义UIView子类继承自UIView。

    2.1.UIView的拖拽

    那么,如何实现UIView的拖拽呢?也就是让UIView随着手指的移动而移动。
      - 重写touchsMoved:withEvent:方法
    此时需要用到参数touches,下面是UITouch的属性和方法:

    NS_CLASS_AVAILABLE_IOS(2_0) @interface UITouch : NSObject

    @property(nonatomic,readonly) NSTimeInterval timestamp;
    @property(nonatomic,readonly) UITouchPhase phase;
    @property(nonatomic,readonly) NSUInteger tapCount; // touch down within a certain point within a certain amount of time

    // majorRadius and majorRadiusTolerance are in points
    // The majorRadius will be accurate +/- the majorRadiusTolerance
    @property(nonatomic,readonly) CGFloat majorRadius NS_AVAILABLE_IOS(8_0);
    @property(nonatomic,readonly) CGFloat majorRadiusTolerance NS_AVAILABLE_IOS(8_0);

    @property(nullable,nonatomic,readonly,strong) UIWindow *window;
    @property(nullable,nonatomic,readonly,strong) UIView *view;
    @property(nullable,nonatomic,readonly,copy) NSArray *gestureRecognizers NS_AVAILABLE_IOS(3_2);

    - (CGPoint)locationInView:(nullable UIView *)view;
    - (CGPoint)previousLocationInView:(nullable UIView *)view;

    // Force of the touch, where 1.0 represents the force of an average touch
    @property(nonatomic,readonly) CGFloat force NS_AVAILABLE_IOS(9_0);
    // Maximum possible force with this input mechanism
    @property(nonatomic,readonly) CGFloat maximumPossibleForce NS_AVAILABLE_IOS(9_0);

    2.1.1.UITouch对象

    当用户用一根手指触摸屏幕时,会创建一个与手指相关的UITouch对象

    一根手指对应一个UITouch对象

    如果两根手指同时触摸一个view,那么view只会调用一次touchesBegan:withEvent:方法,touches参数中装着2个UITouch对象

    如果这两根手指一前一后分开触摸同一个view,那么view会分别调用2次touchesBegan:withEvent:方法,并且每次调用时的touches参数中只包含一个UITouch对象

    2.1.1.1.UITouch的作用

      保存着跟手指相关的信息,比如触摸的位置、时间、阶段

      当手指移动时,系统会更新同一个UITouch对象,使之能够一直保存该手指在的触摸位置

      当手指离开屏幕时,系统会销毁相应的UITouch对象
      提 示:iPhone开发中,要避免使用双击事件!

      2.1.1.2.UITouch的属性

      触摸产生时所处的窗口
      @property(nonatomic,readonly,retain) UIWindow *window;

      触摸产生时所处的视图
      @property(nonatomic,readonly,retain) UIView *view
      ;

      短时间内点按屏幕的次数,可以根据tapCount判断单击、双击或更多的点击
      @property(nonatomic,readonly) NSUInteger tapCount;

      记录了触摸事件产生或变化时的时间,单位是秒
      @property(nonatomic,readonly) NSTimeInterval timestamp;

      当前触摸事件所处的状态
      @property(nonatomic,readonly) UITouchPhase phase;

      2.1.1.3.UITouch的方法

      (CGPoint)locationInView:(UIView *)view;
      // 返回值表示触摸在view上的位置
      // 这里返回的位置是针对view的坐标系的(以view的左上角为原点(0, 0))
      // 调用时传入的view参数为nil的话,返回的是触摸点在UIWindow的位置

      (CGPoint)previousLocationInView:(UIView *)view;
      // 该方法记录了前一个触摸点的位置

      - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event{ 
      // 想让控件随着手指移动而移动,监听手指移动
      // 获取UITouch对象
      UITouch *touch = [touches anyObject];
      // 获取当前点的位置
      CGPoint curP = [touch locationInView:self];
      // 获取上一个点的位置
      CGPoint preP = [touch previousLocationInView:self];
      // 获取它们x轴的偏移量,每次都是相对上一次
      CGFloat offsetX = curP.x - preP.x;
      // 获取y轴的偏移量
      CGFloat offsetY = curP.y - preP.y;
      // 修改控件的形变或者frame,center,就可以控制控件的位置
      // 形变也是相对上一次形变(平移)
      // CGAffineTransformMakeTranslation:会把之前形变给清空,重新开始设置形变参数
      // make:相对于最原始的位置形变
      // CGAffineTransform t:相对这个t的形变的基础上再去形变
      // 如果相对哪个形变再次形变,就传入它的形变
      self.transform = CGAffineTransformTranslate(self.transform, offsetX, offsetY);}

      (三)iOS中的事件的产生和传递

      3.1.事件的产生

      发生触摸事件后,系统会将该事件加入到一个由UIApplication管理的事件队列中,为什么是队列而不是栈?因为队列的特点是FIFO,即先进先出,先产生的事件先处理才符合常理,所以把事件添加到队列。

      UIApplication会从事件队列中取出最前面的事件,并将事件分发下去以便处理,通常,先发送事件给应用程序的主窗口(keyWindow)。

      主窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件,这也是整个事件处理过程的第一步。

      找到合适的视图控件后,就会调用视图控件的touches方法来作具体的事件处理。

      3.2.事件的传递

      触摸事件的传递是从父控件传递到子控件
      也就是UIApplication->window->寻找处理事件最合适的view
      注 意: 如果父控件不能接受触摸事件,那么子控件就不可能接收到触摸事件

      应用如何找到最合适的控件来处理事件?

      1.首先判断主窗口(keyWindow)自己是否能接受触摸事件

      2.判断触摸点是否在自己身上

      3.子控件数组中从后往前遍历子控件,重复前面的两个步骤(所谓从后往前遍历子控件,就是首先查找子控件数组中最后一个元素,然后执行1、2步骤)

      4.view,比如叫做fitView,那么会把这个事件交给这个fitView,再遍历这个fitView的子控件,直至没有更合适的view为止。

      5.如果没有符合条件的子控件,那么就认为自己最合适处理这个事件,也就是自己是最合适的view。

      UIView不能接收触摸事件的三种情况:

      1.不允许交互:userInteractionEnabled = NO
      2.隐藏:如果把父控件隐藏,那么子控件也会隐藏,隐藏的控件不能接受事件
      3.透明度:如果设置一个控件的透明度<0.01,会直接影响子控件的透明度。alpha:0.0~0.01为透明。

      注 意:默认UIImageView不能接受触摸事件,因为不允许交互,即userInteractionEnabled = NO。所以如果希望UIImageView可以交互,需要设置UIImageView的userInteractionEnabled = YES。

      总结一下

      1.点击一个UIView或产生一个触摸事件A,这个触摸事件A会被添加到由UIApplication管理的事件队列中(即,首先接收到事件的是UIApplication)。

      2.UIApplication会从事件对列中取出最前面的事件(此处假设为触摸事件A),把事件A传递给应用程序的主窗口(keyWindow)。

      3.窗口会在视图层次结构中找到一个最合适的视图来处理触摸事件。(至此,第一步已完成)

    摘自链接:https://blog.csdn.net/wywinstonwy/article/details/105293525

    收起阅读 »

    iOS-异步绘制原理

    在 UIView 中有一个 CALayer 的属性,负责 UIView 具体内容的显示。具体过程是系统会把 UIView 显示的内容(包括 UILabel 的文字,UIImageView 的图片等)绘制在一张画布上,完成后倒出图片赋值给 CALayer 的 c...
    继续阅读 »

    在 UIView 中有一个 CALayer 的属性,负责 UIView 具体内容的显示。具体过程是系统会把 UIView 显示的内容(包括 UILabel 的文字,UIImageView 的图片等)绘制在一张画布上,完成后倒出图片赋值给 CALayer 的 contents 属性,完成显示。

    这其中的工作都是在主线程中完成的,这就导致了主线程频繁的处理 UI 绘制的工作,如果要绘制的元素过多,过于频繁,就会造成卡顿。

    那么是否可以将复杂的绘制过程放到后台线程中执行,从而减轻主线程负担,来提升 UI 流畅度呢?

    答案是可以的,系统给我们留下的异步绘制的口子,请看下面的流程图,它是我们进行基本绘制的基础。

    UIView 调用 setNeedsDisplay 方法其实是调用其 layer 属性的同名方法,这时 layer 并不会立刻调用 display 方法,而是要等到当前 runloop 即将结束的时候调用 display,进入到绘制流程。在 UIView 中 layer.delegate 就是 UIView 本身,UIView 并没有实现 displayLayer: 方法,所以进入系统的绘制流程,我们可以通过实现 displayLayer: 方法来进行异步绘制。

    有了上面的异步绘制原理流程图,我们可以得到一个实现异步绘制的初步思路:
    在“异步绘制入口”去开辟子线程,然后在子线程中实现和系统类似的绘制流程。

    二、系统绘制流程

    要实现异步绘制,我们首先要了解系统的绘制流程,看下面一张流程图:


    三、异步绘制流程

    我们看一幅时序图


    #import "AsyncDrawLabel.h"
    #import <CoreText/CoreText.h>

    @implementation AsyncDrawLabel

    - (void)setText:(NSString *)text {
    _text = text;
    [self.layer setNeedsDisplay];
    }

    - (void)setFont:(UIFont *)font {
    _font = font;
    [self.layer setNeedsDisplay];
    }

    - (void)displayLayer:(CALayer *)layer {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    __block CGSize size;
    dispatch_sync(dispatch_get_main_queue(), ^{
    size = self.bounds.size;
    });
    UIGraphicsBeginImageContextWithOptions(size, NO, UIScreen.mainScreen.scale);
    CGContextRef context = UIGraphicsGetCurrentContext();
    [self draw:context size:size];
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    dispatch_async(dispatch_get_main_queue(), ^{
    self.layer.contents = (__bridge id)image.CGImage;
    });
    });
    }

    - (void)draw:(CGContextRef)context size:(CGSize)size {
    // 将坐标系上下翻转,因为底层坐标系和 UIKit 坐标系原点位置不同。
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    CGContextTranslateCTM(context, 0, size.height); // 原点为左下角
    CGContextScaleCTM(context, 1, -1);

    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, CGRectMake(0, 0, size.width, size.height));

    NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc]initWithString:self.text];
    [attrStr addAttribute:NSFontAttributeName value:self.font range:NSMakeRange(0, self.text.length)];

    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrStr);
    CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attrStr.length), path, NULL);
    CTFrameDraw(frame, context);
    }

    @end

    AsyncDrawLabel 是一个继承 UIView 的类,其 Label 的文本绘制功能需要我们自己实现。

    我们在 - (void)displayLayer:(CALayer *)layer 方法中异步在全局队列中创建上下文环境然后使用 - (void)draw:(CGContextRef)context size:(CGSize)size 方法进行文本的简单绘制,再回到主线程为 self.layer.contents 赋值。从而完成了一个简单的异步绘制。

    当然这样的绘制的问题是,如果绘制数量较多,绘制频繁,会阻塞全局队列,因为全局队列中还有一些系统提交的任务需要执行,可能会对其造成影响。

    YYAsyncLayer

    我们需要更加优化的方式去管理异步绘制的线程和执行流程,使用 YYAsyncLayer 可以让我们把注意力放在具体的绘制(需要我们做的是上面代码中 - draw: size: 做的事情),而不需要考虑线程的管理,绘制的时机等,大大提高绘制的效率以及我们编程的速度。

    YYAsyncLayer 的主要流程如下

    在主线程的 RunLoop 中注册一个 observer,它的优先级要比系统的 CATransaction 低,保证系统先做完必须的工作。

    把需要异步绘制的操作集中起来。比如设置字体、颜色、背景色等,不是设置一个就绘制一个,而是把它们集中起来,RunLoop 会在 observer 需要的时机通知统一处理。

    处理时机到时,执行异步绘制,并在主线程中把绘制结果传递给 layer.contents。

    流程图如下:


    使用 YYAsyncLayer 的代码:

    #import "AsyncDrawLabel.h"
    #import <YYAsyncLayer.h>
    #import <CoreText/CoreText.h>

    @interface AsyncDrawLabel ()<YYAsyncLayerDelegate>

    @end

    @implementation AsyncDrawLabel

    + (Class)layerClass {
    return YYAsyncLayer.class;
    }

    - (void)setText:(NSString *)text {
    _text = text.copy;
    [self commitTransaction];
    }

    - (void)setFont:(UIFont *)font {
    _font = font;
    [self commitTransaction];
    }

    - (void)layoutSubviews {
    [super layoutSubviews];
    [self commitTransaction];
    }

    - (void)contentsNeedUpdated {
    [self.layer setNeedsDisplay];
    }

    - (void)commitTransaction {
    [[YYTransaction transactionWithTarget:self selector:@selector(contentsNeedUpdated)] commit];
    }

    // 在这里创建异步绘制的任务
    - (YYAsyncLayerDisplayTask *)newAsyncDisplayTask {
    YYAsyncLayerDisplayTask *task = [YYAsyncLayerDisplayTask new];
    task.willDisplay = ^(CALayer * _Nonnull layer) {

    };
    task.display = ^(CGContextRef _Nonnull context, CGSize size, BOOL (^ _Nonnull isCancelled)(void)) {
    if (isCancelled() || self.text.length == 0) {
    return;
    }
    // 在这里进行异步绘制
    [self draw:context size:size];
    };
    task.didDisplay = ^(CALayer * _Nonnull layer, BOOL finished) {
    if (finished) {

    } else {

    }
    };
    return task;
    }

    - (void)draw:(CGContextRef)context size:(CGSize)size {
    // 将坐标系上下翻转,因为底层坐标系和 UIKit 坐标系原点位置不同。
    CGContextSetTextMatrix(context, CGAffineTransformIdentity);
    CGContextTranslateCTM(context, 0, size.height); // 原点为左下角
    CGContextScaleCTM(context, 1, -1);

    CGMutablePathRef path = CGPathCreateMutable();
    CGPathAddRect(path, NULL, CGRectMake(0, 0, size.width, size.height));

    NSMutableAttributedString *attrStr = [[NSMutableAttributedString alloc]initWithString:self.text];
    [attrStr addAttribute:NSFontAttributeName value:self.font range:NSMakeRange(0, self.text.length)];

    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)attrStr);
    CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attrStr.length), path, NULL);
    CTFrameDraw(frame, context);
    }
    @end

    原文链接:https://blog.csdn.net/wywinstonwy/article/details/105660643

    收起阅读 »

    iOS-视图&图像相关

    Auto Layout 原理Auto Layout是一种全新的布局方式,它采用一系列约束(constraints)来实现自动布局,当你的屏幕尺寸发生变化或者屏幕发生旋转时,可以不用添加代码来保持原有布局不变,实现视图的自动布局。所谓约束,通常是定义了两个视图之...
    继续阅读 »

    Auto Layout 原理

    Auto Layout是一种全新的布局方式,它采用一系列约束(constraints)来实现自动布局,当你的屏幕尺寸发生变化或者屏幕发生旋转时,可以不用添加代码来保持原有布局不变,实现视图的自动布局。

    所谓约束,通常是定义了两个视图之间的关系(当然你也可以一个视图自己跟自己设定约束)。如下图就是一个约束的例子,当然要确定一个视图的位置,跟基于frame一样,也是需要确定视图的横纵坐标以及宽度和高度的,只是,这个横纵坐标和宽度高度不再是写死的数值,而是根据约束计算得来,从而达到自动布局的效果

    UIView之drawRect: & layoutSubviews的作用和机制

    drawRect 调用机制

    1、调用时机:loadView ->ViewDidload ->drawRect:

    2、如果在UIView初始化时没有设置rect大小,将直接导致drawRect:不被自动调用。

    3、通过设置contentMode属性值为UIViewContentModeRedraw。那么将在每次设置或更改frame的时候自动调用drawRect:

    4、直接调用setNeedsDisplay,或者setNeedsDisplayInRect:触发drawRect:,但是有个前提条件是:view当前的rect不能为nil

    5、该方法在调用sizeThatFits后被调用,所以可以先调用sizeToFit计算出size。然后系统自动调用drawRect:方法。

    这里简单说一下sizeToFit和sizeThatFit:
    sizeToFit:会计算出最优的 size 而且会改变自己的size
    sizeThatFits:会计算出最优的 size 但是不会改变 自己的 size

    注意事项
    1、若使用UIView绘图,只能在drawRect:方法中获取相应的contextRef并绘图。如果在其他方法中获取到一个invalidate的ref保存下来,在drawRect中并不能用于画图。等到在这里调用时,可能当前上下文环境已经变化。
    2、若使用CALayer绘图,只能在drawInContext: 中(类似于drawRect)绘制,或者在delegate中的相应方法绘制。同样也是调用setNeedDisplay等间接调用以上方法。
    3、若要实时画图,不能使用gestureRecognizer,只能使用touchbegan等方法来掉用setNeedsDisplay实时刷新屏幕。
    4、UIImageView继承自UIView,但是UIImageView能不重写drawRect方法用于实现自定义绘图。具体原因如下:
    Apple在文档中指出:UIImageView是专门为显示图片做的控件,用了最优显示技术,是不让调用darwrect方法, 要调用这个方法,只能从uiview里重写。

    layoutSubviews

    这个方法是用来对subviews重新布局,默认没有做任何事情,需要子类进行重写。
    当我们在某个类的内部调整子视图位置时,需要调用。

    反过来的意思就是说:如果你想要在外部设置subviews的位置,就不要重写。

    ①、- (void)layoutSubviews;
    对subview重新布局
    ②、- (void)setNeedsLayout;
    将视图标记为需要重新布局, 这个方法会在系统runloop的下一个周期自动调用layoutSubviews。
    ③、- (void)layoutIfNeeded;
    如果有需要刷新的标记立即调用layoutSubviews进行布局(如果没有标记,不会调用layoutSubviews)

    这里注意一个点:标记,没有标记,即使我们掉了该函数也不起作用
    如果要立即刷新,要先调用[view setNeedsLayout],把标记设为需要布局,然后马上调用[view layoutIfNeeded],实现布局.
    在视图第一次显示之前,标记总是“需要刷新”的,可以直接调用[view layoutIfNeeded]

    这里有必要描述下三者之间的关系:
    在没有外界干预的情况下,一个view的frame或者bounds发生变化时,系统会先去标记flag这个view,等下一次渲染时机到来时(也就是runloop的下一次循环),会去按照最新的布局去重新布局视图。
    setNeedLayout就是给这个view添加一个标记,告诉系统下一次渲染时机需要重新布局这个视图。
    layoutIfNeed就是告诉系统,如果已经设置了flag,那不用等待下个渲染时机到来,立即重新渲染。前提是设置了flag。
    layoutSubviews则是由系统去调用,不需要我们主动调用,我们只需要调用layoutIfNeed,告诉系统是否立即执行重新布局的操作。

    layoutSubviews调用时机

    结论是经过搜索得到的,基于此笔者进行了验证,并得到了些结果:
    1、init初始化不会触发layoutSubviews。
    2、addSubview会触发layoutSubviews。(当然这里frame为0,是不会调用的,同上面的drawrect:一样)
    3、设置view的Frame会触发layoutSubviews,(当然前提是frame的值设置前后发生了变化。)
    4、滚动一个UIScrollView会触发layoutSubviews。
    5、旋转屏幕会触发父UIView上的layoutSubviews事件。(这个我们开发中会经常遇到,比如屏幕旋转时,为了界面美观我们需要修改子view的frame,那就会在layoutSubview中做相应的操作)
    6、改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件。
    7、直接调用setLayoutSubviews。(Apple是不建议这么做的)
    这里需要补充一点:layoutSubview是布局相关,而drawRect则是负责绘制。因此从调用时序上来讲,layoutSubviews要早于drawRect:函数。

    摘自链接:https://www.jianshu.com/p/2ab322e1c7d4
    收起阅读 »

    iOS底层系列:Category

    前言Category是我们平时用到的比较多的一种技术,比如说给某个类增加方法,添加成员变量,或者用Category优化代码结构。我们通过下面这几个问题作为切入点,结合runtime的源码,探究一下Category的底层原理。我们在Category中,可以直接添...
    继续阅读 »

    前言

    Category是我们平时用到的比较多的一种技术,比如说给某个类增加方法,添加成员变量,或者用Category优化代码结构。
    我们通过下面这几个问题作为切入点,结合runtime的源码,探究一下Category的底层原理。
    我们在Category中,可以直接添加方法,而且我们也都知道,添加的方法会合并到本类当中,同时我们也可以声明属性,但是此时的属性没有功能,也就是不能存值,这就类似于Swift中的计算属性,如果我们想让这个属性可以储存值,就要用runtime的方式,动态的添加。

    探究

    1. Category为什么能添加方法不能添加成员变量
    首先我们先创建一个Person类,然后创建一个Person+Run的Category,并在Person+Run中实现-run方法。
    我们可以使用命令行对Person+Run.m进行编译

    xcrun -sdk iphonesimulator clang -rewrite-objc Person+Run.m

    得到一个Person+Run.cpp文件,在文件的底部,可以找到这样一个结构体

    struct _category_t {
    const char *name;
    struct _class_t *cls;
    const struct _method_list_t *instance_methods;
    const struct _method_list_t *class_methods;
    const struct _protocol_list_t *protocols;
    const struct _prop_list_t *properties;
    };

    这些字段几乎都是见名知意了。
    每一个Category都会编译然后存储在一个_category_t类型的变量中

    static struct _category_t _OBJC_$_CATEGORY_Person_$_Run __attribute__ ((used, section ("__DATA,__objc_const"))) = 
    {
    "Person",
    0, // &OBJC_CLASS_$_Person,
    (const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Run,
    0,
    0,
    0,
    };

    因为我们的Person+Run里面只有一个实例方法,从上述代码中来看,也只有对应的位置传值了。
    通过这个_category_t的结构结构我们也可以看出,属性存储在_prop_list_t,这里并没有类中的objc_ivar_list结构体,所以Category的_category_t结构体中根本没有储存ivar的地方,所以不能添加成员变量。
    如果我们在分类中手动为成员变量添加了set和get方法之后,也可以调用,但实际上是没有内存来储值的,这就好像Swift中的计算属性,只起到了计算的作用,就相当于是两个方法(set和get),但是并不能拥有真用的内存来存储值。
    举个例子

    @property (copy, nonatomic) NSString * name;

    下面这个声明如果实在类中,系统会默认帮我们声明一个成员变量_name, 在.h中声明setName和name两个方法,并提供setName和name方法的默认实现。
    如果是在Category中,只相当于声明setName和name两个方法,没有实现也没有_name。

    2. Category的方法是何时合并到类中的
    大家都知道Category分类肯定是我们的应用启动是,通过运行时特性加载的,但是这个加载过程具体的细节就要结合runtime的源码来分析了。
    runtime源码太多了,我们先通过大概浏览代码来定位实现功能的相关位置。
    我从objc-runtime-new.mm中找到了下面这个方法。

    void attachLists(List* const * addedLists, uint32_t addedCount)

    而且他的注释一些的很清楚,修复类的方法,协议和变量列表,关联还未关联的分类。
    然后我们继续找,就找到了我们需要的这个方法。

    void attachLists(List* const * addedLists, uint32_t addedCount)

    我们从其中摘出一段代码来分析就可以解决我们的问题了。

    // many lists -> many lists
    uint32_t oldCount = array()->count;
    uint32_t newCount = oldCount + addedCount;
    setArray((array_t *)realloc(array(), array_t::byteSize(newCount)));
    array()->count = newCount;
    memmove(array()->lists + addedCount, array()->lists,
    oldCount * sizeof(array()->lists[0]));
    memcpy(array()->lists, addedLists,
    addedCount * sizeof(array()->lists[0]));

    在调用此方法之前我们所有的分类会被方法一个list里面(每一个分类都是一个元素),然后再调用attachLists方法,我们可以看到,在realloc的时候传进一个newCount,这是因为要增加分类中的方法,所以需要对之前的数组扩容,在扩容结束后先调用了memmove方法,在调用memcopy,大家可以上网查一下这两个方法具体的区别,这里简单一说,其实完成的效果都是把后面的内存的内容拷贝到前面内存中去,但是memmove可以处理内存重叠的问题。
    其实也就是首先将原来数组中的每个元素先往后移动(我们要添加几个元素,就移动几位),因为移动后的位置,其实也是数组自己的内存空间,所以存在重叠问题,直接移动会导致元素丢失的问题,所以用memmove(会检测是否有内存重叠)。
    移动完之后,把我们储存分类中方法的list中的元素移动到数组前面位置。
    过程就是这样子了,其实我们第三个问题就顺便解决完了。

    3. Category方法和类中方法的执行顺序
    上面其实说到了,类中原来的方法是要往后面移动的,分类的方法添加到前面的位置,而且调用方法的时候是在list中遍历查找,所以我们调用方法的时候,肯定会先调用到Category中的方法,但是这并不是覆盖,因为我们的原方法还在,只是这中机制保证了如果分类中有重写类的方法,会被优先查找到。

    4. +load和+initialize的区别
    对于这个问题我们从两个角度出发分析,调用方式调用时刻

    +load
    简单的举一个例子,我们创建一个Person类,然后重写+load方法,然后为Person新建两个Category,都分别实现+load。

    @implementation Person
    + (void)load {
    NSLog(@"Person - load");
    }
    @end

    @implementation Person (Test1)
    + (void)load {
    NSLog(@"Person Test1 - load");
    }
    @end

    @implementation Person (Test2)
    + (void)load {
    NSLog(@"Person Test2 - load");
    }
    @end

    当我们进行项目的时候,会得到下面的打印结果。

    2020-09-14 09:34:41.900161+0800 Category[4533:53426] Person - load
    2020-09-14 09:34:41.900629+0800 Category[4533:53426] Person Test1 - load
    2020-09-14 09:34:41.900700+0800 Category[4533:53426] Person Test2 - load

    我们并没有使用这个Person类和他的Category,所以应该是项目运行后,runtime在加载类和分类的时候,就会调用+load方法。
    我们从源码中找到下面这个方法
    void load_images(const char *path __unused, const struct mach_header *mh)
    方法中的最后一行调用了call_load_methods(),这个call_load_methods()中就是实现了+load的调用方式。
    下面是call_load_methods()函数的实现 ,大家简单浏览一遍

    void call_load_methods(void)
    {
    static bool loading = NO;
    bool more_categories;

    loadMethodLock.assertLocked();

    // Re-entrant calls do nothing; the outermost call will finish the job.
    if (loading) return;
    loading = YES;

    void *pool = objc_autoreleasePoolPush();

    do {
    // 1. Repeatedly call class +loads until there aren't any more
    while (loadable_classes_used > 0) {
    call_class_loads();
    }

    // 2. Call category +loads ONCE
    more_categories = call_category_loads();

    // 3. Run more +loads if there are classes OR more untried categories
    } while (loadable_classes_used > 0 || more_categories);

    objc_autoreleasePoolPop(pool);

    loading = NO;
    }

    从源码中很清楚的可以看到, 先调用call_class_loads(), 再调用call_category_loads(),这就说明了在调用所有的+load方法时,实现调用了所有类的+load方法,再去调用分类中的+load方法。
    然后我们在进入到call_class_loads()函数中

    static void call_class_loads(void)
    {
    int i;

    // Detach current loadable list.
    struct loadable_class *classes = loadable_classes;
    int used = loadable_classes_used;
    loadable_classes = nil;
    loadable_classes_allocated = 0;
    loadable_classes_used = 0;

    // Call all +loads for the detached list.
    for (i = 0; i < used; i++) {
    Class cls = classes[i].cls;
    load_method_t load_method = (load_method_t)classes[i].method;
    if (!cls) continue;

    if (PrintLoading) {
    _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
    }
    (*load_method)(cls, @selector(load));
    }

    // Destroy the detached list.
    if (classes) free(classes);
    }

    从中间的循环中可以看出,是取到了每个类的+load函数的指针,直接通过指针调用了这个函数。 call_category_loads()函数中体现出来的Category的+load方法的调用,也是同理。
    同时这也解答了我们的另一个疑惑,那就是为什么总是先调用类的+load,在调用Category的+load。
    思考:如果存在继承的情况,+load又会是怎样的调用顺序呢?
    从上面call_class_loads()函数中可以看到有一个list:loadable_classes,我们猜测这里面应该就是存放着我们所有的类,因为下面的循环是从0开始循环,所以我们要研究所有的类的+load方法的执行顺序,就要看这个list中的类的顺序是怎么样的。
    我们从个源码中可以找到这样一个方法,prepare_load_methods,在其实现中调用了schedule_class_load方法,我们看一下schedule_class_load的源码

    static void schedule_class_load(Class cls)
    {
    if (!cls) return;
    ASSERT(cls->isRealized()); // _read_images should realize

    if (cls->data()->flags & RW_LOADED) return;

    // Ensure superclass-first ordering
    schedule_class_load(cls->superclass);

    add_class_to_loadable_list(cls);
    cls->setInfo(RW_LOADED);
    }

    从源码中schedule_class_load(cls->superclass);这一句中可以看出,递归调用自己本身,并且传入自己的父类,结果递归之后,才调用add_class_to_loadable_list,这就说明父类总是在子类前面加入到list当中,所有在调用一个类的+load方法之前,肯定要先调用其父类的+load方法。
    那如果是其他没有继承关系的类呢,这就跟编译顺序有关系了,大家可以自己尝试验证一下。
    小结:

    • +load方法会在runtime加载类和分类时调用

    • 每个类和分类的+load方法之后调用一次

    • 调用顺序:先调用类的+load

    +initialize
    +initialize的调用是不同的,如果某一个类我们没有使用过,他的+initialize方法是不会调用的,到我们使用这个类(调用了类的某个方法)的时候,才会触发+initialize方法的调用。

    @implementation Person
    + (void)initialize {
    NSLog(@"Person - initialize");
    }
    @end

    @implementation Person (Test1)
    + (void)initialize {
    NSLog(@"Person Test1 - initialize");
    }
    @end

    @implementation Person (Test2)
    + (void)initialize {
    NSLog(@"Person Test2 - initialize");
    }
    @end

    当我们执行[Person alloc];的时候,才会走+initialize方法,而且执行的Category中的+initialize:

    2020-09-14 10:40:23.579623+0800 Category[9134:94173] Person Test2 - initialize

    这个我们之前已经说过了,Category的方法会添加list的前面,所以会先被找到并且执行,所以我们猜测+initialize的执行是走的正常的消息机制,objc_msgSend。

    由于objc_msgSend实现并没有完全开源,都是汇编代码,所以我们需要换一个思路来研究源码。

    objc_msgSend本质是什么?以调用实例方法为例,其实就是通过isa指针找到该类,然后寻找方法,找到之后调用。如果没有找到则通过superClass找到父类,继续查找方法。上面的例子中,我们仅仅是调用了一个alloc方法,但是也执行了+initialize方法,所以我们猜测+initialize会在查找方法的时候调用到。通过这个思路,我们定位到了class_getInstanceMethod()函数(class_getInstanceMethod函数就是在类中查找某个sel时候调用的),在这个函数中,又调用了IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)

    在该函数中我们可以找到下面这段代码

    if ((behavior & LOOKUP_INITIALIZE)  &&  !cls->isInitialized()) {
    initializeNonMetaClass (_class_getNonMetaClass(cls, inst));
    }

    可以看出如果类还没有执行+initialize 就会先执行,我们再看一下if语句中的initializeNonMetaClass函数,他会先拿到superClass,执行superClass的+initialize

    supercls = cls->superclass;
    if (supercls && !supercls->isInitialized()) {
    initializeNonMetaClass(supercls);
    }

    这就是存在继承的情况,为什么会先执行父类的+initialize。

    大总结

    1. 调用方式:
      load是根据函数地址直接调用
      initialize是通过消息机制objc_msgSend调用

    2. 调用时刻:

      load是在runtime加载类和分类时调用(只会调用一次)
      initialize是在类第一次收到消息时调用个,默认没有继承的情况下每个类只会initialize一次(父类的initialize可能会被执行多次)

    3. 调用顺序

      load
      先调用类的load:先编译的类先调用,子类调用之前,先调用父类的
      在调用Category的load:先编译的先调用

      initialize
      先初始化父类
      在初始化子类(初始化子类可能调用父类的initialize)

    补充
    上面总结的时候说到父类的initialize会被执行多次,什么情况下会被执行多次,为什么?举个例子:

    @implementation Person
    + (void)initialize {
    NSLog(@"Person - initialize");
    }
    @end

    @implementation Student
    @end

    Student类继承Person类,并且只有父类Person中实现了+initialize,Student类中并没有实现
    此时我们调用[Student alloc];, 会得到如下的打印。

    2020-09-14 11:31:55.377569+0800 Category[11483:125034] Person - initialize
    2020-09-14 11:31:55.377659+0800 Category[11483:125034] Person - initialize

    Person的+initialize被执行了两次,但这两个意义是不同的,第一次执行是因为在调用子类的+initialize方法之前必须先执行父类的了+initialize,所以会打印一次。当要执行子类的+initialize时,通过消息机制,student类中并没有找到实现的+initialize的实现,所以要通过superClass指针去到父类中继续查找,因为父类中实现了+initialize,所以才会有了第二次的打印。

    结尾
    本文的篇幅略长,笔者按照自己的思路和想法写完了此文,陈述过程不一定那么调理和完善,大家在阅读过程中发现问题,可以留言交流。
    感谢阅读。

    转自:https://www.jianshu.com/p/141b04e376d4

    收起阅读 »

    iOS --常见崩溃和防护(二)

    接上一章。。。。。。。iOS9之前会crash,iOS9之后苹果系统已优化。在iOS9之后,即使开发者没有移除observer,Notification crash也不会再产生了。四、NSTimer Crash 防护产生的原因:NSTimer会 强引用 tar...
    继续阅读 »

    接上一章。。。。。。。

    三、NSNotification Crash

    产生的原因:
    当一个对象添加了notification之后,如果dealloc的时候,仍然持有notification,就会出现NSNotification类型的crash。NSNotification类型的crash多产生于程序员写代码时候犯疏忽,在NSNotificationCenter添加一个对象为observer之后,忘记了在对象dealloc的时候移除它。

    iOS9之前会crash,iOS9之后苹果系统已优化。在iOS9之后,即使开发者没有移除observer,Notification crash也不会再产生了。

    解决方案:
    NSNotification Crash的防护原理很简单, 利用method swizzling hook NSObject的dealloc函数,再对象真正dealloc之前先调用一下:[[NSNotificationCenter defaultCenter] removeObserver:self],即可。
    #import 

    /**
    当一个对象添加了notification之后,如果dealloc的时候,仍然持有notification,就会出现NSNotification类型的crash。

    iOS9之后专门针对于这种情况做了处理,所以在iOS9之后,即使开发者没有移除observer,Notification crash也不会再产生了
    */

    NS_ASSUME_NONNULL_BEGIN

    @interface NSObject (NSNotificationCrash)

    + (void)xz_enableNotificationProtector;

    @end

    NS_ASSUME_NONNULL_END
    #import "NSObject+NSNotificationCrash.h"
    #import "NSObject+XZSwizzle.h"
    #import


    static const char *isNSNotification = "isNSNotification";

    @implementation NSObject (NSNotificationCrash)


    + (void)xz_enableNotificationProtector {

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    NSObject *objc = [[NSObject alloc] init];

    [objc xz_instanceSwizzleMethod:@selector(addObserver:selector:name:object:) replaceMethod:@selector(xz_addObserver:selector:name:object:)];

    // 在ARC环境下不能显示的@selector dealloc。
    [objc xz_instanceSwizzleMethod:NSSelectorFromString(@"dealloc") replaceMethod:NSSelectorFromString(@"xz_dealloc")];
    });
    }

    - (void)xz_addObserver:(id)observer selector:(SEL)aSelector name:(nullable NSNotificationName)aName object:(nullable id)anObject {

    // 添加标志位,在delloc中只有isNSNotification是YES,才会移除通知
    [observer setIsNSNotification:YES];
    [self xz_addObserver:observer selector:aSelector name:aName object:anObject];
    }


    - (void)setIsNSNotification:(BOOL)yesOrNo {
    objc_setAssociatedObject(self, isNSNotification, @(yesOrNo), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }

    - (BOOL)isNSNotification {
    NSNumber *number = objc_getAssociatedObject(self, isNSNotification);;
    return [number boolValue];
    }

    /**
    如果一个对象从来没有添加过通知,那就不要remove操作
    */
    - (void)xz_dealloc
    {
    if ([self isNSNotification]) {
    NSLog(@"CrashProtector: %@ is dealloc,but NSNotificationCenter Also exsit",self);
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    }

    [self xz_dealloc];
    }

    @end

    四、NSTimer Crash 防护

    产生的原因:
    NSTimer会 强引用 target实例,所以需要在合适的时机invalidate 定时器,否则就会由于定时器timer强引用target的关系导致 target不能被释放,造成内存泄露,甚至在定时任务触发时导致crash。与此同时,如果NSTimer是无限重复的执行一个任务的话,也有可能导致target的selector一直被重复调用且处于无效状态,对app的CPU,内存等性能方面均是没有必要的浪费。所以,很有必要设计出一种方案,可以有效的防护NSTimer的滥用问题。

    解决方案:
    定义一个抽象类,NSTimer实例强引用抽象类,而在抽象类中,弱引用target,这样target和NSTimer之间的关系也就是弱引用了,意味着target可以自由的释放,从而解决了循环引用的问题。

    具体方式:
    1、定义一个抽象类,抽象类中弱引用target。

    #import 

    NS_ASSUME_NONNULL_BEGIN

    @interface XZProxy : NSProxy

    + (instancetype)proxyWithTarget:(id)target;

    @end

    NS_ASSUME_NONNULL_END
    #import "XZProxy.h"


    @interface XZProxy ()

    /// 消息转发的对象
    @property (nonatomic, weak) id target;

    @end

    @implementation XZProxy

    + (instancetype)proxyWithTarget:(id)target {
    // NSProxy没有init方法, 只需要调用alloc创建对象即可
    XZProxy *proxy = [XZProxy alloc];
    proxy.target = target;
    return proxy;
    }

    - (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [self.target methodSignatureForSelector:sel];
    }

    - (void)forwardInvocation:(NSInvocation *)invocation {
    [invocation invokeWithTarget:self.target];
    }

    @end

    2、创建category,交换系统方法,实现NSTimer强引用抽象类。
    ps:也可以不使用分类,不用交换方法,直接在创建timer实例的时候,将原本的target指向抽象类即可

    #import 

    NS_ASSUME_NONNULL_BEGIN

    @interface NSObject (NSTimerCrash)

    + (void)xz_enableTimerProtector;

    @end

    NS_ASSUME_NONNULL_END
    #import "NSObject+NSTimerCrash.h"
    #import "NSObject+XZSwizzle.h"
    #import "XZProxy.h"

    @implementation NSObject (NSTimerCrash)


    + (void)xz_enableTimerProtector {

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

    // 创建一个timer并把它指定到一个默认的runloop模式中,并且在 TimeInterval时间后 启动定时器
    [NSTimer xz_classSwizzleMethod:@selector(scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:) replaceMethod:@selector(xz_scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:)];

    // 创建一个定时器,但是么有添加到运行循环,我们需要在创建定时器后手动的调用 NSRunLoop 对象的 addTimer:forMode: 方法。
    [NSTimer xz_classSwizzleMethod:@selector(timerWithTimeInterval:target:selector:userInfo:repeats:) replaceMethod:@selector(xz_timerWithTimeInterval:target:selector:userInfo:repeats:)];
    });
    }


    + (NSTimer *)xz_scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval target:(id)target selector:(SEL)selector userInfo:(id)userInfo repeats:(BOOL)repeats {

    return [self xz_scheduledTimerWithTimeInterval:timeInterval target:[XZProxy proxyWithTarget:target] selector:selector userInfo:userInfo repeats:repeats];
    }

    + (NSTimer *)xz_timerWithTimeInterval:(NSTimeInterval)timeInterval target:(id)target selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo {

    return [self xz_timerWithTimeInterval:timeInterval target:[XZProxy proxyWithTarget:target] selector:aSelector userInfo:userInfo repeats:yesOrNo];
    }

    @end


    摘自链接:https://www.jianshu.com/p/3324786893a1

    收起阅读 »

    iOS --常见崩溃和防护(一)

    iOS 的崩溃我们常见的crash有哪些呢?1.unrecognized selector crash (没找到对应的函数)2.KVO crash :(KVO的被观察者dealloc时仍然注册着KVO导致的crash,添加KVO重复添加观察者或重复移除观察者 ...
    继续阅读 »

    iOS 的崩溃

    我们常见的crash有哪些呢?

    1.unrecognized selector crash (没找到对应的函数)

    2.KVO crash :(KVO的被观察者dealloc时仍然注册着KVO导致的crash,添加KVO重复添加观察者或重复移除观察者 )

    3.NSNotification crash:(当一个对象添加了notification之后,如果dealloc的时候,仍然持有notification)

    4.NSTimer类型crash:(需要在合适的时机invalidate 定时器,否则就会由于定时器timer强引用target的关系导致 target不能被释放,造成内存泄露,甚至在定时任务触发时导致crash)

    5.Container类型crash:(数组,字典,常见的越界,插入,nil)

    6.野指针类型的crash

    7.非主线程刷UI类型:(在非主线程刷UI将会导致app运行crash)……

    如何防护crash

    一、unrecognized selector crash

    unrecognized selector类型的crash,通常是因为一个对象调用了一个不属于它方法的方法导致的。而我们可以从方法调用的过程中,寻找到避免程序崩溃的突破口。

    方法调用的过程是哪样的呢?

    方法调用的过程--调用实例方法
    1.在对象的<缓存方法列表> 中去找要调用的方法,找到直接执行其实现。
    2.对象的<缓存方法列表> 里没找到,就去<类的方法列表>里找,找到了就执行其实现。
    3.还没找到,说明这个类自己没有了,就会通过isa去向其父类里执行1、2。
    4.如果找到了根类还没找到,那么就是没有了,会转向一个拦截调用的方法,可以自己在拦截调用方法里面做一些处理。
    5.如果没有在拦截调用里做处理,那么就会报错崩溃。

    方法调用的过程--调用类方法
    1.在类的<缓存方法列表> 中去找要调用的方法,找到直接执行其实现。
    2.类的<缓存方法列表> 里没找到,就去里找,找到了就执行其实现。
    3.还没找到,说明这个类自己没有了,就会通过isa去meta类的父类里执行1、2。
    4.如果找到了根meta类还没找到,那么就是没有了,会转向一个拦截调用的方法,可以自己在拦截调用方法里面做一些处理。
    5.如果没有在拦截调用里做处理,那么就会报错崩溃。

    从上面的方法调用过程可以看出,在找不到调用的方法程序崩溃之前,我们可以通过重写NSObject方法进行拦截调用,阻止程序的crash。这里面就用到了消息的转发机制:

    runtime提供了3种方式去补救:

    1:调用resolveInstanceMethod给个机会让类添加这个实现这个函数
    2:调用forwardingTargetForSelector让别的对象去执行这个函数
    3:调用forwardInvocation(函数执行器)灵活的将目标函数以及其他形式执行。
    如果都不行,系统才会调用doesNotRecognizeSelector抛出异常。

    既然可以补救,我们完全也可以利用消息转发机制来做文章,但是我们选择哪一步比较合适呢?
    1:resolveInstanceMethod需要在类的本身动态的添加它本身不存在的方法,这些方法对于该类本身来说是冗余的
    2:forwardInvocation可以通过NSInvocation的形式将消息转发给多个对象,但是其开销比较大,需要创建新的 NSInvocation对象,并且forwardInvocation的函数经常被使用者调用来做消息的转发选择机制,不适合多次重写
    3:forwardingTargetForSelector可以将消息转发给一个对象,开销较小,并且被重写的概率较低,适合重写

    对于NSObject方法的重写,我们可以分为以下几步:
    第一步:为类动态的创建一个消息接受类。
    第二步:为类动态为桩类添加对应的Selector,用一个通用的返回0的函数来实现该SEL的IMP
    第三步:将消息直接转发到这个消息接受类类对象上。

    解决方法:

    1、创建一个消息接受类。(继承至NSObject)

    当调用方法的消息转发给该类后,该类也没有这个方法,回调用resolveInstanceMethod:方法,在消息接受类中重写方法,返回YES,表明该消息已经处理,这样就不会崩溃了。
    重写的resolveInstanceMethod:方法中一定要有动态添加方法的处理,不然会继续走消息转发的流程,从而造成死循环。

    #import 


    @interface XZUnrecognizedSelectorSolveObject : NSObject

    @property (nonatomic, weak) NSObject *objc;

    @end
    #Import "XZUnrecognizedSelectorSolveObject.h"
    #import

    @interface XZUnrecognizedSelectorSolveObject ()

    @end

    @implementation XZUnrecognizedSelectorSolveObject

    + (BOOL)resolveInstanceMethod:(SEL)sel {
    // 如果没有动态添加方法的话,还会调用forwardingTargetForSelector:方法,从而造成死循环
    class_addMethod([self class], sel, (IMP)addMethod, "v@:@");
    return YES;
    }

    id addMethod(id self, SEL _cmd) {
    NSLog(@"WOCrashProtector: unrecognized selector: %@", NSStringFromSelector(_cmd));
    return 0;
    }

    @end
    2、为NSObject添加分类,拦截NSObject的forwardingTargetForSelector:方法。

    实现原理:在分类中自定义一个xz_forwardingTargetForSelector:方法,然后替换掉系统的forwardingTargetForSelector:方法

    #import 

    NS_ASSUME_NONNULL_BEGIN

    @interface NSObject (SelectorCrash)

    + (void)xz_enableSelectorProtector;

    @end

    NS_ASSUME_NONNULL_END
    #import "NSObject+SelectorCrash.h"
    #import
    #import "NSObject+XZSwizzle.h"
    #Import "XZUnrecognizedSelectorSolveObject.h"

    @implementation NSObject (SelectorCrash)

    + (void)xz_enableSelectorProtector {

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    NSObject *object = [[NSObject alloc] init];
    [object xz_instanceSwizzleMethod:@selector(forwardingTargetForSelector:) replaceMethod:@selector(xz_forwardingTargetForSelector:)];
    });
    }

    - (id)xz_forwardingTargetForSelector:(SEL)aSelector {
    // 判断某个类是否有某个实例方法,有则返回YES,否则返回NO
    if (class_respondsToSelector([self class], @selector(forwardInvocation:))) {
    // 有forwardInvocation实例方法
    IMP impOfNSObject = class_getMethodImplementation([NSObject class], @selector(forwardInvocation:));
    IMP imp = class_getMethodImplementation([self class], @selector(forwardInvocation:));

    if (imp != impOfNSObject) {
    return nil;
    }
    }

    // 新建桩类转发消息
    XZUnrecognizedSelectorSolveObject *solveObject = [XZUnrecognizedSelectorSolveObject new];
    solveObject.objc = self;
    return solveObject;
    }

    @end

    交换方法代码 如下:

    #import 

    NS_ASSUME_NONNULL_BEGIN

    @interface NSObject (XZSwizzle)

    /**
    对类方法进行拦截并替换

    @param originalSelector 原有类方法
    @param replaceSelector 自定义类替换方法
    */
    + (void)xz_classSwizzleMethod:(SEL _Nonnull)originalSelector replaceMethod:(SEL _Nonnull)replaceSelector;

    /**
    对类方法进行拦截并替换

    @param kClass 具体的类
    @param originalSelector 原有类方法
    @param replaceSelector 自定义类替换方法
    */
    + (void)xz_classSwizzleMethodWithClass:(Class _Nonnull)kClass orginalMethod:(SEL _Nonnull)originalSelector replaceMethod:(SEL _Nonnull)replaceSelector;



    /**
    对实例方法进行拦截并替换

    @param originalSelector 原有实例方法
    @param replaceSelector 自定义实例替换方法
    */
    - (void)xz_instanceSwizzleMethod:(SEL _Nonnull)originalSelector replaceMethod:(SEL _Nonnull)replaceSelector;

    /**
    对实例方法进行拦截并替换

    @param kClass 具体的类
    @param originalSelector 原有实例方法
    @param replaceSelector 自定义实例替换方法
    */
    - (void)xz_instanceSwizzleMethodWithClass:(Class _Nonnull)kClass orginalMethod:(SEL _Nonnull)originalSelector replaceMethod:(SEL _Nonnull)replaceSelector;

    @end

    NS_ASSUME_NONNULL_END

    #import "NSObject+XZSwizzle.h"
    #import

    @implementation NSObject (XZSwizzle)


    /**
    对类方法进行拦截并替换

    @param originalSelector 类原有方法
    @param replaceSelector 自定义替换方法
    */
    + (void)xz_classSwizzleMethod:(SEL _Nonnull)originalSelector replaceMethod:(SEL _Nonnull)replaceSelector {
    Class class = [self class];

    [self xz_classSwizzleMethodWithClass:class orginalMethod:originalSelector replaceMethod:replaceSelector];
    }

    /**
    对类方法进行拦截并替换

    @param kClass 具体的类
    @param originalSelector 原有类方法
    @param replaceSelector 自定义类替换方法
    */
    + (void)xz_classSwizzleMethodWithClass:(Class _Nonnull)kClass orginalMethod:(SEL _Nonnull)originalSelector replaceMethod:(SEL _Nonnull)replaceSelector {

    // Method中包含IMP函数指针,通过替换IMP,使SEL调用不同函数实现
    Method originalMethod = class_getClassMethod(kClass, originalSelector);
    Method replaceMethod = class_getClassMethod(kClass, replaceSelector);

    // 获取MetaClass (交换、添加等类方法需要用metaClass)
    Class metaClass = objc_getMetaClass(NSStringFromClass(kClass).UTF8String);

    // class_addMethod:如果发现方法已经存在,会失败返回,也可以用来做检查用,我们这里是为了避免源方法没有实现的情况;如果方法没有存在,我们则先尝试添加被替换的方法的实现
    BOOL didAddMethod = class_addMethod(metaClass, originalSelector, method_getImplementation(replaceMethod), method_getTypeEncoding(replaceMethod));

    if (didAddMethod) {
    // 添加成功(原方法未实现,为防止crash,需要将刚添加的原方法替换)
    class_replaceMethod(metaClass, replaceSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    } else {
    // 添加失败(原本就有原方法, 直接交换两个方法)
    method_exchangeImplementations(originalMethod, replaceMethod);
    }
    }


    /**
    对实例方法进行拦截并替换

    @param originalSelector 原有实例方法
    @param replaceSelector 自定义实例替换方法
    */
    - (void)xz_instanceSwizzleMethod:(SEL _Nonnull)originalSelector replaceMethod:(SEL _Nonnull)replaceSelector {
    Class class = [self class];

    [self xz_instanceSwizzleMethodWithClass:class orginalMethod:originalSelector replaceMethod:replaceSelector];
    }

    /**
    对实例方法进行拦截并替换

    @param kClass 具体的类
    @param originalSelector 原有实例方法
    @param replaceSelector 自定义实例替换方法
    */
    - (void)xz_instanceSwizzleMethodWithClass:(Class _Nonnull)kClass orginalMethod:(SEL _Nonnull)originalSelector replaceMethod:(SEL _Nonnull)replaceSelector {

    Method originalMethod = class_getInstanceMethod(kClass, originalSelector);
    Method replaceMethod = class_getInstanceMethod(kClass, replaceSelector);

    // class_addMethod:如果发现方法已经存在,会失败返回,也可以用来做检查用,我们这里是为了避免源方法没有实现的情况;如果方法没有存在,我们则先尝试添加被替换的方法的实现
    BOOL didAddMethod = class_addMethod(kClass, originalSelector, method_getImplementation(replaceMethod), method_getTypeEncoding(replaceMethod));

    if (didAddMethod) {
    // 添加成功(原方法未实现,为防止crash,需要将刚添加的原方法替换)
    class_replaceMethod(kClass, replaceSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    } else {
    // 添加失败(原本就有原方法, 直接交换两个方法)
    method_exchangeImplementations(originalMethod, replaceMethod);
    }
    }

    @end
    调用:
    在AppDelegate调用[NSObject xz_enableSelectorProtector]; 就可以了

    二、KVO Crash

    KVO Crash,通常是KVO的被观察者dealloc时仍然注册着KVO导致的crash,添加KVO重复添加观察者或重复移除观察者引起的。
    一个被观察的对象上有若干个观察者,每个观察者又有若干条keypath。如果观察者和keypathx的数量一多,很容易不清楚被观察的对象整个KVO关系,导致被观察者在dealloc的时候,仍然残存着一些关系没有被注销,同时还会导致KVO注册者和移除观察者不匹配的情况发生。尤其是多线程的情况下,导致KVO重复添加观察者或者移除观察者的情况,这种类似的情况通常发生的比较隐蔽,很难从代码的层面上排查。

    解决方法:

    可以让观察对象持有一个KVO的delegate,所有和KVO相关的操作均通过delegate来进行管理,delegate通过建立一张MAP表来维护KVO的整个关系,这样做的好处有2个:

    1:如果出现KVO重复添加观察或者移除观察者(KVO注册者不匹配的)情况,delegate,可以直接阻止这些非正常的操作。

    2:被观察对象dealloc之前,可以通过delegate自动将与自己有关的KVO关系都注销掉,避免了KVO的被观察者dealloc时仍然注册着KVO导致的crash

    具体方式:
    1、自定义一个继承自NSObject的代理类,并通过Catagory将这个代理类作为NSObject的属性进行关联
    #import 
    #import "XZKVOProxy.h"

    NS_ASSUME_NONNULL_BEGIN

    @interface NSObject (KVOCrash)

    @property (nonatomic, strong) XZKVOProxy * _Nullable KVOProxy; // 自定义的kvo关系的代理

    @end

    NS_ASSUME_NONNULL_END
    #import "NSObject+KVOCrash.h"
    #import "XZKVOProxy.h"
    #import


    #pragma mark - NSObject + KVOCrash

    static void *NSObjectKVOProxyKey = &NSObjectKVOProxyKey;

    @implementation NSObject (KVOCrash)

    - (XZKVOProxy *)KVOProxy {
    id proxy = objc_getAssociatedObject(self, NSObjectKVOProxyKey);

    if (nil == proxy) {
    proxy = [XZKVOProxy kvoProxyWithObserver:self];
    self.KVOProxy = proxy;
    }

    return proxy;
    }

    - (void)setKVOProxy:(XZKVOProxy *)proxy
    {
    objc_setAssociatedObject(self, NSObjectKVOProxyKey, proxy, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }

    @end

    2、在自定义代理类中建立一个map来维护KVO整个关系

    #import 


    typedef void (^XZKVONitificationBlock)(id _Nullable observer, id _Nullable object, NSDictionary * _Nullable change);

    /**
    KVO配置类
    用于存储KVO里面的相关设置参数
    */
    @interface XZKVOInfo : NSObject

    //- (instancetype _Nullable)initWithObserver:(id _Nonnull)object keyPath:(NSString * _Nullable)keyPath options:(NSKeyValueObservingOptions)options context:(void * _Nullable)context block:(XZKVONitificationBlock _Nonnull )block;

    @end


    NS_ASSUME_NONNULL_BEGIN
    /**
    KVO管理类
    用于管理object添加和移除的消息,(通过Map进行KVO之间的关系)(字典应该也可以)
    */
    @interface XZKVOProxy : NSObject

    @property (nullable, nonatomic, weak, readonly) id observer;


    + (instancetype)kvoProxyWithObserver:(nullable id)observer;

    - (void)xz_observer:(id _Nullable)object keyPath:(NSString * _Nullable)keyPath options:(NSKeyValueObservingOptions)options context:(void * _Nullable)context block:(XZKVONitificationBlock)block;

    - (void)xz_unobserver:(id _Nullable)object keyPath:(NSString * _Nullable)keyPath;
    - (void)xz_unobserver:(id _Nullable)object;

    - (void)xz_unobserverAll;

    @end

    NS_ASSUME_NONNULL_END

    #import "XZKVOProxy.h"
    #import


    @interface XZKVOInfo ()
    {
    @public
    __weak id _object; // 观察对象
    NSString *_keyPath;
    NSKeyValueObservingOptions _options;
    SEL _action;
    void *_context;
    XZKVONitificationBlock _block;
    }
    @end

    @implementation XZKVOInfo

    - (instancetype _Nullable)initWithObserver:(id _Nonnull)object
    keyPath:(NSString * _Nullable)keyPath
    options:(NSKeyValueObservingOptions)options
    context:(void * _Nullable)context {
    return [self initWithObserver:object keyPath:keyPath options:options block:NULL action:NULL context:context];
    }

    - (instancetype _Nullable)initWithObserver:(id _Nonnull)object
    keyPath:(NSString * _Nullable)keyPath
    options:(NSKeyValueObservingOptions)options
    context:(void * _Nullable)context
    block:(XZKVONitificationBlock)block {

    return [self initWithObserver:object keyPath:keyPath options:options block:block action:NULL context:context];
    }

    - (instancetype _Nullable)initWithObserver:(id _Nonnull)object
    keyPath:(NSString * _Nullable)keyPath
    options:(NSKeyValueObservingOptions)options
    block:(_Nullable XZKVONitificationBlock)block
    action:(_Nullable SEL)action
    context:(void * _Nullable)context {
    if (self = [super init]) {
    _object = object;
    _block = block;
    _keyPath = [keyPath copy];
    _options = options;
    _action = action;
    _context = context;
    }
    return self;
    }

    @end



    /**
    此类用来管理混乱的KVO关系
    让被观察对象持有一个KVO的delegate,所有和KVO相关的操作均通过delegate来进行管理,delegate通过建立一张map来维护KVO整个关系

    好处:
    不会crash如果出现KVO重复添加观察者或重复移除观察者(KVO注册观察者与移除观察者不匹配)的情况,delegate可以 1.直接阻止这些非正常的操作。

    crash 2.被观察对象dealloc之前,可以通过delegate自动将与自己有关的KVO关系都注销掉,避免了KVO的被观察者dealloc时仍然注册着KVO导致的crash。

    👇:
    重复添加观察者不会crash,即不会走@catch
    多次添加对同一个属性观察的观察者,系统方法内部会强应用这个观察者,同理即可remove该观察者同样次数。

    */
    @interface XZKVOProxy ()
    {
    pthread_mutex_t _mutex;
    NSMapTable *> *_objectInfoMap;///< map来维护KVO整个关系
    }
    @end

    @implementation XZKVOProxy

    + (instancetype)kvoProxyWithObserver:(nullable id)observer {
    return [[self alloc] initWithObserver:observer];
    }

    - (instancetype)initWithObserver:(nullable id)observer {
    if (self = [super init]) {
    _observer = observer;
    _objectInfoMap = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPointerPersonality valueOptions:NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPointerPersonality capacity:0];
    }
    return self;
    }

    /**
    加锁、解锁
    */
    - (void)lock {
    pthread_mutex_lock(&_mutex);
    }

    - (void)unlock {
    pthread_mutex_unlock(&_mutex);
    }


    /**
    添加、删除 观察者
    */
    - (void)xz_observer:(id _Nullable)object keyPath:(NSString * _Nullable)keyPath options:(NSKeyValueObservingOptions)options context:(void * _Nullable)context block:(XZKVONitificationBlock)block {

    // 断言
    NSAssert(0 != keyPath.length && NULL != block, @"missing required parameters observe:%@ keyPath:%@ block:%p", object, keyPath, block);
    if (nil == object || 0 == keyPath.length || NULL == block) {
    return;
    }

    // 将观察的信息转成info对象
    // self即kvoProxy是观察者;object是被观察者
    XZKVOInfo *info = [[XZKVOInfo alloc] initWithObserver:self keyPath:keyPath options:options context:context block:block];

    if (info) {
    // 将info以key-value的形式存储到map中。key是被观察对象;value是观察信息的集合。
    // 加锁
    [self lock];

    NSMutableSet *infos = [_objectInfoMap objectForKey:object];


    BOOL _isExisting = NO;
    for (XZKVOInfo *existingInfo in infos) {
    if ([existingInfo->_keyPath isEqualToString:info->_keyPath]) {
    // 观察者已存在
    _isExisting = YES;
    break;
    }
    }

    if (_isExisting == YES) {
    // 解锁
    [self unlock];
    return;
    }
    // // check for info existence
    // XZKVOInfo *existingInfo = [infos member:info];
    // if (nil != existingInfo) {
    // // observation info already exists; do not observe it again
    //
    // // 解锁
    // [self unlock];
    // return;
    // }

    // 不存在
    if (infos == nil) {
    // 创建set,并将set添加进Map里
    infos = [NSMutableSet set];
    [_objectInfoMap setObject:infos forKey:object];
    }
    // 将要添加的KVOInfo添加进set里面
    [infos addObject:info];

    // 解锁
    [self unlock];


    // 将 kvoProxy 作为观察者;添加观察者
    [object addObserver:self forKeyPath:info->_keyPath options:info->_options context:info->_context];
    }
    }

    - (void)xz_unobserver:(id _Nullable)object keyPath:(NSString * _Nullable)keyPath {

    // 将观察的信息转成info对象
    // self即kvoProxy是观察者;object是被观察者
    XZKVOInfo *info = [[XZKVOInfo alloc] initWithObserver:self keyPath:keyPath options:0 context:nil];

    // 加锁
    [self lock];

    // 从map中获取object对应的KVOInfo集合
    NSMutableSet *infos = [_objectInfoMap objectForKey:object];

    BOOL _isExisting = NO;
    for (XZKVOInfo *existingInfo in infos) {
    if ([existingInfo->_keyPath isEqualToString:info->_keyPath]) {
    // 观察者已存在
    _isExisting = YES;
    info = existingInfo;
    break;
    }
    }

    if (_isExisting == YES) {
    // 存在
    [infos removeObject:info];

    // remove no longer used infos
    if (0 == infos.count) {
    [_objectInfoMap removeObjectForKey:object];
    }

    // 解锁
    [self unlock];


    // 移除观察者
    [object removeObserver:self forKeyPath:info->_keyPath context:info->_context];
    } else {
    // 解锁
    [self unlock];
    }

    // XZKVOInfo *registeredInfo = [infos member:info];
    //
    // if (nil != registeredInfo) {
    // [infos removeObject:registeredInfo];
    //
    // // remove no longer used infos
    // if (0 == infos.count) {
    // [_objectInfoMap removeObjectForKey:object];
    // }
    //
    // // 解锁
    // [self unlock];
    //
    //
    // // 移除观察者
    // [object removeObserver:self forKeyPath:registeredInfo->_keyPath context:registeredInfo->_context];
    // } else {
    // // 解锁
    // [self unlock];
    // }
    }

    - (void)xz_unobserver:(id _Nullable)object {
    // 加锁
    [self lock];

    // 从map中获取object对应的KVOInfo集合
    NSMutableSet *infos = [_objectInfoMap objectForKey:object];

    [_objectInfoMap removeObjectForKey:object];
    // 解锁
    [self unlock];

    // 批量移除观察者
    for (XZKVOInfo *info in infos) {
    // 移除观察者
    [object removeObserver:self forKeyPath:info->_keyPath context:info->_context];
    }
    }

    - (void)xz_unobserverAll {

    if (_objectInfoMap) {
    // 加锁
    [self lock];

    // copy一份map,防止删除数据异常冲突
    NSMapTable *objectInfoMaps = [_objectInfoMap copy];

    [_objectInfoMap removeAllObjects];

    // 解锁
    [self unlock];

    // 移除全部观察者
    for (id object in objectInfoMaps) {

    NSSet *infos = [objectInfoMaps objectForKey:object];
    if (!infos || infos.count == 0) {
    continue;
    }

    for (XZKVOInfo *info in infos) {
    [object removeObserver:self forKeyPath:info->_keyPath context:info->_context];
    }
    }

    }
    }



    - (void)observeValueForKeyPath:(NSString *)keyPath
    ofObject:(id)object
    change:(NSDictionary *)change
    context:(void *)context {

    // NSAssert(context, @"missing context keyPath:%@ object:%@ change:%@", keyPath, object, change);



    NSLog(@"%@",keyPath);
    NSLog(@"%@",object);
    NSLog(@"%@",change);
    NSLog(@"%@",context);


    // 从map中获取object对应的KVOInfo集合
    NSMutableSet *infos = [_objectInfoMap objectForKey:object];

    BOOL _isExisting = NO;
    XZKVOInfo *info;
    for (XZKVOInfo *existingInfo in infos) {
    if ([existingInfo->_keyPath isEqualToString:keyPath]) {
    // 观察者已存在
    _isExisting = YES;
    info = existingInfo;
    break;
    }
    }

    if (_isExisting == YES && info) {
    XZKVOProxy *proxy = info->_object;
    id observer = proxy.observer;

    XZKVONitificationBlock block = info->_block;

    if (block) {
    block(observer, object, change);
    }
    }
    }



    - (void)dealloc {

    // 移除所有观察者
    [self xz_unobserverAll];

    // 销毁mutex
    pthread_mutex_destroy(&_mutex);
    }

    @end


    摘自链接:https://www.jianshu.com/p/3324786893a1
    收起阅读 »

    iOS - 剖析性能优化相关

    性能优化的几个点:1.卡顿优化在了解卡顿优化相关的前头,首先要了解 CPU 和 GPU。CPU(Central Processing Unit,中央处理器)对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制(Core...
    继续阅读 »

    性能优化的几个点:

    1.卡顿优化

    在了解卡顿优化相关的前头,首先要了解 CPU 和 GPU。

    CPU(Central Processing Unit,中央处理器)
    对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制(Core Graphics)都是通过 CPU 来做的。

    GPU(Graphics Processing Unit,图形处理器)
    纹理的渲染、


    所要显示的信息一般是通过 CPU 计算或者解码,经过 CPU 的数据交给 GPU 渲染,渲染的工作在帧缓存的地方完成,然后从帧缓存读取数据到视频控制器上,最终显示在屏幕上。

    在 iOS 中有双缓存机制,有前帧缓存、后帧缓存,这样渲染的效率很高。

    屏幕成像原理

    我们所看到的动态的屏幕的成像其实和视频一样也是一帧一帧组成的。为了把显示器的显示过程和系统的视频控制器进行同步,显示器(或者其他硬件)会用硬件时钟产生一系列的定时信号。当电子枪换到新的一行,准备进行扫描时,显示器会发出一个水平同步信号(Horizonal Synchronization),简称 HSync;而当一帧画面绘制完成后,电子枪回复到原位,准备画下一帧前,显示器会发出一个垂直同步信号(Vertical Synchronization),简称 VSync。显示器通常以固定频率进行刷新,这个刷新率就是 VSync 信号产生的频率。

    卡顿成因

    前面我们知道,完成显示信息的过程是:CPU 计算数据 -> GPU 进行渲染 -> 屏幕发出 VSync 信号 -> 成像,假如屏幕已经发出了 VSync 但 GPU 还没有渲染完成,则只能将上一次的数据显示出来,以致于当前计算的帧数据丢失,这样就产生了卡顿,当前的帧数据计算好后只能等待下一个周期去渲染。

    解决办法

    解决卡顿现象的主要思路就是:尽可能减少 CPU 和 GPU 资源的消耗。
    按照 60fps 的刷帧率,每隔 16ms 就会有一次 VSync 信号产生。那么针对 CPU 和 GPU 有以下优化方案:
    CPU
  • 尽量用轻量级的对象 如:不用处理事件的 UI 控件可以考虑使用 CALayer;
  • 不要频繁地调用 UIView 的相关属性 如:frame、bounds、transform 等;
  • 尽量提前计算好布局,在有需要的时候一次性调整对应属性,不要多次修改;
  • Autolayout 会比直接设置 frame 消耗更多的 CPU 资源;
  • 图片的 size 和 UIImageView 的 size 保持一致;
  • 控制线程的最大并发数量;
  • 耗时操作放入子线程;如文本的尺寸计算、绘制,图片的解码、绘制等;
  • GPU
    • 尽量避免短时间内大量图片显示;
    • GPU 能处理的最大纹理尺寸是 4096 * 4096,超过这个尺寸就会占用 CPU 资源,所以纹理不能超过这个尺寸;
    • 尽量减少透视图的数量和层次;
    • 减少透明的视图(alpha < 1),不透明的就设置 opaque 为 YES;
    • 尽量避免离屏渲染;
    离屏渲染

    在 OpenGL 中,GPU 有两种渲染方式:

    On-Screen Rendering:当前屏幕渲染,在当前用于显示的屏幕缓冲区进行渲染操作;
    Off-Screen Rendering:离屏渲染,在当前屏幕缓冲区外开辟新的缓冲区进行渲染操作;

    离屏渲染消耗性能的原因:

    离屏渲染的整个过程,需要多次切换上下文环境,先是从当前屏幕(On-Screen)切换到离屏(Off-Screen),渲染结束后,将离屏缓冲区的渲染结果显示到屏幕上,上下文环境从离屏切换到当前屏幕,这个过程会造成性能的消耗。

    哪些操作会触发离屏渲染?
  • 光栅化,layer.shouldRasterize = YES
  • 遮罩,layer.mask
  • 圆角,同时设置 layer.masksToBounds = YESlayer.cornerRadius > 0
    • 可以用 CoreGraphics 绘制裁剪圆角
  • 阴影
    • 如果设置了 layer.shadowPath 不会产生离屏渲染

    卡顿检测

    这里的卡顿检测主要是针对在主线程执行了耗时的操作所造成的,这样可以通过 RunLoop 来检测卡顿:添加 Observer 到主线程 RunLoop 中,通过监听 RunLoop 状态的切换的耗时,达到监控卡顿的目的。

    耗电优化

    耗电的主要来源为:

    1.CPU 处理;

    2.网络请求;

    3.定位;

    4.图像渲染;

    优化思路

    1.尽可能降低 CPU、GPU 功耗;

    2.少用定时器;

    3.优化 I/O 操作;

    尽量不要频繁写入小数据,最好一次性批量写入;
    读写大量重要数据时,可以用 dispatch_io,它提供了基于 GCD 的异步操作文件的 API,使用该 API 会优化磁盘访问;
    数据量大时,用数据库管理数据;

    4.网络优化;

  • 减少、压缩网络数据(JSON 比 XML 文件性能更高);
  • 若多次网络请求结果相同,尽量使用缓存;
  • 使用断点续传,否则网络不稳定时可能多次传输相同的内容;
  • 网络不可用时,不进行网络请求;
  • 让用户可以取消长时间运行或者速度很慢的网络操作,设置合适的超时时间;
  • 批量传输,如下载视频,不要传输很小的数据包,直接下载整个文件或者大块下载,然后慢慢展示;
  • 5.定位优化

  • 如果只是需要快速确定用户位置,用 CLLocationManager 的 requestLocation 方法定位,定位完成后,定位硬件会自动断电;
  • 若不是导航应用,尽量不要实时更新位置,并为完毕就关掉定位服务;
  • 尽量降低定位精度,如不要使用精度最高的 KCLLocationAccuracyBest
  • 需要后台定位时,尽量设置 pausesLocationUpdatesAutomatically 为 YES,若用户不怎么移动的时候,系统会自暂停位置更新;
  • 启动优化

    App 的启动分为两种:冷启动(Cold Launch) 和热启动(Warm Launch)
    前者表示从零开始启动 App,后者表示 App 已经存在内存中,在后台依然活着,再次点击图标启动 App。

    App 启动的优化主要是针对冷启动的优化,通过添加环境变量可以打印出 App 的启动时间分析:Edit Scheme -> Run -> Arguments -> Environment Variables 添加 DYLD_PRINT_STATISTICS 设置为 1。


    运行程序则会打印:


    这里打印的是在执行 main 函数之前的耗时信息,若想打印更详细的信息则添加环境变量为:
    DYLD_PRINT_STATISTICS_DETAILS 设置为 1。


    App 冷启动

    冷启动可分为三个阶段:dyld 阶段、Runtime 阶段、main 阶段。

    第一个阶段就是处理程序的镜像的阶段,第二个阶段是加载本程序的类、分类信息等等的 Runtime 阶段,最后是调用 main 函数阶段。

    dyld

    dyld(Dynamic Link Editor),Apple 的动态链接器,可以用来装载 Mach-O 文件(可执行文件、动态库等)


    启动 App 时,dyld 会装载 App 的可执行文件,同时会递归加载所有依赖的动态库,当 dyld 把可执行文件、动态库都装载完毕后,会通知 Runtime 进行做下一步的处理。

    Runtime

    启动 App 时,调用 map_images 进行可执行文件的内容解析和处理,再 load_images 中调用 call_load_methods调用所有 Class 和 Category 的 load 方法,然后进行 objc 结构的初始化(注册类、初始化类对象等)。然后调用 C++ 静态初始化器和 __attribute_((constructor)) 修饰的函数,到此为止,可执行文件的和动态库中所有的符号(类、协议、方法等)都已经按照格式加载到内存中,被 Runtime 管理。

    main

    在 Runtime 阶段完成后,dyld 会调用 main 函数,接下来是 UIApplication 函数,AppDelegate 的 application: didFinishLaunchingWithOptions: 函数。

    启动优化思路

    针对不同的阶段,有不同的优化思路:
    dyld

    1.减少动态库、合并动态库,定期清理不必要的动态库;

    2.减少类、分类的数量,减少 Selector 的数量,定期清理不必要的类、分类;

    3.减少 C++ 虚函数数量;

    4.Swift 开发尽量使用 struct;

    虚函数和 Java 中的抽象函数有点类似,但区别是,基类定义的虚函数,子类可以实现也可以不实现,而抽象函数子类一定要实现。

    Runtime

    用 inilialize 方法和 dispatch_once 取代所有的 __attribute_((constructor))、C++ 静态构造器、以及 Objective-C 中的 load 方法;

    main
    将一些耗时操作延迟执行,不要全部都放在 finishLaunching 方法中;

    安装包瘦身

    安装包(ipa)主要由可执行文件和资源文件组成,若不管理妥善则会造成安装包体积越来越大,所以针对资源优化我们可以将资源采取无损压缩,去除没用的资源。

    对于可执行文件的瘦身,我们可以:

    1.从编译器层面优化

    1.Strip Linked Product、Make Strings Read-Only、Symbols Hidden by Default 设置为 YES
    2.去掉异常支持,Enable C++ Exceptions、Enable Objective-C Exceptions 设置为 NO,Other C Flags 添加 -fno-exceptions;
    3.利用 AppCode,检测未使用代码检测:菜单栏 -> Code -> Inspect Code;
    4.编写 LLVM 插件检测重复代码、未调用代码;
    5.通过生成 LinkMap 文件检测;

    LinkMap

    Build Setting -> LD_MAP_FILE_PATH: 设置文件路径 ,Build Setting -> LD_GENERSTE_MAP_FILE -> YES


    运行程序可看到:


    打开可看见各种信息:


    我们可根据这个信息针对某个类进行优化。

    摘自链接:https://www.jianshu.com/p/fe566ec32d28

    收起阅读 »

    iOS Universal Link(点击链接跳转到APP)

    Universe Link跳转流程步骤1.登录苹果开发者中心  选择对应的appid ☑️勾选 Associated Domains  此处标记的Team ID 和 bundle ID  后面文件会用到2. 用text  ...
    继续阅读 »

    Universe Link跳转流程


    步骤

    1.登录苹果开发者中心  选择对应的appid ☑️勾选 Associated Domains  此处标记的Team ID 和 bundle ID  后面文件会用到


    2. 用text   创建  apple-app-site-association  文件     去掉后缀!!!!!


    3.打开xcode 工程 配置下图文件


    4.在appdelegate 里面 回调接收url  获取链接里面的参数


    5.最重要的一步来了!!!!!

    用txt 把创建好的  apple-app-site-association  给后台 开发人员  将此文件 放到服务器的根目录下面 例如 https://www.baidu.com/apple-app-site-association

    重点!!!!!!!!  必须用https  

    收起阅读 »

    iOS--图形图像渲染原理

    引言作为程序员,我们或多或少知道可视化应用程序都是由 CPU 和 GPU 协作执行的。那么我们就先来了解一下两者的基本概念:1.CPU(Central Processing Unit):现代计算机的三大核心部分之一,作为整个系统的运算和控制单元。CPU 内部的...
    继续阅读 »

    引言

    作为程序员,我们或多或少知道可视化应用程序都是由 CPU 和 GPU 协作执行的。那么我们就先来了解一下两者的基本概念:

    1.CPU(Central Processing Unit):现代计算机的三大核心部分之一,作为整个系统的运算和控制单元。CPU 内部的流水线结构使其拥有一定程度的并行计算能力。

    2.GPU(Graphics Processing Unit):一种可进行绘图运算工作的专用微处理器。GPU 能够生成 2D/3D 的图形图像和视频,从而能够支持基于窗口的操作系统、图形用户界面、视频游戏、可视化图像应用和视频播放。GPU 具有非常强的并行计算能力。

    这时候可能会产生一个问题:CPU 难道不能代替 GPU 来进行图形渲染吗?答案当然是肯定的,不过在看了下面这个视频就明白为什么要用 GPU 来进行图形渲染了。

    GPU CPU 模拟绘图视频

    使用 GPU 渲染图形的根本原因就是:速度。GPU 的并行计算能力使其能够快速将图形结果计算出来并在屏幕的所有像素中进行显示。

    那么像素是如何绘制在屏幕上的?计算机将存储在内存中的形状转换成实际绘制在屏幕上的对应的过程称为 渲染。渲染过程中最常用的技术就是 光栅化

    关于光栅化的概念,以下图为例,假如有一道绿光与存储在内存中的一堆三角形中的某一个在三维空间坐标中存在相交的关系。那么这些处于相交位置的像素都会被绘制到屏幕上。当然这些三角形在三维空间中的前后关系也会以遮挡或部分遮挡的形式在屏幕上呈现出来。一句话总结:

    光栅化就是将数据转化成可见像素的过程。


    GPU 则是执行转换过程的硬件部件。由于这个过程涉及到屏幕上的每一个像素,所以 GPU 被设计成了一个高度并行化的硬件部件。

    下面,我们来简单了解一下 GPU 的历史。

    GPU 历史

    GPU 还未出现前,PC 上的图形操作是由 视频图形阵列(VGA,Video Graphics Array) 控制器完成。VGA 控制器由连接到一定容量的DRAM上的存储控制器和显示产生器构成。

    1997 年,VGA 控制器开始具备一些 3D 加速功能,包括用于 三角形生成、光栅化、纹理贴图 和 阴影。

    2000 年,一个单片处图形处理器继承了传统高端工作站图形流水线的几乎每一个细节。因此诞生了一个新的术语 GPU 用来表示图形设备已经变成了一个处理器。

    随着时间的推移,GPU 的可编程能力愈发强大,其作为可编程处理器取代了固定功能的专用逻辑,同时保持了基本的 3D 图形流水线组织。

    近年来,GPU 增加了处理器指令和存储器硬件,以支持通用编程语言,并创立了一种编程环境,从而允许使用熟悉的语言(包括 C/C++)对 GPU 进行编程。

    如今,GPU 及其相关驱动实现了图形处理中的 OpenGL 和 DirectX 模型,从而允许开发者能够轻易地操作硬件。OpenGL 严格来说并不是常规意义上的 API,而是一个第三方标准(由 khronos 组织制定并维护),其严格定义了每个函数该如何执行,以及它们的输出值。至于每个函数内部具体是如何实现的,则由 OpenGL 库的开发者自行决定。实际 OpenGL 库的开发者通常是显卡的生产商。DirectX 则是由 Microsoft 提供一套第三方标准。

    GPU 图形渲染流水线


    GPU 图形渲染流水线的主要工作可以被划分为两个部分:

    把 3D 坐标转换为 2D 坐标

    把 2D 坐标转变为实际的有颜色的像素

    GPU 图形渲染流水线的具体实现可分为六个阶段,如下图所示。

    顶点着色器(Vertex Shader)
    形状装配(Shape Assembly),又称 图元装配
    几何着色器(Geometry Shader)
    光栅化(Rasterization)
    片段着色器(Fragment Shader)
    测试与混合(Tests and Blending)


    第一阶段,顶点着色器。该阶段的输入是 顶点数据(Vertex Data) 数据,比如以数组的形式传递 3 个 3D 坐标用来表示一个三角形。顶点数据是一系列顶点的集合。顶点着色器主要的目的是把 3D 坐标转为另一种 3D 坐标,同时顶点着色器可以对顶点属性进行一些基本处理。

    第二阶段,形状(图元)装配。该阶段将顶点着色器输出的所有顶点作为输入,并将所有的点装配成指定图元的形状。图中则是一个三角形。图元(Primitive) 用于表示如何渲染顶点数据,如:点、线、三角形。

    第三阶段,几何着色器。该阶段把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状。例子中,它生成了另一个三角形。

    第四阶段,光栅化。该阶段会把图元映射为最终屏幕上相应的像素,生成片段。片段(Fragment) 是渲染一个像素所需要的所有数据。

    第五阶段,片段着色器。该阶段首先会对输入的片段进行 裁切(Clipping)。裁切会丢弃超出视图以外的所有像素,用来提升执行效率。

    第六阶段,测试与混合。该阶段会检测片段的对应的深度值(z 坐标),判断这个像素位于其它物体的前面还是后面,决定是否应该丢弃。此外,该阶段还会检查 alpha 值( alpha 值定义了一个物体的透明度),从而对物体进行混合。因此,即使在片段着色器中计算出来了一个像素输出的颜色,在渲染多个三角形的时候最后的像素颜色也可能完全不同。

    关于混合,GPU 采用如下公式进行计算,并得出最后的颜色。

    R = S + D * (1 - Sa)

    关于公式的含义,假设有两个像素 S(source) 和 D(destination),S 在 z 轴方向相对靠前(在上面),D 在 z 轴方向相对靠后(在下面),那么最终的颜色值就是 S(上面像素) 的颜色 + D(下面像素) 的颜色 * (1 - S(上面像素) 颜色的透明度)。

    上述流水线以绘制一个三角形为进行介绍,可以为每个顶点添加颜色来增加图形的细节,从而创建图像。但是,如果让图形看上去更加真实,需要足够多的顶点和颜色,相应也会产生更大的开销。为了提高生产效率和执行效率,开发者经常会使用 纹理(Texture) 来表现细节。纹理是一个 2D 图片(甚至也有 1D 和 3D 的纹理)。纹理一般可以直接作为图形渲染流水线的第五阶段的输入。

    最后,我们还需要知道上述阶段中的着色器事实上是一些程序,它们运行在 GPU 中成千上万的小处理器核中。这些着色器允许开发者进行配置,从而可以高效地控制图形渲染流水线中的特定部分。由于它们运行在 GPU 中,因此可以降低 CPU 的负荷。着色器可以使用多种语言编写,OpenGL 提供了 GLSL(OpenGL Shading Language) 着色器语言。

    GPU 存储系统

    早期的 GPU,不同的着色器对应有着不同的硬件单元。如今,GPU 流水线则使用一个统一的硬件来运行所有的着色器。此外,nVidia 还提出了 CUDA(Compute Unified Device Architecture) 编程模型,可以允许开发者通过编写 C 代码来访问 GPU 中所有的处理器核,从而深度挖掘 GPU 的并行计算能力。

    下图所示为 GPU 内部的层级结构。最底层是计算机的系统内存,其次是 GPU 的内部存储,然后依次是两级 cache:L2 和 L1,每个 L1 cache 连接至一个 流处理器(SM,stream processor)。

    SM L1 Cache 的存储容量大约为 16 至 64KB。

    GPU L2 Cache 的存储容量大约为几百 KB。

    GPU 的内存最大为 12GB。

    GPU 上的各级存储系统与对应层级的计算机存储系统相比要小不少。

    此外,GPU 内存并不具有一致性,也就意味着并不支持并发读取和并发写入。


    GPU 流处理器

    下图所示为 GPU 中每个流处理器的内部结构示意图。每个流处理器集成了一个 L1 Cache。顶部是处理器核共享的寄存器堆。


    CPU-GPU 异构系统

    至此,我们大致了解了 GPU 的工作原理和内部结构,那么实际应用中 CPU 和 GPU 又是如何协同工作的呢?

    下图所示为两种常见的 CPU-GPU 异构架构。

    左图是分离式的结构,CPU 和 GPU 拥有各自的存储系统,两者通过 PCI-e 总线进行连接。这种结构的缺点在于 PCI-e 相对于两者具有低带宽和高延迟,数据的传输成了其中的性能瓶颈。目前使用非常广泛,如PC、智能手机等。

    右图是耦合式的结构,CPU 和 GPU 共享内存和缓存。AMD 的 APU 采用的就是这种结构,目前主要使用在游戏主机中,如 PS4。

    注意,目前很多 SoC 都是集成了CPU 和 GPU,事实上这仅仅是在物理上进行了集成,并不意味着它们使用的就是耦合式结构,大多数采用的还是分离式结构。耦合式结构是在系统上进行了集成。

    在存储管理方面,分离式结构中 CPU 和 GPU 各自拥有独立的内存,两者共享一套虚拟地址空间,必要时会进行内存拷贝。对于耦合式结构,GPU 没有独立的内存,与 GPU 共享系统内存,由 MMU 进行存储管理。

    图形应用程序调用 OpenGL 或 Direct3D API 功能,将 GPU 作为协处理器使用。API 通过面向特殊 GPU 优化的图形设备驱动向 GPU 发送命令、程序、数据。

    GPU 资源管理模型

    下图所示为分离式异构系统中 GPU 的资源管理模型示意图。


    MMIO(Memory-Mapped I/O)

    CPU 通过 MMIO 访问 GPU 的寄存器状态。
    通过 MMIO 传送数据块传输命令,支持 DMA 的硬件可以实现块数据传输。

    GPU Context

    上下文表示 GPU 的计算状态,在 GPU 中占据部分虚拟地址空间。多个活跃态下的上下文可以在 GPU 中并存。

    CPU Channel

    来自 CPU 操作 GPU 的命令存储在内存中,并提交至 GPU channel 硬件单元。
    每个 GPU 上下文可拥有多个 GPU Channel。每个 GPU 上下文都包含 GPU channel 描述符(GPU 内存中的内存对象)。
    每个 GPU Channel 描述符存储了channel 的配置,如:其所在的页表。
    每个 GPU Channel 都有一个专用的命令缓冲区,该缓冲区分配在 GPU 内存中,通过 MMIO 对 CPU 可见。

    GPU 页表

    GPU 上下文使用 GPU 页表进行分配,该表将虚拟地址空间与其他地址空间隔离开来。
    GPU 页表与 CPU 页表分离,其驻留在 GPU 内存中,物理地址位于 GPU 通道描述符中。
    通过 GPU channel 提交的所有命令和程序都在对应的 GPU 虚拟地址空间中执行。
    GPU 页表将 GPU 虚拟地址不仅转换为 GPU 设备物理地址,还转换为主机物理地址。这使得 GPU 页面表能够将 GPU 存储器和主存储器统一到统一的 GPU 虚拟地址空间中,从而构成一个完成的虚拟地址空间。

    PFIFO Engine

    PFIFO 是一个提交 GPU 命令的特殊引擎。
    PFIFO 维护多个独立的命令队列,即 channel。
    命令队列是带有 put 和 get 指针的环形缓冲器。
    PFIFO 引擎会拦截多有对通道控制区域的访问以供执行。
    GPU 驱动使用一个通道描述符来存储关联通道的设置。

    BO

    缓冲对象(Buffer Object)。一块内存,可以用来存储纹理,渲染对象,着色器代码等等。

    CPU-GPU 工作流

    下图所示为 CPU-GPU 异构系统的工作流,当 CPU 遇到图像处理的需求时,会调用 GPU 进行处理,主要流程可以分为以下四步:

    1.将主存的处理数据复制到显存中

    2.CPU 指令驱动 GPU

    3.GPU 中的每个运算单元并行处理

    4.GPU 将显存结果传回主存


    屏幕图像显示原理

    介绍屏幕图像显示的原理,需要先从 CRT 显示器原理说起,如下图所示。CRT 的电子枪从上到下逐行扫描,扫描完成后显示器就呈现一帧画面。然后电子枪回到初始位置进行下一次扫描。为了同步显示器的显示过程和系统的视频控制器,显示器会用硬件时钟产生一系列的定时信号。当电子枪换行进行扫描时,显示器会发出一个水平同步信号(horizonal synchronization),简称 HSync;而当一帧画面绘制完成后,电子枪回复到原位,准备画下一帧前,显示器会发出一个垂直同步信号(vertical synchronization),简称 VSync。显示器通常以固定频率进行刷新,这个刷新率就是 VSync 信号产生的频率。虽然现在的显示器基本都是液晶显示屏了,但其原理基本一致。


    下图所示为常见的 CPU、GPU、显示器工作方式。CPU 计算好显示内容提交至 GPU,GPU 渲染完成后将渲染结果存入帧缓冲区,视频控制器会按照 VSync 信号逐帧读取帧缓冲区的数据,经过数据转换后最终由显示器进行显示。


    最简单的情况下,帧缓冲区只有一个。此时,帧缓冲区的读取和刷新都都会有比较大的效率问题。为了解决效率问题,GPU 通常会引入两个缓冲区,即 双缓冲机制。在这种情况下,GPU 会预先渲染一帧放入一个缓冲区中,用于视频控制器的读取。当下一帧渲染完毕后,GPU 会直接把视频控制器的指针指向第二个缓冲器。


    双缓冲虽然能解决效率问题,但会引入一个新的问题。当视频控制器还未读取完成时,即屏幕内容刚显示一半时,GPU 将新的一帧内容提交到帧缓冲区并把两个缓冲区进行交换后,视频控制器就会把新的一帧数据的下半段显示到屏幕上,造成画面撕裂现象,如下图:


    为了解决这个问题,GPU 通常有一个机制叫做垂直同步(简写也是 V-Sync),当开启垂直同步后,GPU 会等待显示器的 VSync 信号发出后,才进行新的一帧渲染和缓冲区更新。这样能解决画面撕裂现象,也增加了画面流畅度,但需要消费更多的计算资源,也会带来部分延迟。

    摘自:http://chuquan.me/2018/08/26/graphics-rending-principle-gpu

    收起阅读 »

    iOS中的emoji表情处理

    emoji在社交类APP很常用,比如发动态,圈子,还有回复评论,还有会话后台在处理emoji的态度,直接就是不处理,所以我们需要对emoji包括中文,数字,还有特殊字符进行编码还有解码//编码NSString *uniStr = [NSString strin...
    继续阅读 »

    emoji在社交类APP很常用,比如发动态,圈子,还有回复评论,还有会话


    后台在处理emoji的态度,直接就是不处理,所以我们需要对emoji包括中文,数字,还有特殊字符进行编码还有解码

    //编码

    NSString *uniStr = [NSString stringWithUTF8String:[_barrageText.text UTF8String]];
    NSData *uniData = [uniStr dataUsingEncoding:NSNonLossyASCIIStringEncoding];
    NSString *goodStr = [[NSString alloc] initWithData:uniData encoding:NSUTF8StringEncoding] ;
    NSLog(@"---编码--->[%@]",goodStr);

    //解码

    const char *jsonString = [goodStr UTF8String];   // goodStr 服务器返回的 json
    NSData *jsonData = [NSData dataWithBytes:jsonString length:strlen(jsonString)];
    NSString *goodMsg1 = [[NSString alloc] initWithData:jsonData encoding:NSNonLossyASCIIStringEncoding];
    NSLog(@"---解码--->[%@]",goodMsg1);

    2017-05-15 10:16:17.858 DFRomwe[650:153981] ---编码--->[hello\ud83d\ude18\ud83d\ude18world\u4e16\u754chaha\ud83d\ude17]
    2017-05-15 10:16:17.859 DFRomwe[650:153981] ---解码--->[hello😘😘world世界haha😗]

    总想着事情就能这么轻松解决!!!
    可是,然后,呵呵呵,你不去了解一下东西,还是不行的
    果然,后台不作处理的情况下,如果返回JSON这就不行了,因为会默认带有转义字符: *** "\" *** 会导致下面这个情况:

    //在这里以😀表情为例,😀的Unicode编码为U+1F604,UTF-16编码为:\ud83d\ude04
    NSString * emojiUnicode = @"\U0001F604";
    NSLog(@"emojiUnicode:%@",emojiUnicode);
    //如果直接输入\ud83d\ude04会报错,加了转义后不会报错,但是会输出字符串\ud83d\ude04,而不是😀
    NSString * emojiUTF16 = @"\\ud83d\\ude04";
    NSLog(@"emojiUTF16:%@",emojiUTF16);
    //转换
    emojiUTF16 = [NSString stringWithCString:[emojiUTF16 cStringUsingEncoding:NSUTF8StringEncoding] encoding:NSNonLossyASCIIStringEncoding];
    NSLog(@"emojiUnicode2:%@",emojiUTF16);

    输出:

    emojiUnicode:😄
    emojiUnicode1:\ud83d\ude04
    emojiUnicode2:😄

    果断百度另外的方法

    //解码
    - (NSString *)decodeEmoji{
    NSString *tepStr1 ;
    if ([self containsString:@"\\u"]) {
    tepStr1 = [self stringByReplacingOccurrencesOfString:@"\\u"withString:@"\U"];
    }else{
    tepStr1 = [self stringByReplacingOccurrencesOfString:@"\u"withString:@"\U"];
    }
    NSString *tepStr2 = [tepStr1 stringByReplacingOccurrencesOfString:@"""withString:@"\""];
    NSString *tepStr3 = [[@""" stringByAppendingString:tepStr2]stringByAppendingString:@"""];
    NSData *tepData = [tepStr3 dataUsingEncoding:NSUTF8StringEncoding];
    NSString *axiba = [NSPropertyListSerialization propertyListWithData:tepData options:NSPropertyListMutableContainers format:NULL error:NULL];
    return [axiba stringByReplacingOccurrencesOfString:@"\r\n"withString:@"\n"];
    }

    //编码
    - (NSString *)encodeEmoji{

    NSUInteger length = [self length];
    NSMutableString *s = [NSMutableString stringWithCapacity:0];

    for (int i = 0;i < length; i++){
    unichar _char = [self characterAtIndex:i];
    //判断是否为英文和数字
    if (_char <= '9' && _char >='0'){
    [s appendFormat:@"%@",[self substringWithRange:NSMakeRange(i,1)]];
    }else if(_char >='a' && _char <= 'z'){
    [s appendFormat:@"%@",[self substringWithRange:NSMakeRange(i,1)]];
    }else if(_char >='A' && _char <= 'Z')
    {
    [s appendFormat:@"%@",[self substringWithRange:NSMakeRange(i,1)]];
    }else{
    [s appendFormat:@"\\"];

    [s appendFormat:@"\\u%x",[self characterAtIndex:i]];
    }
    }
    return s;

    }

    这是从JSON解码与编码,其实原理也很简单:

    A :就是把多余的转义斜杠扔掉,

    B :然后Unicode转utf-8;

    C :然后utf-8转Unicode;

    这里我写了一个NSString的一个分类:#import "NSString+Emoji.h"

    还添加了一些方法:

    //判断是否存在emoji表情:因为emoji表情室友Unicode编码区间的

    + (BOOL)stringContainsEmoji:(NSString *)string
    {
    __block BOOL returnValue = NO;
    [string enumerateSubstringsInRange:NSMakeRange(0, [string length])
    options:NSStringEnumerationByComposedCharacterSequences
    usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) {
    const unichar hs = [substring characterAtIndex:0];
    if (0xd800 <= hs && hs <= 0xdbff) {
    if (substring.length > 1) {
    const unichar ls = [substring characterAtIndex:1];
    const int uc = ((hs - 0xd800) * 0x400) + (ls - 0xdc00) + 0x10000;
    if (0x1d000 <= uc && uc <= 0x1f77f) {
    returnValue = YES;
    }
    }
    } else if (substring.length > 1) {
    const unichar ls = [substring characterAtIndex:1];
    if (ls == 0x20e3) {
    returnValue = YES;
    }
    } else {
    if (0x2100 <= hs && hs <= 0x27ff) {
    returnValue = YES;
    } else if (0x2B05 <= hs && hs <= 0x2b07) {
    returnValue = YES;
    } else if (0x2934 <= hs && hs <= 0x2935) {
    returnValue = YES;
    } else if (0x3297 <= hs && hs <= 0x3299) {
    returnValue = YES;
    } else if (hs == 0xa9 || hs == 0xae || hs == 0x303d || hs == 0x3030 || hs == 0x2b55 || hs == 0x2b1c || hs == 0x2b1b || hs == 0x2b50) {
    returnValue = YES;
    }
    }

    }];
    return returnValue;
    }

    //判断是否存在中文
    //因为要保证之前的utf-8的数据也能显示
    - (BOOL)includeChinese
    {
    for(int i=0; i< [self length];i++)
    {
    int a =[self characterAtIndex:i];
    if( a >0x4e00&& a <0x9fff){
    return YES;
    }
    }
    return NO;
    }

    //判断是否以中文开头

    - (BOOL)JudgeChineseFirst{
    //是否以中文开头(unicode中文编码范围是0x4e00~0x9fa5)
    int utfCode = 0;
    void *buffer = &utfCode;
    NSRange range = NSMakeRange(0, 1);
    //判断是不是中文开头的,buffer->获取字符的字节数据 maxLength->buffer的最大长度 usedLength->实际写入的长度,不需要的话可以传递NULL encoding->字符编码常数,不同编码方式转换后的字节长是不一样的,这里我用了UTF16 Little-Endian,maxLength为2字节,如果使用Unicode,则需要4字节 options->编码转换的选项,有两个值,分别是NSStringEncodingConversionAllowLossy和NSStringEncodingConversionExternalRepresentation range->获取的字符串中的字符范围,这里设置的第一个字符 remainingRange->建议获取的范围,可以传递NULL
    BOOL b = [self getBytes:buffer maxLength:2 usedLength:NULL encoding:NSUTF16LittleEndianStringEncoding options:NSStringEncodingConversionExternalRepresentation range:range remainingRange:NULL];
    if (b && (utfCode >= 0x4e00 && utfCode <= 0x9fa5))
    return YES;
    else
    return NO;
    }


    收起阅读 »

    iOS .a与framework打包以及shell自动合并

    静态库打包的流程:.a打包将提前准备的项目文件及项目资源导入到SDK制作工程中添加New Header Phase将制作静态库需要的.h文件添加到Project中,将静态库调用的头文件添加到Public中静态库打包bundle文件>由于演示制作的静态库包...
    继续阅读 »

    静态库打包的流程:


    .a打包


    将提前准备的项目文件及项目资源导入到SDK制作工程中


    添加New Header Phase


    将制作静态库需要的.h文件添加到Project中,将静态库调用的头文件添加到Public中


    静态库打包bundle文件>由于演示制作的静态库包含图片和xib文件,因此为了规范,我们需要把图片和xib文件添加到bundle中,如图添加给静态库添加bundle资源包


    创建好之后,将图片和xib文件添加到Copy Bundle Resources中


    由于.bundle文件属于macOX类型,所以我们需要改一些配置来适配iOS,如图所示
    TARGETS ->选择bundle -> Build Settings ->Base SDK ->选择Latest iOS (iOS 11.2)

    设置Build Setting 中的COMBINE_HIDPI_IMAEGS 为NO,否则Bundle中的图片就是tiff格式了。


    作为资源包,仅仅需要编译就好,无需安装相关配置,设置Skip Install为YES,同样需要删除安装路径Installation Dirctory的值



    到此为止bundle文件的设置完成


    打包工程和资源文件

    找到源文件路径,如下图所示,到此静态库制作完成,将.libStaticSDK.a和source.bundle和头文件StaticSDK.h导入到项目中即可使用


    找到源文件路径

    3、合并静态库真机和模拟器文件

    我们在制作静态库的时候,编译会产两个.a文件,一个适用于模拟器的,一个是用于真机的,为了开发方便我们可以使用终端命令将.a文件进行合并

    lipo -create XXX/模拟器.a路径 XXX/真机.a路径 -output 合并后的文件名称.a

    4、注意点,由于资源文件在Bundle文件中因此在使用时需注意,以下我举两个例子,一个是加载图片,一个是加载xib文件




    对于使用了Cocoapod导入第三方的xcode工程来讲 需要在Podfile中 做如下修改 之后 pod install
    需要同时对住工程target 和Framework的target 配置pod环境



    2.build Setting 设置

    选择工程文件>target第一项>Build Setting>搜索linking,然后几个需要设置的选项都显现出来,首先是Dead Code Stripping设置为NO,网上对此项的解释如下,大致意思是如果开启此项就会对代码中的”dead”、”unreachable”的代码过滤,不过这个开关是否关闭,似乎没有多大影响,不过为了完整还原framework中的代码,将此项关闭也未曾不可。

    The resulting executable will not include any “dead” or unreachable code

    然后将Link With Standard Libraries关闭,我想可能是为了避免重复链接

    最后将Mach-O Type设为Static Library,framework可以是动态库也可以是静态库,对于系统的framework是动态库,而用户制作的framework只能是静态库。

    开始将下图中的build Active Architecture only选项设为YES,导致其编译时只生成当前机器的框架,将其设置为NO后,发现用模拟器编译后生成的framework同时包含x86_64和i386架构。不过这个无所谓,我们之后会使用编译脚本,脚本会将所有的架构全包含


    分别编译

    show in finder 如下

    Debug-iphoneos 为Debug模式下真机使用的
    Debug-iphonesimulator 为Debug模式下模拟器使用的
    Release -iphoneos 为Release模式下真机使用的
    Release-iphonesimulator 为Release模式下模拟器使用的



    下面的合并和.a一样操作

    下面介绍自动shell脚本合并

    1:生成脚本target


    2.target设置

    1.添加target依赖

    Target Dependencies 选中需要打包的framework + 选择New Run Script Phase 出现 Run Scirpt

    2.设置脚本路径

    可以在命令行里设置
    也可以直接将脚本粘贴在这里


    # 取得项目名字(get project name)
    FMK_NAME=${PROJECT_NAME}
    # 取得生成的静态库文件路径 (get framework path)
    INSTALL_DIR=${SRCROOT}/Products/${FMK_NAME}.framework
    # 设置真机和模拟器生成的静态库路径 (set devcie framework and simulator framework path)
    WRK_DIR=build
    DEVICE_DIR=${WRK_DIR}/Release-iphoneos/${FMK_NAME}.framework
    SIMULATOR_DIR=${WRK_DIR}/Release-iphonesimulator/${FMK_NAME}.framework
    # 模拟器和真机编译 (device and simulator build)
    xcodebuild -configuration "Release" -target "${FMK_NAME}" -sdk iphoneos clean build
    xcodebuild -configuration "Release" -target "${FMK_NAME}" -sdk iphonesimulator clean build
    # 删除临时文件 (delete temp file)
    if [ -d "${INSTALL_DIR}" ]
    then
    rm -rf "${INSTALL_DIR}"
    fi
    mkdir -p "${INSTALL_DIR}"
    # 拷贝真机framework文件到生成路径下 (copy device file to product path)
    cp -R "${DEVICE_DIR}/" "${INSTALL_DIR}/"
    # 合并生成,替换真机framework里面的二进制文件,并且打开 (merger and open)
    lipo -create "${DEVICE_DIR}/${FMK_NAME}" "${SIMULATOR_DIR}/${FMK_NAME}" -output "${INSTALL_DIR}/${FMK_NAME}"
    echo "${DEVICE_DIR}/${FMK_NAME}"
    echo "${SIMULATOR_DIR}/${FMK_NAME}"
    rm -rf "${WRK_DIR}"
    open "${INSTALL_DIR}"



    摘自作者:Cooci
    原贴链接:https://www.jianshu.com/p/bf1cc6ac7d17

    收起阅读 »

    腾讯iOS面试题一分析

    网络相关:1. 项目使用过哪些网络库?用过ASIHttp库嘛AFNetworking、ASIHttpRequest、Alamofire(swift)1、AFN的底层实现基于OC的NSURLConnection和NSURLSession2、ASI的底层实现基于纯...
    继续阅读 »

    网络相关:

    1. 项目使用过哪些网络库?用过ASIHttp库嘛
    AFNetworking、ASIHttpRequest、Alamofire(swift)
    1、AFN的底层实现基于OC的NSURLConnection和NSURLSession
    2、ASI的底层实现基于纯C语言的CFNetwork框架
    3、因为NSURLConnection和NSURLSession是在CFNetwork之上的一层封装,因此ASI的运行性能高于AFN

    2. 断点续传怎么实现的?
    需要怎么设置断点续传就是从文件上次中断的地方开始重新下载或上传数据。要实现断点续传 , 服务器必须支持(这个很重要,一个巴掌是拍不响的,如果服务器不支持,那么客户端写的再好也没用)。总结:断点续传主要依赖于 HTTP 头部定义的 Range 来完成。有了 Range,应用可以通过 HTTP 请求获取失败的资源,从而来恢复下载该资源。当然并不是所有的服务器都支持 Range,但大多数服务器是可以的。Range 是以字节计算的,请求的时候不必给出结尾字节数,因为请求方并不一定知道资源的大小。

    // 1 指定下载文件地址 URLString
    // 2 获取保存的文件路径 filePath
    // 3 创建 NSURLRequest
    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:URLString]];
    unsigned long long downloadedBytes = 0;

    if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
    // 3.1 若之前下载过 , 则在 HTTP 请求头部加入 Range
    // 获取已下载文件的 size
    downloadedBytes = [self fileSizeForPath:filePath];

    // 验证是否下载过文件
    if (downloadedBytes > 0) {
    // 若下载过 , 断点续传的时候修改 HTTP 头部部分的 Range
    NSMutableURLRequest *mutableURLRequest = [request mutableCopy];
    NSString *requestRange =
    [NSString stringWithFormat:@"bytes=%", downloadedBytes];
    [mutableURLRequest setValue:requestRange forHTTPHeaderField:@"Range"];
    request = mutableURLRequest;
    }
    }
    // 4 创建 AFHTTPRequestOperation
    AFHTTPRequestOperation *operation
    = [[AFHTTPRequestOperation alloc] initWithRequest:request];

    // 5 设置操作输出流 , 保存在第 2 步的文件中
    operation.outputStream = [NSOutputStream
    outputStreamToFileAtPath:filePath append:YES];

    // 6 设置下载进度处理 block
    [operation setDownloadProgressBlock:^(NSUInteger bytesRead,
    long long totalBytesRead, long long totalBytesExpectedToRead) {
    // bytesRead 当前读取的字节数
    // totalBytesRead 读取的总字节数 , 包含断点续传之前的
    // totalBytesExpectedToRead 文件总大小
    }];

    // 7 设置 success 和 failure 处理 block
    [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation
    *operation, id responseObject) {

    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {

    }];

    // 8 启动 operation
    [operation start];

    3. HTTP请求 什么时候用post、get、put ?GET方法:对这个资源的查操作

    • GET参数通过URL传递,POST放在Request body中。

    • GET请求会被浏览器主动cache,而POST不会,除非手动设置

    • GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。

    • Get 请求中有非 ASCII 字符,会在请求之前进行转码,POST不用,因为POST在Request body中,通过 MIME,也就可以传输非 ASCII 字符。

    • 一般我们在浏览器输入一个网址访问网站都是GET请求

    • HTTP的底层是TCP/IP。HTTP只是个行为准则,而TCP才是GET和POST怎么实现的基本。GET/POST都是TCP链接。GET和POST能做的事情是一样一样的。但是请求的数据量太大对浏览器和服务器都是很大负担。所以业界有了不成文规定,(大多数)浏览器通常都会限制url长度在2K个字节,而(大多数)服务器最多处理64K大小的url。

    • GET产生一个TCP数据包;POST产生两个TCP数据包。对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);而对于POST,浏览器先发送header,服务器响应100 continue,浏览器再发送data,服务器响应200 ok(返回数据)。

    • 在网络环境好的情况下,发一次包的时间和发两次包的时间差别基本可以无视。而在网络环境差的情况下,两次包的TCP在验证数据包完整性上,有非常大的优点。但并不是所有浏览器都会在POST中发送两次包,Firefox就只发送一次。

    PUT和POS都有更改指定URI的语义.但PUT被定义为idempotent的方法,POST则不是.idempotent的方法:如果一个方法重复执行
    多次,产生的效果是一样的,那就是idempotent的。也就是说:
    PUT请求:如果两个请求相同,后一个请求会把第一个请求覆盖掉。(所以PUT用来改资源)
    Post请求:后一个请求不会把第一个请求覆盖掉。(所以Post用来增资源)

    4. HTTP建立断开连接的时候为什么要 三次握手、四次挥手?
    因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。
    client请求连接,Serve发送确认连接,client回复确认连接 ==>连接建立
    但是关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,"你发的FIN报文我收到了"。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。
    注意:
    client两个等待,FIN_Wait 和 Time_WaitTIME_WAIT状态需要经过2MSL(最大报文段生存时间)才能返回到CLOSE状态>。虽然按道理,四个报文都发送完毕,我们可以直接进入CLOSE状态了,但是我们必须假象网络是不可靠的,有可以最后一个ACK丢失。所以TIME_WAIT状态就是用来重发可能丢失的ACK报文。
    client请求断开,Server收到断开请求,server发送断开,client回复断开确认 ==>连接断

    5. 项目中的数据存储都有哪些,iOS中有哪些数据存储方法,什么时候用?

    • 文件

    • NSUserDefaults

    • 数据库4、KeyChain5、iCloud

    文件

    • 沙盒

    • Plist

    • NSKeyedArchiver归档 / NSKeyedUnarchiver解档
      NSUserDefaults

    数据库

    • SQLite3

    • FMDB

    • Core Data

    6、MVVM如何实现绑定?
    MVVM 的实现可以采用KVO进行数据绑定,也可以采用RAC。其实还可以采用block、代理(protocol)实现。
    MVVM比起MVC最大的好处就是可以实现自动绑定,将数据绑定在UI组件上,当UI中的值发生变化时,那么它对应的模型中也跟随着发生变化,这就是双向绑定机制,原因在于它在视图层和数据模型层之间实现了一个绑定器,绑定器可以管理两个值,它一直监听组件UI的值,只要发生变化,它将会把值传输过去改变model中的值。绑定器比较灵活,还可以实现单向绑定。
    实际开发中的做法:

    • 让Controller拥有View和ViewModel属性,VM拥有Model属性;Controller或者View来接收ViewModel发送的Model改变的通知

    • 用户的操作点击或者Controller的视图生命周期里面让ViewModel去执行请求,请求完成后ViewModel将返回数据模型化并保存,从而更新了Model;Controller和View是属于V部分,即实现V改变M(V绑定M)。如果不需要请求,这直接修改Model就是了。

    • 第2步中的Model的改变,VM是知道的(因为持有关系),只需要Model改变后发一个通知;Controller或View接收到通知后(一般是Controller先接收再赋值给View),根据这个新Model去改变视图就完成了M改变V(M绑定V) 。使用RAC(RactiveCocoa)框架实现绑定可以简单到一句话概括:ViewModel中创建好请求的信号RACSignal, Controller中订阅这个信号,在ViewModel完成请求后订阅者调用sendNext:方法,Controller里面订阅时写的block就收到回调了。

    7、block 和 通知的区别
    通知:
    一对多
    Block:

    • 通常拿来OC中的block和swift中的闭包来比较.

    • block注重的是过程

    • block会开辟内存,消耗比较大,delegate则不会

    • block防止循环引用,要用弱引用

    Delegate:
    代理注重的是过程,是一对一的,对于一个协议就只能用一个代理,更适用于多个回调方法(3个以上),block则适用于1,2个回调时

    8、进程间通信方式?线程间通信?

    • URL scheme
      这个是iOS APP通信最常用到的通信方式,APP1通过openURL的方法跳转到APP2,并且在URL中带上想要的参数,有点类似HTTP的get请求那样进行参数传递。这种方式是使用最多的最常见的,使用方法也很简单只需要源APP1在info.plist中配置LSApplicationQueriesSchemes,指定目标App2的scheme;然后再目标App2的info.plist 中配置好URLtypes,表示该App接受何种URL scheme的唤起。

    • Keychain
      iOS 系统的keychain是一个安全的存储容器,它本质上就是一个sqlite数据库,它的位置存储在/private/var/Keychains/keychain-2.db,不过它索八坪村的所有数据都是经过加密的,可以用来为不同的APP保存敏感信息,比如用户名,密码等。iOS系统自己也用keychain来保存VPN凭证和WiFi密码。它是独立于每个APP的沙盒之外的,所以即使APP被删除之后,keychain里面的信息依然存在

    10、UIPasteBoard
    是剪切板功能,因为iOS 的原生空间UItextView,UItextfield,UIwebView ,我们在使用时如果长按,就回出现复制、剪切、选中、全选、粘贴等功能,这个就是利用系统剪切板功能来实现的。

    11、UIDocumentInteractionController
    uidocumentinteractioncontroller 主要是用来实现同设备上APP之间的贡献文档,以及文档预览、打印、发邮件和复制等功能。

    12、Local socket
    原理:一个APP1在本地的端口port1234 进行TCP的bind 和 listen,另外一个APP2在同一个端口port1234发起TCP的connect连接,这样就可以简历正常的TCP连接,进行TCP通信了,然后想传什么数据就可以传什么数据了、

    13、AirDrop
    通过 Airdrop实现不同设备的APP之间文档和数据的分享

    14、UIActivityViewController
    iOS SDK 中封装好的类在APP之间发送数据、分享数据和操作数据

    15、APP Groups
    APP group用于同一个开发团队开发的APP之间,包括APP和extension之间共享同一份读写空间,进行数据共享。同一个团队开发的多个应用之间如果能直接数据共享,大大提高用户体验

    • 线程间通信的体现
      1 .一个线程传递数据给另一个线程
      2 .在一个线程中执行完特定任务后,转到另一个线程继续执行任务复制

    • 代码线程间通信常用的方法
      1、NSThread可以先将自己的当前线程对象注册到某个全局的对象中去,这样相互之间就可以获取对方的线程对象,然后就可以使用下面的方法进行线程间的通信了,由于主线程比较特殊,所以框架直接提供了在主线程执行的方法>    

    - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;

    - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg
    waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);

         2、 GCD一个线程传递数据给另一个线程,如

    {   dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    NSLog(@"donwload---%@", [NSThread currentThread]);

    // 1.子线程下载图片 //耗时操作
    NSURL *url = [NSURL URLWithString:@"http://d.jpg"];
    NSData *data = [NSData dataWithContentsOfURL:url];
    UIImage *image = [UIImage imageWithData:data];

    // 2.回到主线程设置图片
    dispatch_async(dispatch_get_main_queue(), ^{

    NSLog(@"setting---%@ %@", [NSThread currentThread], image);

    [self.button setImage:image forState:UIControlStateNormal];
    });
    });

    16、如何检测应用卡顿问题?
    NSRunLoop调用方法主要就是在kCFRunLoopBeforeSources和kCFRunLoopBeforeWaiting之间,还有kCFRunLoopAfterWaiting之后,也就是如果我们发现这两个时间内耗时太长,那么就可以判定出此时主线程卡顿。

    链接:https://www.jianshu.com/p/7484830d9d74

    收起阅读 »

    iOS 头条一面 面试题

    1、如何高效的切圆角?切圆角共有以下三种方案:cornerRadius + masksToBounds:适用于单个视图或视图不在列表上且量级较小的情况,会导致离屏渲染。CAShapeLayer+UIBezierPath:会导致离屏渲染,性能消耗严重,不推荐使用...
    继续阅读 »

    1、如何高效的切圆角?
    切圆角共有以下三种方案:

    • cornerRadius + masksToBounds:适用于单个视图或视图不在列表上且量级较小的情况,会导致离屏渲染。

    • CAShapeLayer+UIBezierPath:会导致离屏渲染,性能消耗严重,不推荐使用。

    • Core Graphics:不会导致离屏渲染,推荐使用。

    2、什么是隐式动画和显式动画?
    隐式动画指的是改变属性值而产生的默认的过渡动画(如background、cornerRadius等),不需要初始化任何类,系统自己处理的动画属性;显式动画是指自己创建一个动画对象并附加到layer上,如 CAAnimation、CABasicAnimation、CAKeyframeAnimation。

    3、UIView 和 CALayer 的区别?
    UIView 是 CALayer 的 delegate,UIView 可以响应事件,而 CALayer 则不能。

    4、离屏渲染?
    iOS 在不进行预合成的情况下不会直接在屏幕上绘制该图层,这意味着 CPU 和 GPU 必须先准备好屏幕外上下文,然后才能在屏幕上渲染,这会造成更多时间时间和更多的内存的消耗。

    5、Objective - C 是否支持方法重载(overloading)?
    不支持。方法重载(overloading):允许创建多项名称相同但输入输出类型或个数不同的方法。

    // 这两个方法名字是不一样的,虽然都是writeToFile开头
    -(void) writeToFile:(NSString *)path fromInt:(int)anInt;
    -(void) writeToFile:(NSString *)path fromString:(NSString *)aString;

    注:Swift 是支持的。

    func testFunc() {}
    func testFunc(num: Int) {}

    6、KVC 的应用场景及注意事项
    KVC(key-Value coding) 键值编码,指iOS开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。不需要调用明确的存取方法,这样就可以在运行时动态访问和修改对象的属性,而不是在编译时确定。
    它的四个主要方法:

    - (nullable id)valueForKey:(NSString *)key;                          //直接通过Key来取值
    - (void)setValue:(nullable id)value forKey:(NSString *)key; //通过Key来设值
    - (nullable id)valueForKeyPath:(NSString *)keyPath; //通过KeyPath来取值
    - (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath; //通过KeyPath来设值

    应用场景:

    • 动态取值和设值

    • 访问和改变私有变量

    • 修改控件的内部属性

    注意事项:

    • key 不要传 nil,会导致崩溃,可以通过重写setNilValueForKey:来避免。

    • 传入不存在的 key 也会导致崩溃,可以通过重写valueForUndefinedKey:来避免。

    7、如何异步下载多张小图最后合成一张大图?
    使用Dispatch Group追加block到Global Group Queue,这些block如果全部执行完毕,就会执行Main Dispatch Queue中的结束处理的block。

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_async(group, queue, ^{ /*加载图片1 */ });
    dispatch_group_async(group, queue, ^{ /*加载图片2 */ });
    dispatch_group_async(group, queue, ^{ /*加载图片3 */ });
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    // 合并图片
    });

    8、NSTimer 有什么注意事项?在 dealloc 中调用[timer invalidate];会避免循环引用吗?

    • 时间延后。如果 timer 处于耗时较长的 runloop 中,或者当前 runloop 处于不监视 timer 的 mode 时(如 scrollView 滑动时)。它在下次 runloop 才会触发,所以可能会导致比预期时间要晚。

    • 循环引用。target 强引用 timer,timer 强引用 target。

    时间延后

    使用 dispatch_source_t来提高时间精度。

    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    if (timer) {
    dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, interval * NSEC_PER_SEC), interval * NSEC_PER_SEC, (1ull * NSEC_PER_SEC) / 10);
    dispatch_source_set_event_handler(timer, block);
    dispatch_resume(timer);
    }

    循环引用

    在 dealloc 中调用 [timer invalidate];不会避免循环引用。因为 timer 会对 target 进行强引用,所以在 timer 没被释放之前,根本不会走 target 的 dealloc 方法。
    可以通过以下几种方法来避免:

    如果 iOS 10 及以上,可以使用nit(timeInterval:repeats:block:)。target 不再强引用 timer。记得在 dealloc 中调用 [timer invalidate];,否则会造成内存泄漏。

    timer = Timer(timeInterval: 1.0, repeats: true, block: { [weak self] (timer) in
    self?.timerFunc()
    })

    使用中间件的方式来避免循环引用。

    // 定义
    @implementation WeakTimerTarget
    {
    __weak target;
    SEL selector;
    }

    - (void)timerDidFire:(NSTimer *)timer {
    if(target) {
    [target performSelector:selector withObject:timer];
    } else{
    [timer invalidate];
    }
    }
    @end

    // 使用
    WeakTimerTarget *target = [[WeakTimerTarget alloc] initWithTarget:self selector:@selector(tick)];
    timer = [NSTimer scheduledTimerWithTimeInterval:30.0 target:target selector:@selector(timerDidFire:) ...];

    9、对 property 的理解

    @property = ivar + getter + setter;

    10、Notification 的注意事项
    在哪个线程发送通知,就在哪个线程接受通知。

    11、Runloop的理解
    一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出,通常的代码逻辑 是这样的:

    function loop() {
    initialize();
    do {
    var message = get_next_message();
    process_message(message);
    } while (message != quit);
    }

    12、对 OC 中 Class 的源码理解?其中 cache 的理解?
    Class 的底层用 struct 实现,源码如下:

    struct _class_t {
    struct _class_t *isa;
    struct _class_t *superclass;
    void *cache;
    void *vtable;
    struct _class_ro_t *ro;
    };

    Cache用于缓存最近使用的方法。一个类只有一部分方法是常用的,每次调用一个方法之后,这个方法就被缓存到cache中,下次调用时 runtime 会先在 cache 中查找,如果 cache 中没有,才会去 methodList 中查找。以此提升性能。

    13、项目优化做了哪些方面?

    • 删除无用资源文件及代码

    • 在合适的地方加缓存

    • 耗时长的代码异步执行

    14、如何一劳永逸的检测包的裂变(检测包的大小)?
    这个不知道,希望了解的朋友可以在评论区指出来。

    15、实现一个判断 IP 地址是否合法的方法

    func isIPAddress(str: String) -> Bool {
    guard !str.isEmpty else { return false }
    var isIPAddress = false
    let coms = str.components(separatedBy: ".")
    for com in coms {
    if let intCom = Int(com), intCom >= 0, intCom <= 255 {
    isIPAddress = true
    } else {
    isIPAddress = false
    return isIPAddress
    }
    }
    return isIPAddress
    }


    转自:https://www.jianshu.com/p/62c525efe496

    收起阅读 »

    iOS底层-isa

    Root class (class)其实就是NSObject,NSObject是没有超类的,所以Root class(class)的superclass指向nil。每个Class都有一个isa指针指向唯一的Meta classRoot class(meta)的...
    继续阅读 »

    分析消息的走态


    Root class (class)其实就是NSObject,NSObject是没有超类的,所以Root class(class)的superclass指向nil。

    每个Class都有一个isa指针指向唯一的Meta class

    Root class(meta)的superclass指向Root class(class),也就是NSObject,形成一个回路。

    每个Meta class的isa指针都指向Root class (meta)。

    Root class (meta)的isa指针都指向自己

    这里我记录一个重要的点:

    1.对象方法存在类里面

    2.类方法存在元类里面

    3.元类的方法存在根元类

    这是非常重要的,如果我们没有捋清楚,就无法得知我们的消息接受者!!!

    isa 又是什么?

    所谓isa指针,在OC中对象的声明是这样的

    typedef struct objc_object {
    Class isa;
    } *id;

    对象本身是一个带有指向其类别isa指针的结构体。
    当向一个对象发送消息的时候,实际上是通过isa在对象的类别中找到相应的方法。我们知道OC中除了实例方法之外还有类方法,那么类别是否也是个对象呢?

    typedef struct objc_class *Class;
    struct objc_class {
    Class isa;
    Class super_class;
    /* followed by runtime specific details... */
    };

    从上面类别的结构看来,类别也是一个对象,它拥有一个指向其父类的指针,和一个isa指针。当一个类别使用类方法时,类别作为一个对象同样会使用isa指针找到类方法的实现。这时,isa指向的就是这个类别的元类。

    也就是说

    元类是类别的类。
    所有的类方法都储存在元类当中。

    众所周知Objective-C(以下简称OC)中的消息机制。消息的接收者可以是一个对象,也可以是一个类。那么这两种情况要是统一为一种情况不是更方便吗?苹果当然早就想到了,这也正是元类的用处。苹果统一把消息接收者作为对象。等等,这是说,类也是对象?yes,就是这样。就是说,OC中所有的类都一种对象。由一个类实例化来的对象叫实例对象,这好理解,那么,类作为对象(称之为类对象),又是什么类的对象?当然也容易猜到,就是今天的主题——元类(Metaclass)。现在到给元类下定义的时候了:元类就是类对象所属的类。所以,实例对象是类的实例,类作为对象又是元类的实例。已经说了,OC中所有的类都一种对象,所以元类也是对象,那么元类是什么的实例呢?答曰:根元类,根元类是其自身的实例

    摘自作者:Cooc
    原贴链接:https://www.jianshu.com/p/2d1fdb76ed57

    收起阅读 »

    iOS面试必背的算法面试题

    1、实现二分查找算法int binarySearchWithoutRecursion(int array[], int low, int high, int target) {while (low <= high) { int mid = l...
    继续阅读 »

    1、实现二分查找算法

    int binarySearchWithoutRecursion(int array[], int low, int high, int target) {

    while (low <= high) {
    int mid = low + (high - low) / 2;
    if (array[mid] > target) {
    high = mid - 1;
    } else if (array[mid] < target) {
    low = mid + 1;
    } else {
    //找到目标
    return mid;
    }
    }
    return -1;
    }

    递归实现

    int binarySearch(const int arr[], int low, int high, int target)
    {
    int mid = low + (high - low) / 2;

    if(low > high) {
    return -1;
    } else{
    if(arr[mid] == target) {
    return mid;
    } else if(arr[mid] > target) {
    return binarySearch(arr, low, mid-1, target);
    } else {
    return binarySearch(arr, mid+1, high, target);
    }
    }
    }

    2、 对以下一组数据进行降序排序(冒泡排序)。“24,17,85,13,9,54,76,45,5,63”

    int main(int argc, char *argv[]) {

    int array[10] = {24, 17, 85, 13, 9, 54, 76, 45, 5, 63};

    int num = sizeof(array)/sizeof(int);

    for(int i = 0; i < num - 1; i++) {
    int exchanged = 0;
    for(int j = 0; j < num - 1 - i; j++) {
    if(array[j] < array[j+1]) {
    array[j] = array[j]^array[j+1];
    array[j+1] = array[j+1]^array[j];
    array[j] = array[j]^array[j+1];
    exchanged = 1;
    }
    }
    if (exchanged == 0) {
    break;
    }
    }

    for(int i = 0; i < num; i++) {
    printf("%d ", array[i]);
    }
    }

    3、 对以下一组数据进行升序排序(选择排序)。“86, 37, 56, 29, 92, 73, 15, 63, 30, 8”

    void sort(int a[],int n)
    {
    int i, j, min;

    for(i = 0; i < n - 1; i++) {
    min = i;
    for(j = i + 1; j < n; j++) {
    if(a[min] > a[j]) {
    min = j;
    }
    }

    if(min != i) {
    a[i] = a[i] ^ a[min];
    a[min] = a[min] ^ a[i];
    a[i] = a[i] ^ a[min];
    }
    }
    }

    int main(int argc, const char * argv[]) {

    int numArr[10] = {86, 37, 56, 29, 92, 73, 15, 63, 30, 8};

    sort(numArr, 10);

    for (int i = 0; i < 10; i++) {
    printf("%d, ", numArr[i]);
    }

    return 0;
    }

    4、 快速排序算法

    void sort(int *a, int left, int right) {

    if(left >= right) {
    return ;
    }

    int i = left;

    int j = right;

    int key = a[left];

    while (i < j) {
    while (i < j && key >= a[j]) {
    j--;
    }

    if (i < j) {
    a[i] = a[j];
    }


    while (i < j && key < a[i]) {
    i++;
    }

    if (i < j) {
    a[j] = a[i];
    }
    }

    a[i] = key;

    sort(a, left, i-1);

    sort(a, i+1, right);

    }

    5、 归并排序

    void merge(int sourceArr[], int tempArr[], int startIndex, int midIndex, int endIndex) {

    int i = startIndex;

    int j = midIndex + 1;

    int k = startIndex;

    while (i != midIndex + 1 && j != endIndex + 1) {
    if (sourceArr[i] >= sourceArr[j]) {
    tempArr[k++] = sourceArr[j++];
    } else {
    tempArr[k++] = sourceArr[i++];
    }
    }

    while (i != midIndex + 1) {
    tempArr[k++] = sourceArr[i++];
    }

    while (j != endIndex + 1) {
    tempArr[k++] = sourceArr[j++];
    }

    for (i = startIndex; i <= endIndex; i++) {
    sourceArr[i] = tempArr[i];
    }
    }


    void sort(int souceArr[], int tempArr[], int startIndex, int endIndex) {

    int midIndex;

    if (startIndex < endIndex) {

    midIndex = (startIndex + endIndex) / 2;

    sort(souceArr, tempArr, startIndex, midIndex);

    sort(souceArr, tempArr, midIndex + 1, endIndex);

    merge(souceArr, tempArr, startIndex, midIndex, endIndex);

    }
    }

    int main(int argc, const char * argv[]) {

    int numArr[10] = {86, 37, 56, 29, 92, 73, 15, 63, 30, 8};

    int tempArr[10];

    sort(numArr, tempArr, 0, 9);

    for (int i = 0; i < 10; i++) {
    printf("%d, ", numArr[i]);
    }

    return 0;
    }

    6、 二叉树的先序遍历为FBACDEGH,中序遍历为:ABDCEFGH,请写出这个二叉树的后序遍历结果。
    ADECBHGF
    先序+中序遍历还原二叉树:先序遍历是:ABDEGCFH 中序遍历是:DBGEACHF
    首先从先序得到第一个为A,就是二叉树的根,回到中序,可以将其分为三部分:
    左子树的中序序列DBGE,根A,右子树的中序序列CHF
    接着将左子树的序列回到先序可以得到B为根,这样回到左子树的中序再次将左子树分割为三部分:
    左子树的左子树D,左子树的根B,左子树的右子树GE
    同样地,可以得到右子树的根为C
    类似地将右子树分割为根C,右子树的右子树HF,注意其左子树为空
    如果只有一个就是叶子不用再进行了,刚才的GE和HF再次这样运作,就可以将二叉树还原了

    7、 实现一个字符串“how are you”的逆序输出(编程语言不限)。如给定字符串为“hello world”,输出结果应当为“world hello”,进阶:去掉首尾空格,每个单词间只保留一个空格。

    void reverse(char *start, char *end) {
    if (start == NULL || end == NULL) {
    return;
    }

    //翻转字符
    while (start < end) {
    char tmp = *start;
    *start = *end;
    *end = tmp;

    start++;
    end--;
    }
    }

    char *reverseStrings(char * s){
    if (s == NULL) {
    return '\0';
    }

    //去除多余空格
    char *str = s;
    //去除首部空格
    while(*str != '\0') {
    if (*str != ' ') {
    s = str;
    break;
    }
    str++;
    }
    str = s;
    int i,j;
    i = 0;
    j = 0;
    //去除中间或尾部空格
    while(*(str+i) != '\0') {
    if (*(str+j) == ' ') {
    if (*(str+j+1) == ' ') {
    j++;
    continue;
    } else if (*(str+j+1) == '\0' ) {
    //去掉尾部空格
    *(str+i) = '\0';
    break;
    }
    } else if (*(str+j) == '\0' ) {
    //去掉尾部空格
    *(str+i) = '\0';
    break;
    }
    if (*(str+i) != *(str+j)) {
    *(str+i) = *(str+j);
    }
    i++;
    j++;
    }

    char *start,*end;
    start = s;
    end = s;

    while(*end != '\0') {
    end++;
    }
    end--;

    reverse(start,end);

    //翻转单词
    start = s;
    end = s;

    while (*start != '\0') {
    if (*start == ' ') {
    start++;
    end++;
    } else if (*end == ' ' || *end == '\0'){
    end--;
    reverse(start,end);
    start = ++end;
    } else {
    end++;
    }
    }

    return s;
    }

    int main(int argc, const char * argv[]) {

    char *str = reverseStrings("have a brilliant future");
    while (*str != '\0') {
    printf("%c", *str++);
    }

    return 0;
    }

    8、字符串匹,输出子串第一次出现的下标,具体要求如下:
    给定主串“ababcabc”,模式串“abc”,输出结果为:2
    给定主串 “aaaa”,模式串“bb”,输出结果为:-1
    当模式串为空串的时候,输出结果应为:0
    请实现findStringIndex函数。

    int findStringIndex(char * inputs, char * matchs){
    if (inputs == NULL || matchs == NULL) {
    return -1;
    }

    if (*matchs == '\0') {
    return 0;
    }

    int i = 0,j = 0;
    while (*(inputs + i) != '\0' && *(matchs + j) != '\0') {
    if (*(inputs + i) == *(matchs + j)) {
    i++;
    j++;
    } else {
    i = i-j+1;
    j = 0;
    }
    }
    //模式串到串尾说明匹配成功,返回下标
    if (*(matchs + j) == '\0') {
    return i-j;
    }
    return -1;
    }

    int main(int argc, const char * argv[]) {
    printf("index = %d", findStringIndex("ababcabc", "abc"));
    return 0;
    }

    9、字符串匹配进阶,KMP算法:

    void generateNextArr(char *s,int *next) {
    //初始化
    int k = -1,j = 0;
    //next[0]初始化成-1
    *next = -1;

    while (j < strlen(s) - 1) {
    if (k == -1 || *(s + j) == *(s + k)) {
    j++;
    k++;
    //s[j]==s[next[k]]必然会失配
    if (*(s + j) != *(s+k)) {
    *(next + j) = k;
    } else {
    *(next + j) = *(next + k);
    }
    } else {
    k = *(next + k);
    }
    }

    }

    int kmpMatch(char *inputs, char *matchs) {
    if (inputs == NULL || matchs == NULL) {
    return -1;
    }
    //模式串为空串时返回0
    if (*matchs == '\0') {
    return 0;
    }
    int inputLen = strlen(inputs);
    int len = strlen(matchs);

    int *next = (int *)malloc(len*sizeof(int));

    //生成next数组:失配时模式串下标跳转的位置
    generateNextArr(matchs, next);

    int i = 0,j = 0;
    while (i < inputLen && j < len) {
    if (j == -1 || *(inputs + i) == *(matchs + j)) {
    i++;
    j++;
    } else {
    j = *(next + j);
    }
    }
    if (*(matchs + j) == '\0') {
    return i-j;
    }
    free(next);
    return -1;
    }

    int main(int argc, const char * argv[]) {
    printf("index = %d", kmpMatch("aabcbbabcb", "abc"));
    return 0;
    }

    10、如何实现一个数组每个元素依次向右移动k位,后面的元素依次往前面补。比如: [1, 2, 3, 4, 5] 移动两位变成[4, 5, 1, 2, 3]。
    思路:三次反转
    后K位反转:12354
    前部分反转:32154
    整体全部反转:45123

    int * reverse1(int *arr, int start, int end) {
    while (start < end) {
    arr[start] = arr[start] ^ arr[end];
    arr[end] = arr[end] ^ arr[start];
    arr[start] = arr[start] ^ arr[end];
    start++;
    end--;
    }
    return arr;
    }
    int * moveK(int *arr, int numSize, int k) {
    reverse1(arr, numSize - k, numSize-1);
    reverse1(arr, 0, numSize-k-1);
    reverse1(arr, 0, numSize-1);

    return arr;
    }

    int main(int argc, const char * argv[]) {
    int arr[5] = {1,2,3,4,5};
    int numSize = sizeof(arr) / sizeof(int);
    moveK(arr, numSize, 2);
    for (int i = 0; i < numSize; i++) {
    printf("%d ",arr[i]);
    }
    return 0;
    }

    11、 给定一个字符串,输出本字符串中只出现一次并且最靠前的那个字符的位置?如“abaccddeeef”,字符是b,输出应该是2。

    char findChar(char *s){
    if (s == NULL) {
    return ' ';
    }
    int hashTable[256];
    memset(hashTable, 0, sizeof(hashTable));
    char *p = s;
    while(*p != '\0') {
    hashTable[*p]++;
    p++;
    }
    p = s;

    while(*p != '\0') {
    if (hashTable[*p] == 1) {
    return *p;
    }
    p++;
    }

    return ' ';
    }

    int main(int argc, const char * argv[]) {

    char *inputStr = "abaccddeeef";

    char ch = findChar(inputStr);

    printf("%c \n", ch);

    return 0;

    }

    12、 如何实现链表翻转(链表逆序)?
    思路:每次把第二个元素提到最前面来。

    #include <stdio.h>

    #include <stdlib.h>


    typedef struct NODE {

    struct NODE *next;

    int num;

    }node;


    node *createLinkList(int length) {

    if (length <= 0) {

    return NULL;

    }

    node *head,*p,*q;

    int number = 1;

    head = (node *)malloc(sizeof(node));

    head->num = 1;

    head->next = head;

    p = q = head;

    while (++number <= length) {

    p = (node *)malloc(sizeof(node));

    p->num = number;

    p->next = NULL;

    q->next = p;

    q = p;

    }

    return head;
    }


    void printLinkList(node *head) {

    if (head == NULL) {

    return;

    }

    node *p = head;

    while (p) {

    printf("%d ", p->num);

    p = p -> next;

    }

    printf("\n");

    }


    node *reverseFunc1(node *head) {

    if (head == NULL) {

    return head;


    }


    node *p,*q;

    p = head;

    q = NULL;

    while (p) {

    node *pNext = p -> next;

    p -> next = q;

    q = p;

    p = pNext;

    }

    return q;

    }


    int main(int argc, const char * argv[]) {

    node *head = createLinkList(7);

    if (head) {

    printLinkList(head);

    node *reHead = reverseFunc1(head);

    printLinkList(reHead);

    free(reHead);

    }

    free(head);

    return 0;

    }

    13、删除链表中的重复元素,每个重复元素需要出现一次,如给定链表 1->2->2->3->4->5->5,输出结果应当为 1->2->3->4->5。请实现下面的deleteRepeatElements函数:

    typedef struct NODE {

    struct NODE *next;

    int num;

    } node;

    node *deleteRepeatElements(node *head) {
    if (head == NULL) {
    return head;
    }

    struct ListNode* pNode = head;

    while (pNode && pNode->next) {
    if (pNode->val == pNode->next->val) {
    struct ListNode *tempNode = pNode->next;
    pNode->next = pNode->next->next;
    free(tempNode);
    } else {
    pNode=pNode->next;
    }
    }
    return head;
    }

    14、删除链表中重复的元素,只保留不重复的结点。如:1->1->2->3->4->4->5,输出结果:2->3->5,请实现下面的deleteRepeatElements函数。

    typedef struct NODE {

    struct NODE *next;

    int num;

    } node;

    node *deleteRepeatElements(node *head) {
    if (head == NULL) {
    return head;
    }
    //头结点有可能会被删除,先创建一个头结点
    node *pHead = (node *)malloc(sizeof(node));
    pHead->next = head;
    node *current = pHead;

    while(current->next && current->next->next) {
    if (current->next->val == current->next->next->val) {
    node *tempNode = current->next;
    while(tempNode && tempNode->next && tempNode->val == tempNode->next->val) {
    tempNode = tempNode->next;
    }
    current->next = tempNode->next;
    } else {
    current = current->next;
    }
    }
    return pHead->next;
    }

    15、 打印2-100之间的素数。
    判断素数思路:通过分析我们可知5以上的自然数都可以用6x-1,6x,6x+1,6x+2,6x+3,6x+4,6x+5来代替,又因6x,6x+2=2(3x+1),6x+3=3(2x+1),6x+4=2*(3x+2)以上都不可能是素数,所以只需要判断6x-1,6x+1,6x+5(6x两侧的数)即可。

    int main(int argc, const char * argv[]) {

    for (int i = 2; i < 100; i++) {
    int r = isPrime(i);
    if (r == 1) {
    printf("%ld ", i);
    }
    }

    return 0;
    }


    int isPrime(int n)
    {

    if(n == 2 || n == 3) {
    return 1;
    }

    if(n % 6 != 1 && n % 6 != 5) {
    return 0;
    }

    for(int i = 5; (i * i) <= n; i += 6) {
    if(n % i == 0 || n % (i + 2) == 0) {
    return 0;
    }
    }

    return 1;
    }

    16、计算100以内素数的个数

    int countPrime(int n) {
    int i,j,count = 0;
    //开辟空间
    int *prime = (int *)malloc(sizeof(int) * n);
    //初始默认所有数为素数
    memset(prime, 1, sizeof(int) * n);
    for (i = 2; i < n; i++) {
    if (prime[i]) {
    count++;
    for (j = i + i; j < n; j += i) {
    //标记不是素数
    prime[j] = 0;
    }
    }
    }
    return count;
    }

    17、 求两个整数的最大公约数。

    int gcd(int a, int b) {

    while (a != b) {
    if (a > b) {
    a = a - b;
    } else {
    b = b - a;
    }
    }
    return a;
    }

    转自:https://www.jianshu.com/p/746495327da6

    收起阅读 »

    iOS底层-方法的本质

    通过clang -rewrite-objc main.m -o mian.cpp编译的对象调用方法底层int main(int argc, const char * argv[]) { /* @autoreleasepool */ { __AtAutor...
    继续阅读 »

    通过clang -rewrite-objc main.m -o mian.cpp编译的对象调用方法底层

    int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
    LGPerson *p = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("new"));
    ((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("run"));
    }
    return 0;
    }

    可以看出在我们进行LGPerson初始化的时候,我们都知道会调用alloc,init.我这里为了简单只调用'new'.但是底层不是像我们利用[]调用的,而是调用了一个函数objc_msgSend这就是我们消息发送的方法,因为考虑的参数我们进行了前面的强转.如果有一定C功底就知道objc_msgSend就是发送消息,我们在断点调试ViewDidLoad的时候,发现能打印self,_cmd这就是我们的消息底层默认的两个参数id,SEL

    一个是消息接受者

    一个是消息编号

    我们还可以在objc_msgSend末尾继续加参数,但是考虑到编译参数问题,我们需要关闭严格核查

    我通过SEL能找到函数实现,底层是依赖一个IMP的函数指针

    就会找我们具体的函数实现

    我们模拟是不是也可不断发送消息,模拟四种消息发送:

    LGStudent *s = [LGStudent new];
    [s run];
    // 方法调用底层编译
    // 方法的本质: 消息 : 消息接受者 消息编号 ....参数 (消息体)
    objc_msgSend(s, sel_registerName("run"));
    // 类方法编译底层
    [LGStudent walk];
    objc_msgSend(objc_getClass("LGStudent"), sel_registerName("walk"));

    // 向父类发消息(对象方法)
    struct objc_super mySuper;
    mySuper.receiver = s;
    mySuper.super_class = class_getSuperclass([s class]);
    objc_msgSendSuper(&mySuper, @selector(run));

    //向父类发消息(类方法)
    struct objc_super myClassSuper;
    myClassSuper.receiver = [s class];
    myClassSuper.super_class = class_getSuperclass(object_getClass([s class]));
    objc_msgSendSuper(&myClassSuper, sel_registerName("walk"));




    收起阅读 »

    移动iOS架构起航

    架构就如人体骨架,肌肉和血液还有其他就顺着骨架填充!MVC架构思想MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组...
    继续阅读 »
    架构就如人体骨架,肌肉和血液还有其他就顺着骨架填充!


    MVC架构思想

    MVC全名是Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范,用一种业务逻辑、数据、界面显示分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。MVC被独特的发展起来用于映射传统的输入、处理和输出功能在一个逻辑的图形化用户界面的结构中。

    组成MVC的三个模式分别是组合模式、策咯模式、观察者模式,MVC在软件开发中发挥的威力,最终离不开这三个模式的默契配合

    View层,单独实现了组合模式

    Model层和View层,实现了观察者模式

    View层和Controller层,实现了策咯模式

    MVC要实现的目标是将软件用户界面和业务逻辑分离以使代码 可扩展性、可复用性、可维护性、灵活性加强.


    ViewController过重

    通过上面的图大家也看到了非常完美,但是用起来真有问题!

    但是我们实际开发经常会变形:比如我们ViewController会非常之重,动不动几百行,几千行代码!那么是一些什么东西在里面?

    繁重的网络层

    复杂的UI层

    难受的代理

    啰嗦的业务逻辑

    还有一些其他功能


    控制器(controller)的作用就是这么简单, 用来将不同的View和不同的Model组织在一起,顺便替双方传递消息,仅此而已。

    这里建议:

    繁重的网络层 封装到我们业务逻辑管理者比如:present viewModel

    复杂的UI层就应该是UI的事,直接剥离出VC


    难受的代理就可以封装一个功能类比如我们常写的tableview collectionView的代理 我们就可以抽取出来封装为一个公共模块,一些特定的逻辑就可以利用适配器设计模式,根据相应的model消息转发



    耦合性问题

    经常我们在开发过程中会出现下面的线!


    这样的线对我们重用性,灵活性造成了压力

    这里我推荐大家使用不直接依赖model 利用发送消息的方式传递

    MVP架构思想

    MVP 全称:Model-View-Presenter ;MVP 是从经典的模式MVC演变而来,它们的基本思想有相通的地方:Controller/Presenter负责逻辑的处理,Model提供数据,View负责显示。

    我最喜欢MVP的面向协议编程的思想!

    根据产品相应的需求,写出其次需求的接口,然后根据接口去找我们响应的发起者,和接受者!面向协议编程---面向接口编程---面向需求编程---需求驱动代码!

    MVP能够解决:

    代码思路清晰

    耦合度降低显著

    通讯还算比较简单

    缺点:

    我们需要写很多关于代理相关的代码

    视图和Presenter的交互会过于频繁

    如果Presenter过多地渲染了视图,往往会使得它与特定的视图的联系过于紧密。一旦视图需要变更,那么Presenter也需要变更了

    MVVM架构思想

    MVVM是Model-View-ViewModel的简写。它本质上就是MVC 的改进版。MVVM 就是将其中的View 的状态和行为抽象化,让我们将视图 UI 和业务逻辑分开。当然这些事 ViewModel 已经帮我们做了,它可以取出 Model 的数据同时帮忙处理 View 中由于需要展示内容而涉及的业务逻辑

    如果要说MVVM的特色,我觉得最大莫过于:双向绑定


    经常我们在设计我们的架构的时候,ViewModel层会设计响应的反向Block回调,方便我们的数据更新,只需要我们回调Block,那么在相应代码块绑定的视图中就能获取到最新的数据!

    这个时候我们要向完美实现正向传递,经常借助另一个非常牛逼的思想:响应式

    如果要想完美实现双向绑定,那么KVO我不太建议,推荐玩玩ReactiveCocoa这个框架---编程思想之集大成者!如果你们在MVVM架构设计中嵌入响应式,那就是双剑合璧.

    组件路由设计

    在众多架构中,在解耦性方面我觉得组件化开发无意做的真心不错,大家经常在各个控制器跳转,就会像蜘蛛网一样错综复杂。


    站在架构的层面就是把项目规矩化!条理化


    根据合适的边界把这个项目进行组件模块化出来,利用cocoaPods来管理!在整体组件分层下面的模型给大家进行参考学习!


    架构之路,无论在知识的深度还有广度方面都有较高的要求!尤其重要的对问题的的解决思维,不止在普通的应用层的ipa调用;需要大家对思维更加宽广,从代码上升到项目,到产品,甚至到公司!有时候你会很感觉很累很难,但是不将就注定不一样的你!

    摘自作者:Cooci_和谐学习_不急不躁
    原贴链接:https://www.jianshu.com/p/de6ebffdef86
    收起阅读 »

    Charles抓取iPhone接口数据

    抓取HTTP请求安装Charles,自行百度安装我安装的版本是4.2.6的设置代理:Proxy->ProxySetting手机设置,手机跟电脑接同一个局域网,配置HTTP代理抓取HTTPS请求抓取请求需要安装SSL证书,Help->SSL Prox...
    继续阅读 »

    抓取HTTP请求

    安装Charles,自行百度安装
    我安装的版本是4.2.6的

    设置代理:Proxy->ProxySetting


    手机设置,手机跟电脑接同一个局域网,配置HTTP代理


    抓取HTTPS请求

    抓取请求需要安装SSL证书,Help->SSL Proxying,安装证书,根据提示在手机上输入指定url安装CA证书。



    手机安装完后,默认是不信任的,需要手动信任以下该CA证书,打开设置,通用->关于本机->证书信任设置,打开开关信任即可


    证书配置完毕后,charles默认是没有抓取Https请求的,在需要抓取的Https url右击,选中Enable SSL Proxy即可。

    Charles视图简单讲解



    转自:https://www.jianshu.com/p/82096a460e56


    收起阅读 »

    iOS 利用UserDefaults快速实现常用搜索页记录工具

    1、需求分析存储内容为字符串存储内容要去重存储个数会有个上限存储个数达到上限后要先前挤掉旧数据,保留新数据调用动作一般为 存 / 读 / 清空全部2、实现.h文件// RPCustomTool.h// RollingPin//// Created by ...
    继续阅读 »

    1、需求分析

    • 存储内容为字符串
    • 存储内容要去重
    • 存储个数会有个上限
    • 存储个数达到上限后要先前挤掉旧数据,保留新数据
    • 调用动作一般为 存 / 读 / 清空全部

    2、实现

    .h文件

    //  RPCustomTool.h
    // RollingPin
    //
    // Created by RollingPin on 2020/12/31.
    // Copyright © 2020 RollingPin. All rights reserved.
    //
    #import
    #import
    @interface RPCustomTool : NSObject
    ///
    + (void)saveHistoryString:(NSString *)saveStr;
    /// 读
    + (NSArray *)readHistoryList;
    /// 清空
    + (void)deleteHistoryList;
    @end

    .m文件

    //  RPCustomTool.h
    // RollingPin
    //
    // Created by RollingPin on 2020/12/31.
    // Copyright © 2020 RollingPin. All rights reserved.
    //
    #import "RPCustomTool.h"
    @implementation RPCustomTool

    #pragma mark - 存
    + (void)saveHistoryString:(NSString *)saveStr
    {
    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
    NSArray *savedArray = [userDefaults arrayForKey:@"RPSearchHistoryMark"];
    NSMutableArray *savedMuArray = [[NSMutableArray alloc]initWithArray:savedArray];
    //去重
    NSString *repetitiveStr = @"";
    for (NSString * oneStr in savedArray) {
    if ([oneStr isEqualToString:saveStr]) {
    repetitiveStr = oneStr;
    break;
    }
    }
    if (repetitiveStr.length >0) {
    [savedMuArray removeObject:repetitiveStr];
    }
    [savedMuArray addObject:saveStr];
    //设置最大保存数
    if(savedMuArray.count > 10)
    {
    [savedMuArray removeObjectAtIndex:0];
    }
    //最后再存储到NSUserDefaults中
    [userDefaults setObject:savedMuArray forKey:@"RPSearchHistoryMark"];
    [userDefaults synchronize];
    }
    #pragma mark - 读
    + (NSArray *)readHistoryList
    {
    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
    //读取数组NSArray类型的数据
    NSArray *savedArray = [userDefaults arrayForKey:@"RPSearchHistoryMark"];
    NSLog(@"savedArray======%@",savedArray);
    return [savedArray copy];
    }
    #pragma mark - 清空
    + (void)deleteHistoryList
    {
    NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
    [userDefaults setObject:[NSArray array] forKey:@"RPSearchHistoryMark"];
    [userDefaults synchronize];
    }
    @end


    转自:https://www.jianshu.com/p/006bd3fbc044

    收起阅读 »

    UITableviewCell 使用Masonry撑开cell高度 遇见[LayoutConstraints] Unable to simultaneously satisfy constraints

    1、问题描述在布局UITableviewCell 内容时, 可用使用Masonry方便的自动计算高度撑开布局,但是当遇到cell高度不同,多个复杂的子view竖向排列时,容易产生高度计算冲突问题导致报如下一坨2、解决办法使用 Masonry 的 priorit...
    继续阅读 »

    1、问题描述

    在布局UITableviewCell 内容时, 可用使用Masonry方便的自动计算高度撑开布局,但是当遇到cell高度不同,多个复杂的子view竖向排列时,容易产生高度计算冲突问题导致报如下一坨


    2、解决办法

    使用 Masonry 的 priorityHigh 属性来确定优先级

    /**
    * Sets the NSLayoutConstraint priority to MASLayoutPriorityHigh
    */
    - (MASConstraint * (^)(void))priorityHigh;

    具体使用要设置 <最后一个子view> 的 bottom 属性 priorityHigh()

    [self.lastView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.top.equalTo(self.topView.mas_bottom).offset(5);
    make.left.equalTo(superView).offset(36);
    make.right.equalTo(superView).offset(-16);
    make.bottom.equalTo(self.contentView).offset(-16).priorityHigh();
    }];

    转自:https://www.jianshu.com/p/b334b69ab82e

    收起阅读 »

    【iOS】Keychain 钥匙串

    钥匙串,实际上是一个加密后的数据库,如下图所示。即使吧App删除,钥匙串里面的数据也不会丢失。数据都是以 Item 的形式来存储的,每个 Item 由一个加密后的 Data 数据,还有一系列用来描述该 Item 属性的 Attributes 组成。由于是数据库...
    继续阅读 »

    钥匙串,实际上是一个加密后的数据库,如下图所示。
    即使吧App删除,钥匙串里面的数据也不会丢失。


    数据都是以 Item 的形式来存储的,每个 Item 由一个加密后的 Data 数据,还有一系列用来描述该 Item 属性的 Attributes 组成。
    由于是数据库,关键方法只有四种,增删改查,对应的是

    SecItemAdd
    SecItemDelete
    SecItemUpdate
    SecItemCopyMatching
    下面简单讲述一下使用方法

    SecItemAdd

    CFTypeRef result;
    NSDictionary *query = @{
    // 一个典型的新增方法的参数,包含三个部分
    // 1.kSecClass key,它用来指定新增对象的类型
    (NSString *)kSecClass: (NSString *)kSecClassGenericPassword,
    // 2.若干项属性 key,例如 kSecAttrAccount,kSecAttrLabel 等,用来描述新增对象的属性
    (NSString *)kSecAttrAccount: @"uniqueID",
    // 3.kSecValueData key,用来设置新增对象保存的数据
    (NSString *)kSecValueData: [@"token" dataUsingEncoding:NSUTF8StringEncoding],
    // 可选
    // 如果需要获取新增的 Item 对象的属性,需要如下属性,
    (NSString *)kSecReturnData: (NSNumber *)kCFBooleanTrue,
    (NSString *)kSecReturnAttributes: (NSNumber *)kCFBooleanTrue,
    };
    OSStatus status = SecItemAdd((CFDictionaryRef)query, &result);
    if (result == errSecSuccess) {
    // 新增成功
    NSDictionary *itemInfo = (__bridge NSDictionary *)result;
    NSLog(@"info: %@", itemInfo);
    } else {
    // 其他错误
    }

    result类型判断方式


    SecItemDelete

    NSDictionary *query = @{
    // 一个典型的删除方法的参数,包含两个部分
    // 1、kSecClass key,它用来指定删除对象的类型,必填。
    (NSString *)kSecClass: (NSString *)kSecClassGenericPassword,
    // 2、若干项属性 key,可选。
    // 例如 kSecAttrAccount,kSecAttrLabel 等,用来设置删除对象的范围
    // 默认情况下,符合条件的全部 Item 都会被删除
    (NSString *)kSecAttrAccount: @"uniqueID",
    };
    OSStatus status = SecItemDelete((CFDictionaryRef)query);
    if (result == errSecSuccess) {
    // 删除成功
    } else {
    // 其他错误
    }

    SecItemUpdate

    // 1、找出需要更新属性的 Item
    // 参数格式与 SecItemCopyMatching 方法中的参数格式相同
    NSDictionary *query = @{
    (NSString *)kSecClass: (NSString *)kSecClassGenericPassword,
    (NSString *)kSecAttrAccount: @"uniqueID",
    };

    // 2、需要更新的属性
    // 若干项属性 key
    NSDictionary *update = @{
    (NSString *)kSecAttrAccount: @"another uniqueID",
    (NSString *)kSecValueData: @"another value",
    };

    OSStatus status = SecItemUpdate((CFDictionaryRef)query, (CFDictionaryRef)update);

    if (result == errSecSuccess) {
    // 更新成功
    } else {
    // 其他错误
    }

    SecItemDelete

    NSDictionary *query = @{
    // 一个典型的删除方法的参数,包含两个部分
    // 1、kSecClass key,它用来指定删除对象的类型,必填。
    (NSString *)kSecClass: (NSString *)kSecClassGenericPassword,
    // 2、若干项属性 key,可选。
    // 例如 kSecAttrAccount,kSecAttrLabel 等,用来设置删除对象的范围
    // 默认情况下,符合条件的全部 Item 都会被删除
    (NSString *)kSecAttrAccount: @"uniqueID",
    };
    OSStatus status = SecItemDelete((CFDictionaryRef)query);
    if (result == errSecSuccess) {
    // 删除成功
    } else {
    // 其他错误
    }

    SecItemCopyMatching

    CFTypeRef result;
    NSDictionary *query = @{
    // 一个典型的搜索方法的参数,包含三个部分
    // 1、kSecClass key(必填),它用来指定搜索对象的类型
    (NSString *)kSecClass: (NSString *)kSecClassGenericPassword,
    // 2、若干项属性 key(可选),例如 kSecAttrAccount,kSecAttrLabel 等,用来描述搜索对象的属性
    (NSString *)kSecAttrAccount: @"uniqueID",
    // 3、搜索属性(可选)
    // 例如 kSecMatchLimit(搜索一个还是多个,影响返回结果类型)
    // kSecMatchCaseInsensitive 是否大小写敏感等
    (NSString *)kSecMatchLimit: (NSString *)kSecMatchLimitAll,
    (NSString *) kSecMatchCaseInsensitive: (NSNumber *) kCFBooleanTrue,
    // (可选)如果需要获取新增的 Item 对象的属性,需要如下属性,
    (NSString *)kSecReturnData: (NSNumber *)kCFBooleanTrue,
    (NSString *)kSecReturnAttributes: (NSNumber *)kCFBooleanTrue,
    };
    OSStatus status = SecItemCopyMatching((CFDictionaryRef)query, &result);
    if (result == errSecSuccess) {
    // 新增成功
    NSDictionary *itemInfo = (__bridge NSDictionary *)result;
    NSLog(@"info: %@", itemInfo);
    } else {
    // 其他错误
    }

    result类型判断方式


    链接:https://www.jianshu.com/p/8f8db1ff024d


    收起阅读 »

    iOS 网页和原生列表混合布局开发(文章+评论)

    我们总会遇见特别不适合使用原生开发的页面,比如一个文章详情页,上面是文章下面是评论,就比如现在用的简书的手机版这样,那么这种需求应该怎么做呢?最好的方法当然是整个页面都是用H5开发,哈哈哈;当然下面评论有时候会有很多交互导致得用原生控件开发,那这里就面临着严峻...
    继续阅读 »

    我们总会遇见特别不适合使用原生开发的页面,比如一个文章详情页,上面是文章下面是评论,就比如现在用的简书的手机版这样,那么这种需求应该怎么做呢?
    最好的方法当然是整个页面都是用H5开发,哈哈哈;当然下面评论有时候会有很多交互导致得用原生控件开发,那这里就面临着严峻的问题了,上面是网页可以滑动,下面是评论最好是用列表做,具体怎么组合起来就值得我们说道说道了,当然方法有很多种,我这里讲解一种我觉得各方面都不错的。

    ps:问题总结起来还是两个滑动视图上下滑动问题所以用我之前讲解的多个滑动视图冲突解决https://www.jianshu.com/p/cfe517ce437b 也可以解决不过这样使用H5那面配合的地方比较多。这个不多说,下面介绍我们今天要说的。

    这个方案的整体思路:把web和table同时加在一个底层ScrollView上面,滑动底层ScrollView同时不断控制web和table的偏移量位置,使页面看起来是两个滑动视图连在一起的。
    整体结构如图


    一、视图介绍

    黄色的是底层ScrollView,青色的一个加在底层ScrollView上的view(这里我们叫它contentView),然后正加载简书网页的是web,红色部分是table。web和table再加contentView上,这样我们控制整体位置的时候使用contentView就行;

    二、视图之间的高度关系:

    web和table的最大高度都是底层ScrollView的高度,这样做可以正好让其中一个充满整个底层ScrollView。
    contentView的高度是web和table高度的和(毕竟就是为了放他们两)。
    底层ScrollView的可滑动高度这里设定成web和table可滑动高度的总和,方便滑动处理。
    ps:具体代码在后面。

    三、滑动处理思路

    滑动都靠底层ScrollView,禁用web和table的滑动,上面说了底层ScrollView的可滑动高度是web和table的总和所以进度条是正常的。
    然后在滑动的同时不断调整contentView的位置,web和table的偏移量,使页面效果看起来符合预期。

    四、滑动处理具体操作,整个滑动可以分成五阶段。ps:offsety 底层ScrollView的偏移量
    1.offsety<=0,不用过多操作正常滑动
    2.web内部可以滑动。控制contentView悬浮,使web在屏幕可视区域。同时修改web的偏移量。
    3.web滑动到头。保持contentView的位置和web的偏移量,使table滑动到屏幕可视区域
    4.table内部可以滑动。控制contentView悬浮,使table在屏幕可视区域。同时修改table的偏移量。
    5.table滑动到头。保持contentView的位置和table的偏移量,使页面滑动到底部
    五、具体代码
    1.因为web和table都是随内容变高的,这里选择通过监听两者高度变化,同时刷新各个控件的高度,对应第二步骤

    //添加监听
    [self.webView addObserver:self forKeyPath:@"scrollView.contentSize" options:NSKeyValueObservingOptionNew context:nil];
    [self.collectionView addObserver:self forKeyPath:@"contentSize" options:NSKeyValueObservingOptionNew context:nil];
    //刷新各个控件高度
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
    if (object == _webView) {
    if ([keyPath isEqualToString:@"scrollView.contentSize"]) {
    [self updateContainerScrollViewHeight];
    }
    }else if(object == _collectionView) {
    if ([keyPath isEqualToString:@"contentSize"]) {
    [self updateContainerScrollViewHeight];
    }
    }
    }

    - (void)updateContainerScrollViewHeight{
    CGFloat webViewContentHeight = self.webView.scrollView.contentSize.height;
    CGFloat collectionContentHeight = self.collectionView.contentSize.height;

    if (webViewContentHeight == _lastWebViewContentHeight && collectionContentHeight == _lastCollectionContentHeight) {
    return;
    }

    _lastWebViewContentHeight = webViewContentHeight;
    _lastCollectionContentHeight = collectionContentHeight;

    self.containerScrollView.contentSize = CGSizeMake(self.view.width, webViewContentHeight + collectionContentHeight);

    CGFloat webViewHeight = (webViewContentHeight < _contentHeight) ?webViewContentHeight :_contentHeight;
    CGFloat collectionHeight = collectionContentHeight < _contentHeight ?collectionContentHeight :_contentHeight;
    self.webView.height = webViewHeight <= 0.1 ?0.1 :webViewHeight;
    self.contentView.height = webViewHeight + collectionHeight;
    self.collectionView.height = collectionHeight;
    self.collectionView.top = self.webView.bottom;

    [self scrollViewDidScroll:self.containerScrollView];
    }

    2.具体滑动处理代码:

    - (void)scrollViewDidScroll:(UIScrollView *)scrollView{
    if (_containerScrollView != scrollView) {
    return;
    }

    CGFloat offsetY = scrollView.contentOffset.y;

    CGFloat webViewHeight = self.webView.height;
    CGFloat collectionHeight = self.collectionView.height;

    CGFloat webViewContentHeight = self.webView.scrollView.contentSize.height;
    CGFloat collectionContentHeight = self.collectionView.contentSize.height;
    if (offsetY <= 0) {
    self.contentView.top = 0;
    self.webView.scrollView.contentOffset = CGPointZero;
    self.collectionView.contentOffset = CGPointZero;
    }else if(offsetY < webViewContentHeight - webViewHeight){
    self.contentView.top = offsetY;
    self.webView.scrollView.contentOffset = CGPointMake(0, offsetY);
    self.collectionView.contentOffset = CGPointZero;
    }else if(offsetY < webViewContentHeight){
    self.contentView.top = webViewContentHeight - webViewHeight;
    self.webView.scrollView.contentOffset = CGPointMake(0, webViewContentHeight - webViewHeight);
    self.collectionView.contentOffset = CGPointZero;
    }else if(offsetY < webViewContentHeight + collectionContentHeight - collectionHeight){
    self.contentView.top = offsetY - webViewHeight;
    self.collectionView.contentOffset = CGPointMake(0, offsetY - webViewContentHeight);
    self.webView.scrollView.contentOffset = CGPointMake(0, webViewContentHeight - webViewHeight);
    }else if(offsetY <= webViewContentHeight + collectionContentHeight ){
    self.contentView.top = self.containerScrollView.contentSize.height - self.contentView.height;
    self.webView.scrollView.contentOffset = CGPointMake(0, webViewContentHeight - webViewHeight);
    self.collectionView.contentOffset = CGPointMake(0, collectionContentHeight - collectionHeight);
    }else {
    //do nothing
    NSLog(@"do nothing");
    }
    }


    链接:https://www.jianshu.com/p/ca7f826fd39b

    收起阅读 »

    iOS你需要知道的事--Crash分析

    Crash ,,CrashlyticsHockeyapp友盟Bugly 等等但是,所有的但是,这不够!因为我们不再是一个简单会用的iOS开发人员,必将走向底层,了解原理,掌握装逼内容和技巧是我们的必修课首先我们来了解一下Crash的底层原理...
    继续阅读 »
    大家平时在开发过程中,经常会遇到Crash,那也是在正常不过的事,但是作为一个优秀的iOS开发人员,必将这些用户不良体验降到最低。

    线下Crash,我们直接可以调试,结合stack信息,不难定位!
    线上Crash当然也有一些信息,毕竟苹果爸爸的产品还是做得非常不错的!


    通过iPhone的Crash log也可以分析一些,但是这个是需要用户配合的,因为需要用户在手机 中 设置-> 诊断与用量->勾选 自动发送 ,然后在xcode中 Window->Organizer->Crashes 对应的app,就是当前app最新一版本的crash log ,并且是解析过的,可以根据crash 栈 等相关信息 ,尤其是程序代码级别的 有超链接,一键可以直接跳转到程序崩溃的相关代码,这样更容易定位bug出处.

    为了能够第一时间发现程序问题,应用程序需要实现自己的崩溃日志收集服务,成熟的开源项目很多,如  KSCrashplcrashreporterCrashKit 等。追求方便省心,对于保密性要求不高的程序来说,也可以选择各种一条龙Crash统计产品,如 CrashlyticsHockeyapp ,友盟Bugly 等等

    但是,所有的但是,这不够!因为我们不再是一个简单会用的iOS开发人员,必将走向底层,了解原理,掌握装逼内容和技巧是我们的必修课

    首先我们来了解一下Crash的底层原理

    iOS系统自带的 Apple’s Crash Reporter记录在设备中的Crash日志,Exception Type项通常会包含两个元素:Mach异常和 Unix信号。

    Exception Type:         EXC_BAD_ACCESS (SIGSEGV)    
    Exception Subtype: KERN_INVALID_ADDRESS at 0x041a6f3

    Mach异常是什么?它又是如何与Unix信号建立联系的?

    Mach是一个XNU的微内核核心,Mach异常是指最底层的内核级异常,被定义在下 。每个thread,task,host都有一个异常端口数组,Mach的部分API暴露给了用户态,用户态的开发者可以直接通过Mach API设置thread,task,host的异常端口,来捕获Mach异常,抓取Crash事件。

    所有Mach异常都在host层被ux_exception转换为相应的Unix信号,并通过threadsignal将信号投递到出错的线程。iOS中的 POSIX API就是通过Mach之上的 BSD层实现的。


    因此,EXC_BAD_ACCESS (SIGSEGV)表示的意思是:Mach层的EXC_BAD_ACCESS异常,在host层被转换成SIGSEGV信号投递到出错的线程。

    iOS的异常Crash
    * KVO问题
    * NSNotification线程问题
    * 数组越界
    * 野指针
    * 后台任务超时
    * 内存爆出
    * 主线程卡顿超阀值
    * 死锁
    ....

    下面我就拿出最常见的两种Crash分析一下



    Crash分析处理

    上面我们也知道:既然最终以信号的方式投递到出错的线程,那么就可以通过注册相应函数来捕获信号.达到Hook的效果

    + (void)installUncaughtSignalExceptionHandler{
    NSSetUncaughtExceptionHandler(&LGExceptionHandlers);
    signal(SIGABRT, LGSignalHandler);
    }

    我们从上面的函数可以Hook到信息,下面我们开始进行包装处理.这里还是面向统一封装,因为等会我们还需要考虑Signal

    void LGExceptionHandlers(NSException *exception) {
    NSLog(@"%s",__func__);

    NSArray *callStack = [LGUncaughtExceptionHandle lg_backtrace];
    NSMutableDictionary *mDict = [NSMutableDictionary dictionaryWithDictionary:exception.userInfo];
    [mDict setObject:callStack forKey:LGUncaughtExceptionHandlerAddressesKey];
    [mDict setObject:exception.callStackSymbols forKey:LGUncaughtExceptionHandlerCallStackSymbolsKey];
    [mDict setObject:@"LGException" forKey:LGUncaughtExceptionHandlerFileKey];

    // exception - myException

    [[[LGUncaughtExceptionHandle alloc] init] performSelectorOnMainThread:@selector(lg_handleException:) withObject:[NSException exceptionWithName:[exception name] reason:[exception reason] userInfo:mDict] waitUntilDone:YES];
    }

    下面针对封装好的myException进行处理,在这里要做两件事

    1.存储,上传:方便开发人员检查修复

    2.处理Crash奔溃,我们也不能眼睁睁看着BUG闪退在用户的手机上面,希望“起死回生,回光返照”

    - (void)lg_handleException:(NSException *)exception{
    // crash 处理
    // 存
    NSDictionary *userInfo = [exception userInfo];
    [self saveCrash:exception file:[userInfo objectForKey:LGUncaughtExceptionHandlerFileKey]];
    }

    下面是一些封装的一些辅助函数

    保存奔溃信息或者上传:针对封装数据本地存储,和相应上传服务器

    - (void)saveCrash:(NSException *)exception file:(NSString *)file{

    NSArray *stackArray = [[exception userInfo] objectForKey:LGUncaughtExceptionHandlerCallStackSymbolsKey];// 异常的堆栈信息
    NSString *reason = [exception reason];// 出现异常的原因
    NSString *name = [exception name];// 异常名称

    // 或者直接用代码,输入这个崩溃信息,以便在console中进一步分析错误原因
    // NSLog(@"crash: %@", exception);

    NSString * _libPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:file];

    if (![[NSFileManager defaultManager] fileExistsAtPath:_libPath]){
    [[NSFileManager defaultManager] createDirectoryAtPath:_libPath withIntermediateDirectories:YES attributes:nil error:nil];
    }

    NSDate *dat = [NSDate dateWithTimeIntervalSinceNow:0];
    NSTimeInterval a=[dat timeIntervalSince1970];
    NSString *timeString = [NSString stringWithFormat:@"%f", a];

    NSString * savePath = [_libPath stringByAppendingFormat:@"/error%@.log",timeString];

    NSString *exceptionInfo = [NSString stringWithFormat:@"Exception reason:%@\nException name:%@\nException stack:%@",name, reason, stackArray];

    BOOL sucess = [exceptionInfo writeToFile:savePath atomically:YES encoding:NSUTF8StringEncoding error:nil];

    NSLog(@"保存崩溃日志 sucess:%d,%@",sucess,savePath);
    }

    获取函数堆栈信息,这里可以获取响应调用堆栈的符号信息,通过数组回传

    + (NSArray *)lg_backtrace{

    void* callstack[128];
    int frames = backtrace(callstack, 128);//用于获取当前线程的函数调用堆栈,返回实际获取的指针个数
    char **strs = backtrace_symbols(callstack, frames);//从backtrace函数获取的信息转化为一个字符串数组
    int i;
    NSMutableArray *backtrace = [NSMutableArray arrayWithCapacity:frames];
    for (i = LGUncaughtExceptionHandlerSkipAddressCount;
    i < LGUncaughtExceptionHandlerSkipAddressCount+LGUncaughtExceptionHandlerReportAddressCount;
    i++)
    {
    [backtrace addObject:[NSString stringWithUTF8String:strs[i]]];
    }
    free(strs);
    return backtrace;
    }

    获取应用信息,这个函数提供给Siganl数据封装

    NSString *getAppInfo(){
    NSString *appInfo = [NSString stringWithFormat:@"App : %@ %@(%@)\nDevice : %@\nOS Version : %@ %@\n",
    [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"],
    [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"],
    [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"],
    [UIDevice currentDevice].model,
    [UIDevice currentDevice].systemName,
    [UIDevice currentDevice].systemVersion];
    // [UIDevice currentDevice].uniqueIdentifier];
    NSLog(@"Crash!!!! %@", appInfo);
    return appInfo;
    }

    做完这些准备,你可以非常清晰的看到程序奔溃,哈哈哈!(好像以前奔溃还不清晰似的),这里说一下:我的意思你非常清晰的知道奔溃之前做了一些什么!
    下面是检测我们奔溃之前的沙盒存储的信息:error.log


    下面我们来一个骚操作:在监听的信息的时候来了一个Runloop,我们监听所有的mode,开启循环

    (一个相对于我们应用程序自启的Runloop的平行空间).

    SCLAlertView *alert = [[SCLAlertView alloc] initWithNewWindowWidth:300.0f];
    [alert addButton:@"奔溃" actionBlock:^{
    self.dismissed = YES;
    }];
    [alert showSuccess:exception.name subTitle:exception.reason closeButtonTitle:nil duration:0];
    // 本次异常处理
    CFRunLoopRef runloop = CFRunLoopGetCurrent();
    CFArrayRef allMode = CFRunLoopCopyAllModes(runloop);
    while (!self.dismissed) {
    // machO
    // 后台更新 - log
    // kill
    //
    for (NSString *mode in (__bridge NSArray *)allMode) {
    CFRunLoopRunInMode((CFStringRef)mode, 0.0001, false);
    }
    }

    CFRelease(allMode);

    在这个平行空间我们开启一个弹框,这个弹框,跟着我们的应用程序保活,并且具备相应的响应能力,到目前为止:此时此刻还有谁!这不就是回光返照?只要我们的条件成立,那么在相应的这个平行空间继续做一些我们的工作,程序不死:what is dead may never die,but rises again harder and stronger


    signal 函数拦截不到的解决方式

    在debug模式下,如果你触发了崩溃,那么应用会直接崩溃到主函数,断点都没用,此时没有任何log信息显示出来,如果你想看log信息的话,你需要在lldb中,拿SIGABRT来说吧,敲入pro hand -p true -s false SIGABRT命令,不然你啥也看不到。


    然后断开断点,程序进入监听,下面剩下的操作就是包装异常,操作类似Exception


    最后我们需要注意的针对我们的监听回收相应内存:

    NSSetUncaughtExceptionHandler(NULL);
    signal(SIGABRT, SIG_DFL);
    signal(SIGILL, SIG_DFL);
    signal(SIGSEGV, SIG_DFL);
    signal(SIGFPE, SIG_DFL);
    signal(SIGBUS, SIG_DFL);
    signal(SIGPIPE, SIG_DFL);

    if ([[exception name] isEqual:UncaughtExceptionHandlerSignalExceptionName])
    {
    kill(getpid(), [[[exception userInfo] objectForKey:UncaughtExceptionHandlerSignalKey] intValue]);
    }
    else
    {
    [exception raise];
    }

    到目前为止,我们响应的Crash处理已经入门,如果你还想继续探索也是有很多地方比如:

    我们能否hook系统奔溃,异常的方法NSSetUncaughtExceptionHandler,已达到拒绝传递 UncaughtExceptionHandler的效果

    我们在处理异常的时候,利用Runloop回光返照,有没有更加合适的方法

    Runloop回光返照我们怎么继续保证应用程序稳定执行


    摘自作者:Cooci_和谐学习_不急不躁
    原贴链接:https://www.jianshu.com/p/56f96167a6e9

    收起阅读 »

    iOS-UIView常用的setNeedsDisplay和setNeedsLayout

    UIView的setNeedsDisplay和setNeedsLayout方法      首先两个方法都是异步执行的。而setNeedsDisplay会调用自动调用drawRect方法,这样可以拿到 UIGraphics...
    继续阅读 »
    • UIView的setNeedsDisplay和setNeedsLayout方法
          首先两个方法都是异步执行的。而setNeedsDisplay会调用自动调用drawRect方法,这样可以拿到 UIGraphicsGetCurrentContext,就可以画画了。而setNeedsLayout会默认调用layoutSubViews,就可以 处理子视图中的一些数据。综上所诉,setNeedsDisplay方便绘图,而    layoutSubViews方便出来数据。
    • layoutSubviews在以下情况下会被调用:

    1、init初始化不会触发layoutSubviews。
    2、addSubview会触发layoutSubviews。
    3、设置view的Frame会触发layoutSubviews,当然前提是frame的值设置前后发生了变化。
    4、滚动一个UIScrollView会触发layoutSubviews。
    5、旋转Screen会触发父UIView上的layoutSubviews事件。
    6、改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件。
    7、直接调用setLayoutSubviews。
    • drawRect在以下情况下会被调用:

    1、如果在UIView初始化时没有设置rect大小,将直接导致drawRect不被自动调用。drawRect调用是在Controller->loadView, Controller->viewDidLoad 两方法之后掉用的.所以不用担心在控制器中,这些View的drawRect就开始画了.这样可以在控制器中设置一些值给View(如果这些View draw的时候需要用到某些变量值).
    2、该方法在调用sizeToFit后被调用,所以可以先调用sizeToFit计算出size。然后系统自动调用drawRect:方法。
    3、通过设置contentMode属性值为UIViewContentModeRedraw。那么将在每次设置或更改frame的时候自动调用drawRect:。
    4、直接调用setNeedsDisplay,或者setNeedsDisplayInRect:触发drawRect:,但是有个前提条件是rect不能为0。
    以上1,2推荐;而3,4不提倡
    • drawRect方法使用注意点:

    1、若使用UIView绘图,只能在drawRect:方法中获取相应的contextRef并绘图。如果在其他方法中获取将获取到一个invalidate的ref并且不能用于画图。drawRect:方法不能手动显示调用,必须通过调用setNeedsDisplay 或者 setNeedsDisplayInRect,让系统自动调该方法。
    2、若使用CAlayer绘图,只能在drawInContext: 中(类似于drawRect)绘制,或者在delegate中的相应方法绘制。同样也是调用setNeedDisplay等间接调用以上方法
    3、若要实时画图,不能使用gestureRecognizer,只能使用touchbegan等方法来掉用setNeedsDisplay实时刷新屏幕

    链接:https://www.jianshu.com/p/33a28bb14749

    收起阅读 »

    iOS Crash分析中的Signal

    下面是一些信号说明1.SIGHUP本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联。登录Linux时,系统会分配给登录用户一个终端(Session)。在这个终端运...
    继续阅读 »

    下面是一些信号说明

    1.SIGHUP

    本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联。
    登录Linux时,系统会分配给登录用户一个终端(Session)。在这个终端运行的所有程序,包括前台进程组和后台进程组,一般都属于这个 Session。当用户退出Linux登录时,前台进程组和后台有对终端输出的进程将会收到SIGHUP信号。这个信号的默认操作为终止进程,因此前台进 程组和后台有终端输出的进程就会中止。不过可以捕获这个信号,比如wget能捕获SIGHUP信号,并忽略它,这样就算退出了Linux登录, wget也 能继续下载。
    此外,对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。

    2.SIGINT

    程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。

    3.SIGQUIT

    和SIGINT类似, 但由QUIT字符(通常是Ctrl-)来控制. 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。

    4.SIGILL

    执行了非法指令. 通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出时也有可能产生这个信号。

    5.SIGTRAP

    由断点指令或其它trap指令产生. 由debugger使用。

    6.SIGABRT

    调用abort函数生成的信号。

    7.SIGBUS

    非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数, 但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。

    8.SIGFPE

    在发生致命的算术运算错误时发出. 不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误。

    9.SIGKILL

    用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号。

    10.SIGUSR1

    留给用户使用

    11.SIGSEGV

    试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据.

    12.SIGUSR2

    留给用户使用

    13.SIGPIPE

    管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止。

    14.SIGALRM

    时钟定时信号, 计算的是实际的时间或时钟时间. alarm函数使用该信号.

    15.SIGTERM

    程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号。如果进程终止不了,我们才会尝试SIGKILL。

    16.SIGCHLD

    子进程结束时, 父进程会收到这个信号。
    如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这时的子进程称为僵尸进程。这种情 况我们应该避免(父进程或者忽略SIGCHILD信号,或者捕捉它,或者wait它派生的子进程,或者父进程先终止,这时子进程的终止自动由init进程 来接管)。

    17.SIGCONT

    让一个停止(stopped)的进程继续执行. 本信号不能被阻塞. 可以用一个handler来让程序在由stopped状态变为继续执行时完成特定的工作. 例如, 重新显示提示符

    18.SIGSTOP

    停止(stopped)进程的执行. 注意它和terminate以及interrupt的区别:该进程还未结束, 只是暂停执行. 本信号不能被阻塞, 处理或忽略.

    19.SIGTSTP

    停止进程的运行, 但该信号可以被处理和忽略. 用户键入SUSP字符时(通常是Ctrl-Z)发出这个信号

    20.SIGTTIN

    当后台作业要从用户终端读数据时, 该作业中的所有进程会收到SIGTTIN信号. 缺省时这些进程会停止执行.

    21.SIGTTOU

    类似于SIGTTIN, 但在写终端(或修改终端模式)时收到.

    22.SIGURG

    有”紧急”数据或out-of-band数据到达socket时产生.

    23.SIGXCPU

    超过CPU时间资源限制. 这个限制可以由getrlimit/setrlimit来读取/改变。

    24.SIGXFSZ

    当进程企图扩大文件以至于超过文件大小资源限制。

    25.SIGVTALRM

    虚拟时钟信号. 类似于SIGALRM, 但是计算的是该进程占用的CPU时间.

    26.SIGPROF

    类似于SIGALRM/SIGVTALRM, 但包括该进程用的CPU时间以及系统调用的时间.

    27.SIGWINCH

    窗口大小改变时发出.

    28.SIGIO

    文件描述符准备就绪, 可以开始进行输入/输出操作.

    SIGPWR
    Power failure
    SIGSYS

    非法的系统调用。


    关键点注意

    在以上列出的信号中,程序不可捕获、阻塞或忽略的信号有:

    SIGKILL,SIGSTOP

    不能恢复至默认动作的信号有:

    SIGILL,SIGTRAP

    默认会导致进程流产的信号有:

    SIGABRT,SIGBUS,SIGFPE,SIGILL,SIGIOT,SIGQUIT,SIGSEGV,SIGTRAP,SIGXCPU,SIGXFSZ

    默认会导致进程退出的信号有:

    SIGALRM,SIGHUP,SIGINT,SIGKILL,SIGPIPE,SIGPOLL,SIGPROF,SIGSYS,SIGTERM,SIGUSR1,SIGUSR2,SIGVTALRM

    默认会导致进程停止的信号有:

    SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU

    默认进程忽略的信号有:

    SIGCHLD,SIGPWR,SIGURG,SIGWINCH

    此外,SIGIO在SVR4是退出,在4.3BSD中是忽略;SIGCONT在进程挂起时是继续,否则是忽略,不能被阻塞。


    摘自作者:Cooci_和谐学习_不急不躁

    原贴链接:https://www.jianshu.com/p/3a9dc6bd5e58

    收起阅读 »

    iOS——SDWebImage加载WebP图片

    1.确定第三方库首先直接去SDWebImage的仓库,里面直接就有关于WebP的仓库地址也就是SDWebImageWebPCoder,直接pod 'SDWebImageWebPCoder'就行。(如果项目里没有SDWebImage,需要pod 'SDWebIm...
    继续阅读 »

    1.确定第三方库

    首先直接去SDWebImage的仓库,里面直接就有关于WebP的仓库地址



    也就是SDWebImageWebPCoder,直接pod 'SDWebImageWebPCoder'就行。(如果项目里没有SDWebImage,需要pod 'SDWebImage')

    这里要注意!!!是pod 'SDWebImageWebPCoder'

    我搜索SDWebImage加载WebP,权重高的答案都是pod 'SDWebImage/WebP',但是这个仓库我在SDWebImage的repositories里搜索不到,也就是说没有这个仓库,结果如图。


    猜测可能之前的旧仓库是这个名字,那些文章也一直没更新,但是权重又高,不免误人子弟了一番。

    2.导入SDWebImageWebPCoder

    大概率会在pod install时报错,因为libwebp这个仓库的地址连接不上。

    1、在终端输入pod repo 查看 cocoapods 在本机的PATH,每个人的路径都可能不一样


    2、复制trunk的path,command + shift + G 输入上一步的地址,依次点击Specs-->1-->9-->2-->libwebp。(这里要注意有可能你的路径是cocoapods的path)

    3、选择报错的版本打开,将source下git地址更改为

    https://github.com/webmproject/libwebp.git


    4、pod install(如果还报一样的错,那么是第2步出了问题,去另一个路径改source-git的地址即可)

    3.使用SDWebImageWebPCoder

    SDImageWebPCoder *webPCoder = [SDImageWebPCoder sharedCoder];
    [[SDImageCodersManager sharedManager] addCoder:webPCoder];

    NSData *webpData;
    UIImage *wimage = [[SDImageWebPCoder sharedCoder] decodedImageWithData:webpData options:nil];
    NSData *webpData;
    [UIImage sd_imageWithWebPData:webpData];

    经测试以上两种写法都能成功加载webp图片

    转自:https://www.jianshu.com/p/74fab9c7de77

    收起阅读 »

    iOS dispatch_semaphore信号量的使用(for循环请求网络时,使用信号量导致死锁)

    有的时候我们会遇到这样的需求:循环请求网络,但是在循环的过程中,必须上一个网络回调完成后才能请求下一个网络即进行下一个循环,也就是所谓的多个异步网络做同步请求,首先想到的就是用信号量拦截,但是发现AFNetWorking配合信号量使用时,网络不回调了,是什么原...
    继续阅读 »

    有的时候我们会遇到这样的需求:
    循环请求网络,但是在循环的过程中,必须上一个网络回调完成后才能请求下一个网络即进行下一个循环,也就是所谓的多个异步网络做同步请求,首先想到的就是用信号量拦截,但是发现AFNetWorking配合信号量使用时,网络不回调了,是什么原因引起的网络无法回调。下面我们模拟下正常使用过程并分析,如下:

    -(void)semaphoreTest{

    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

    for (int i = 0; i<10; i++) {
    [self semaphoreTestBlock:^(NSString *TNT) {
    NSLog(@"任务完成 %d",i);
    dispatch_semaphore_signal(semaphore);
    }];

    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"信号量限制 %d",i);
    }
    }

    //这里用延迟模拟异步网络请求
    -(void)semaphoreTestBlock:(void(^)(NSString * TNT))block{
    /*
    queue 的类型无论是串行队列还是并行队列并不影响最终结果
    如果 queue = dispatch_get_main_queue() 将会堵塞组线程,造成死锁
    */
    dispatch_queue_t queue = dispatch_queue_create("myqueue",DISPATCH_QUEUE_SERIAL);

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), queue, ^{
    block(@"完成");
    });
    }

    这段代码的输出结果为:

    2019-10-11 14:40:23.961328+0800 LJC[9013:1358198] 任务完成 0
    2019-10-11 14:40:23.961751+0800 LJC[9013:1356826] 信号量限制 0
    2019-10-11 14:40:25.061312+0800 LJC[9013:1358198] 任务完成 1
    2019-10-11 14:40:25.061673+0800 LJC[9013:1356826] 信号量限制 1
    2019-10-11 14:40:26.062082+0800 LJC[9013:1356931] 任务完成 2
    2019-10-11 14:40:26.062381+0800 LJC[9013:1356826] 信号量限制 2
    2019-10-11 14:40:27.062883+0800 LJC[9013:1356931] 任务完成 3
    2019-10-11 14:40:27.063275+0800 LJC[9013:1356826] 信号量限制 3
    2019-10-11 14:40:28.160535+0800 LJC[9013:1356931] 任务完成 4
    2019-10-11 14:40:28.160988+0800 LJC[9013:1356826] 信号量限制 4
    2019-10-11 14:40:29.161327+0800 LJC[9013:1356931] 任务完成 5
    2019-10-11 14:40:29.161512+0800 LJC[9013:1356826] 信号量限制 5
    2019-10-11 14:40:30.161756+0800 LJC[9013:1356931] 任务完成 6
    2019-10-11 14:40:30.161989+0800 LJC[9013:1356826] 信号量限制 6
    2019-10-11 14:40:31.261507+0800 LJC[9013:1356931] 任务完成 7
    2019-10-11 14:40:31.261912+0800 LJC[9013:1356826] 信号量限制 7
    2019-10-11 14:40:32.361503+0800 LJC[9013:1356931] 任务完成 8
    2019-10-11 14:40:32.361870+0800 LJC[9013:1356826] 信号量限制 8
    2019-10-11 14:40:33.461544+0800 LJC[9013:1358198] 任务完成 9
    2019-10-11 14:40:33.461953+0800 LJC[9013:1356826] 信号量限制 9

    如果我们把
    dispatch_queue_t queue = dispatch_queue_create("myqueue",DISPATCH_QUEUE_SERIAL);
    替换成
    dispatch_queue_t queue = dispatch_get_main_queue()
    发现输出结果为空

    为什么呢?
    首先我们要知道
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    他怎么才能实现锁的功能,他的锁其实是针对线程的,我们当前任务是在主线程执行的,我们就需要在主线程上锁。
    完成任务我们去将信号量+1,即执行
    dispatch_semaphore_signal(semaphore)
    这个时候发现你的回调也是在主线程触发的,但是此时主线程上锁,已经卡住了,是不能让你在主线程做任务的,这就形成了相互等待,卡死了,所以我们需要将回调任务放在非主线程中(以目前这个例子来说,就是非主线程,其实我们最终调整的目的是让执行任务和回调任务不在同一线程即可)。

    那我们如果将任务(for循环)在子线程中执行,回调在主线程中是否可以呢?下面我们修改代码

    -(void)semaphoreTest{

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

    for (int i = 0; i<10; i++) {
    [self semaphoreTestBlock:^(NSString *TNT) {
    NSLog(@"任务完成 %d",i);
    dispatch_semaphore_signal(semaphore);
    }];

    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
    NSLog(@"信号量限制 %d",i);
    }
    });
    }

    -(void)semaphoreTestBlock:(void(^)(NSString * TNT))block{

    // dispatch_queue_t queue = dispatch_queue_create("myqueue",DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), queue, ^{
    block(@"完成");
    });
    }

    输出结果:

    2019-10-11 14:51:00.224109+0800 LJC[9063:1362953] 任务完成 0
    2019-10-11 14:51:00.224486+0800 LJC[9063:1363099] 信号量限制 0
    2019-10-11 14:51:01.325117+0800 LJC[9063:1362953] 任务完成 1
    2019-10-11 14:51:01.325493+0800 LJC[9063:1363099] 信号量限制 1
    2019-10-11 14:51:02.425129+0800 LJC[9063:1362953] 任务完成 2
    2019-10-11 14:51:02.425491+0800 LJC[9063:1363099] 信号量限制 2
    2019-10-11 14:51:03.524266+0800 LJC[9063:1362953] 任务完成 3
    2019-10-11 14:51:03.524715+0800 LJC[9063:1363099] 信号量限制 3
    2019-10-11 14:51:04.625254+0800 LJC[9063:1362953] 任务完成 4
    2019-10-11 14:51:04.625659+0800 LJC[9063:1363099] 信号量限制 4
    2019-10-11 14:51:05.725228+0800 LJC[9063:1362953] 任务完成 5
    2019-10-11 14:51:05.725573+0800 LJC[9063:1363099] 信号量限制 5
    2019-10-11 14:51:06.726094+0800 LJC[9063:1362953] 任务完成 6
    2019-10-11 14:51:06.726442+0800 LJC[9063:1363099] 信号量限制 6
    2019-10-11 14:51:07.825270+0800 LJC[9063:1362953] 任务完成 7
    2019-10-11 14:51:07.825613+0800 LJC[9063:1363099] 信号量限制 7
    2019-10-11 14:51:08.925323+0800 LJC[9063:1362953] 任务完成 8
    2019-10-11 14:51:08.925674+0800 LJC[9063:1363099] 信号量限制 8
    2019-10-11 14:51:10.025359+0800 LJC[9063:1362953] 任务完成 9
    2019-10-11 14:51:10.025722+0800 LJC[9063:1363099] 信号量限制 9

    这就验证了我们的想法, 执行任务和任务回调是不能在一个线程中的

    整理

    在使用信号量的时候,需要注意 dispatch_semaphore_wait 需要和 任务 放在同一线程,在任务执行异步回调的时候,需要将回调放在与执行任务不同的线程中,因为如果在同一线程中 dispatch_semaphore_wait 操作会造成相互等待导致死锁问题,我们在使用 AFNetWorking 的时候,他默认的回调是在 主线程中,所以我们在配合 AFNetWorking 使用信号量的时候可以指定 AFNetWorking 的回调线程,或者我们在执行任务的时候,将任务放在其他线程

    注释:
    写这篇文章是因为我在用信号量配合AFNetWorking做网路任务的时候发现一只卡死,在网上找的都说指定AFNetWorking 的 completionQueue ,然后我更改了代码,request是我们网络对AFNetWorking的封装对象实例,按理来说是没问题的,但是不知道为什么还是会造成死锁。目前原因没找到。所以我将for循环再放了子线程中

    request.sessionManager.completionQueue = dispatch_get_global_queue(0, 0);

    如发现理解错误,望指出 ^_^ THANKS

    转自:https://www.jianshu.com/p/91e9e38e3f51

    收起阅读 »

    iOS 登录接口封装实践

    登录。。。基本所有APP都少不了,开始写APP,可能首先就是从登录开始我也一样,我手上有一个封装了所有账户体系相关接口的SDK,运行良好但也遇到一些烦心事,就拿登录来说说吧。首先有如下相关封装,很常见,也无需太多解释:import Foundationpubl...
    继续阅读 »

    登录。。。基本所有APP都少不了,开始写APP,可能首先就是从登录开始
    我也一样,我手上有一个封装了所有账户体系相关接口的SDK,运行良好但也遇到一些烦心事,就拿登录来说说吧。

    首先有如下相关封装,很常见,也无需太多解释:

    import Foundation

    public typealias Response = (_ json: String?, _ error: Error?) -> Void

    // 账户体系管理器
    public class AccountMgr: NSObject {
    private override init() {}
    public static let shared = AccountMgr()
    }

    public extension AccountMgr {
    /// 登录
    /// - Parameters:
    /// - accountType: 账户类型 see `AccountType`
    /// - password: 密码
    /// - res: 请求结果
    func login(by accountType: AccountType, password: String, res: Response?) {
    var params = [String: Any]()
    switch accountType {
    case let .email(email):
    params["type"] = "email"
    params["email"] = email
    case let .mobile(mobile, mobileArea):
    params["type"] = "mobile"
    params["mobile"] = mobile
    params["mobileArea"] = mobileArea
    }

    params["password"] = password
    //网络请求,并回调
    //request(type: .post, api: .login, params: params, res: res)
    }
    }

    /// 账号类型
    public enum AccountType {
    /// 手机号
    /// - mobile: 手机号
    /// - mobileArea: 国家区号(中国 86)
    case mobile(_ phoneNumber: String, mobileArea: String = "86")
    /// 邮箱
    case email(_ email: String)
    }

    使用也很方便:

    // 分开使用
    AccountMgr.shared.login(by: .email(""), password: "", res: nil)
    AccountMgr.shared.login(by: .mobile("", mobileArea: ""), password: "", res: nil)

    // 合并使用
    var loginType: AccountType
    if inputEmail {
    loginType = .email("test@weixian.com")
    } else {
    loginType = .mobile("18000000000", mobileArea: "86")
    }
    AccountMgr.shared.login(by: loginType, password: "xxxxx", res: nil)

    无论是邮箱,手机号登录分开逻辑登录,还是统一的登录管理器登录都能胜任,并且只有两种登录,分开写也不会多很多代码。

    有一天,这个SDK需要在OC项目中使用

    感觉没爱了,懒得想太多,直接废弃了Swift 枚举的便利性,写成了两个方法:

    public class AccountMgr: NSObject {
    private override init() {}
    @objc(shareInstance)
    public static let shared = AccountMgr()
    }

    @objc func loginBy(email: String, password: String, res: Response?)

    @objc func loginBy(mobile: String, mobilArea: String, password: String, res: Response?)

    之所以写成loginBy(email:)而不是login(by email:),主要是为了swift 转 OC 后使用的时候能直接看懂,也不需要去查看定义,看如下截图就能明白了:


    第一个方法不看定义,应该没办法了解参数应该填什么了。

    就这样,我的SDK又运行了一段时间,看起来也没什么大问题,无非是手机登录和邮箱登录一定要分开调用罢了

    又有一天,这个登录方法要增加用户账号登录

    依样画葫芦,我又增加了一个接口~~~,只是这样,那故事就结束了。

    可惜,我还有第三方绑定接口,即微信登录后绑定手机,邮箱,或账号、、、、这里又三个接口,还有查询账号信息又三个,还有。。。又三个。。。,还有。。。又三个。。。

    这个时候我又开始怀念第一版的接口了,其实这很容易解决,只要一个整型枚举,然后把多出来的参数设置为可选,虽然使用的时候会有点奇怪,但是很好的解决了问题。并且最终我也是这么做的,可我还是想在Swift中能够更好的使用Swfit特性,写出更简洁的代码。。所以我写了两套接口。。。。,一套OC使用,一套Swfit使用,因为我总觉得在不久的将来,我就不需要支持OC了:

    首先增加了一个OC的类型枚举:

    @objc public enum AccountType_OC: Int {
    case mobile
    case email
    case userId
    }

    然后增加了一个只有OC可用的方法:

    @available(swift 10.0)
    @objc func loginBy(accountType: AccountType_OC, account: String, password: String, mobileArea: String?, res: Response?) {
    let type = getSwiftAccountType(accountType: accountType, account: account, mobileArea: mobileArea)
    login(by: type, password: password, res: res)
    }

    private func getSwiftAccountType(accountType: AccountType_OC, account: String, mobileArea: String?) -> AccountType {
    var type: AccountType
    switch accountType {
    case .mobile:
    guard let mobileArea = mobileArea else { fatalError("need mobile area") }
    type = .mobile(account, mobileArea: mobileArea)
    case .email:
    type = .email(account)
    case .userId:
    type = .userId(account)
    }
    return type
    }

    OC中没办法给参数赋默认值,即类似mobileArea: String = "86" 这种,完全没有用。。。

    私有类型转换的方法的封装,使得所有其他方法可以快速转换,关于@available(swift 10.0) 意思就是说只有Swift 版本10.0只后才可以使用。。即变相达到了,在Swift 代码中不会出现这个方法,只有下面方法可以使用:

    func login(by accountType: AccountType, password: String, res: Response?)

    基本就是这样了,看起来很麻烦,也确实挺麻烦,其实完全可以只保留OC使用的方法,这完全归于我的代码洁癖,以及我自己在使用Swift和对于日后去掉OC支持时我可以快乐的删代码的白日幻想。

    当然,如果你只是在自己的混编APP内部封装一些接口,那一套接口应该是比较好的,如果你的是SDK,同时你也不是很怕麻烦,像我这样写也许会有一些意外的收获。

    链接:https://www.jianshu.com/p/247c1e923c5c

    收起阅读 »

    iOS自定义键盘-简单版

    为什么说是简单版,因为这里只说一个数字键盘。一,怎么自定义键盘随便一个view都可以作为键盘,主要代码是为你的输入框指定inputView,这个inputView就是键盘,键盘具体什么样子都可以。kfZNumberKeyBoard * mkb = [kfZNu...
    继续阅读 »

    为什么说是简单版,因为这里只说一个数字键盘。
    一,怎么自定义键盘
    随便一个view都可以作为键盘,主要代码是为你的输入框指定inputView,这个inputView就是键盘,键盘具体什么样子都可以。

    kfZNumberKeyBoard * mkb = [kfZNumberKeyBoard moneyKeyBoardBuyer];

    UITextField * field = [[UITextField alloc]init];
    field.backgroundColor = [UIColor cyanColor];
    field.inputView = mkb;
    [self.view addSubview:field];
    field.frame = CGRectMake(20, NavBottom + 50, DEF_SCREEN_WIDTH - 40, 40);

    二,自定义键盘怎么实现各种输入
    这里千万不要自己拼接字符串太容易出问题了,用系统自带的方法。我们发现不管UITextField还是UITextView都遵循UITextInput协议,这个协议又遵循UIKeyInput协议,我们用的就是UIKeyInput协议中的方法。

    - (void)insertText:(NSString *)text;//插入文字,不用处理光标位置
    - (void)deleteBackward;//删除,不用处理光标位置

    用这两个方法是不是事情就特别简单了,其实说到这里已经可以了,怎么做都说完了。不过我还是推销一下我写的数字键盘吧。最后面我会贴出代码用的可以拷贝改一下。

    三,数字键盘
    先看效果图:


    a.UI布局上,删除和确定是单独的按键,其他部分我用了collectionView,想着之后做的乱序加密效果好做,打乱数据源刷新一下就行(当然现在没有,不是懒,过渡开发是病)
    b.获取当前输入框,这里为了不在外面传,直接在内部监听了输入框开始输入和结束输入。
    c.加了几个输入限制:
    1.有小数点不能在输入小数点
    2.内容为空输入小数点时,前面自动补0
    3.最大小数位数限制(测试不多可能有bug哦)
    4.移除焦点时小数点前面没东西自动补0
    5.输入框有内容确定可以点击,输入框没内容确定不能点击。

    下面是代码了:

    @interface kfZNumberKeyBoard : UIView

    /** 确认按键 */
    @property (nonatomic, strong) UIButton * returnButton;
    /** 有没有小数点 */
    @property (nonatomic, assign) BOOL hiddenPoint;
    /** 小数位数,为0不限制,不需要小数时请使用hiddenPoint隐藏点 默认是2 */
    @property (nonatomic, assign) NSUInteger decimalCount;
    /** 整体高度 */
    @property (nonatomic, assign, readonly) CGFloat KFZNumberKeyBoardHeight;

    +(instancetype)moneyKeyBoardBuyer;
    +(instancetype)moneyKeyBoardSeller;

    -(instancetype)initWitHiddenPoint:(BOOL)hiddenPoint;

    @end
    #import "kfZNumberKeyBoard.h"
    #import "KFZKeyBoardCell.h"
    @interface kfZNumberKeyBoard ()

    @property(nonatomic, weak) UIView * textInputView;

    /** 删除按键 */
    @property (nonatomic, strong) UIButton * deleteButton;

    @property (nonatomic, strong) UICollectionView *collectionView;
    @property (nonatomic, strong) NSArray *dataSource;

    /** 间隔 */
    @property (nonatomic, assign) CGFloat KFZNumberKeyBoardSpace;
    /** 数字按键高度 */
    @property (nonatomic, assign) CGFloat KFZNumberKeyBoardItemHeight;

    @end

    @implementation kfZNumberKeyBoard

    +(instancetype)moneyKeyBoardBuyer{
    kfZNumberKeyBoard * keyBoard = [[kfZNumberKeyBoard alloc]initWitHiddenPoint:NO];
    return keyBoard;
    }

    +(instancetype)moneyKeyBoardSeller{
    kfZNumberKeyBoard * keyBoard = [[kfZNumberKeyBoard alloc]initWitHiddenPoint:NO];
    keyBoard.returnButton.backgroundColor = [UIColor maintonal_sellerMain];
    return keyBoard;
    }

    -(instancetype)initWitHiddenPoint:(BOOL)hiddenPoint{
    self = [super init];
    if (self) {
    _hiddenPoint = hiddenPoint;
    _KFZNumberKeyBoardItemHeight = 50.f;
    _KFZNumberKeyBoardSpace = 0.5;
    _KFZNumberKeyBoardHeight = _KFZNumberKeyBoardItemHeight * 4 + _KFZNumberKeyBoardSpace * 5 + HOMEINDICATOR_HEIGHT;
    _decimalCount = 2;

    self.frame = CGRectMake(0, 0, DEF_SCREEN_WIDTH, _KFZNumberKeyBoardHeight);

    _deleteButton = [[UIButton alloc]init];
    _deleteButton.backgroundColor = [UIColor color_FAFAFA];
    [_deleteButton setImage:[UIImage imageNamed:@"keyboard_icon_backspace"] forState:UIControlStateNormal];
    [_deleteButton addTarget:self action:@selector(deleteEvent) forControlEvents:UIControlEventTouchUpInside];
    [self addSubview:_deleteButton];
    [_deleteButton mas_makeConstraints:^(MASConstraintMaker *make) {
    make.top.mas_equalTo(_KFZNumberKeyBoardSpace);
    make.right.mas_equalTo(0.f);
    make.width.equalTo(self).multipliedBy(0.25);
    }];

    _returnButton = [[UIButton alloc]init];
    [_returnButton setTitle:@"确定" forState:UIControlStateNormal];
    [_returnButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
    _returnButton.titleLabel.font = [UIFont custemFontOfSize:20 weight:UIFontWeightRegular];
    _returnButton.backgroundColor = [UIColor mainTonal_main];
    [_returnButton addTarget:self action:@selector(returnEvent) forControlEvents:UIControlEventTouchUpInside];
    [self addSubview:_returnButton];
    [_returnButton mas_makeConstraints:^(MASConstraintMaker *make) {
    make.top.equalTo(_deleteButton.mas_bottom);
    make.right.equalTo(_deleteButton);
    make.bottom.mas_equalTo(-HOMEINDICATOR_HEIGHT);
    make.height.equalTo(_deleteButton);
    make.width.equalTo(_deleteButton).offset(_KFZNumberKeyBoardSpace);
    }];

    //101对应小数点 102对应收起键盘 修改的话其他的判断逻辑也要修改
    _dataSource = @[@(1), @(2), @(3), @(4), @(5), @(6), @(7), @(8), @(9), @(101), @(0), @(102)];

    UICollectionViewFlowLayout * layout = [[UICollectionViewFlowLayout alloc]init];
    layout.itemSize = CGSizeMake((DEF_SCREEN_WIDTH * 3.f/4.f - _KFZNumberKeyBoardSpace*3)/3.f, (_KFZNumberKeyBoardHeight - HOMEINDICATOR_HEIGHT - _KFZNumberKeyBoardSpace*5)/4.f);
    layout.sectionInset = UIEdgeInsetsMake(_KFZNumberKeyBoardSpace, 0, _KFZNumberKeyBoardSpace, _KFZNumberKeyBoardSpace);
    layout.minimumLineSpacing = _KFZNumberKeyBoardSpace;
    layout.minimumInteritemSpacing = _KFZNumberKeyBoardSpace;

    _collectionView = [[UICollectionView alloc]initWithFrame:CGRectZero collectionViewLayout:layout];
    _collectionView.dataSource = self;
    _collectionView.delegate = self;
    [_collectionView registerClass:[KFZKeyBoardCell class] forCellWithReuseIdentifier:NSStringFromClass([KFZKeyBoardCell class])];
    _collectionView.backgroundColor = [UIColor clearColor];
    _collectionView.scrollEnabled = NO;
    [self addSubview:_collectionView];
    [_collectionView mas_makeConstraints:^(MASConstraintMaker *make) {
    make.top.left.mas_equalTo(0.f);
    make.bottom.mas_equalTo(-HOMEINDICATOR_HEIGHT);
    make.right.equalTo(_deleteButton.mas_left);
    }];


    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textInputViewDidBeginEditing:) name:UITextFieldTextDidBeginEditingNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textInputViewDidEndEditing:) name:UITextFieldTextDidEndEditingNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textInputViewDidBeginEditing:) name:UITextViewTextDidBeginEditingNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(textInputViewDidEndEditing:) name:UITextViewTextDidEndEditingNotification object:nil];
    }
    return self;
    }

    -(void)dealloc{
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UITextFieldTextDidBeginEditingNotification object:nil];
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UITextFieldTextDidEndEditingNotification object:nil];
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UITextViewTextDidBeginEditingNotification object:nil];
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UITextViewTextDidEndEditingNotification object:nil];
    }

    #pragma mark - response

    -(void)textInputWithNumber:(NSNumber *)number{
    NSString *strValue = [self inputViewString];

    if ([number isEqualToNumber:@(101)]) {
    if ([strValue containsString:@"."]){
    return;
    }else{
    if ([strValue length] <= 0)
    [self.textInputView insertText:@"0."];
    else
    [self.textInputView insertText:@"."];
    }
    }else{
    if ([strValue containsString:@"."] && _decimalCount > 0) {
    NSInteger pointLocation = [strValue rangeOfString:@"."].location;
    NSInteger curDecimalCount = strValue.length - pointLocation - 1;
    if (curDecimalCount >= _decimalCount) {
    NSInteger cursorLocation = [self inputViewSelectRangeLocation];
    if (cursorLocation <= pointLocation) {
    [_textInputView insertText:number.stringValue];
    }
    }else{
    [_textInputView insertText:number.stringValue];
    }
    }else{
    [_textInputView insertText:number.stringValue];
    }
    }
    [self freshReturnButtonEnabled];
    }

    -(void)deleteEvent{
    [_textInputView deleteBackward];
    [self freshReturnButtonEnabled];
    }

    -(void)returnEvent{
    [_textInputView resignFirstResponder];
    }

    -(void)textInputViewDidBeginEditing:(NSNotification*)notification{
    _textInputView = notification.object;
    [self freshReturnButtonEnabled];
    }

    -(void)textInputViewDidEndEditing:(NSNotification*)notification{
    NSString *strValue = [self inputViewString];
    if ([strValue startsWithString:@"."]) {
    strValue = [NSString stringWithFormat:@"0%@", strValue];
    [self setInputViewString:strValue];
    }
    _textInputView = nil;

    }

    -(NSString *)inputViewString{
    NSString *strValue = @"";
    if ([self.textInputView isKindOfClass:[UITextView class]]){
    strValue = ((UITextView *)self.textInputView).text;
    }else if ([self.textInputView isKindOfClass:[UITextField class]]){
    strValue = ((UITextField *)self.textInputView).text;
    }
    return strValue;
    }

    -(void)setInputViewString:(NSString *)string{
    if ([self.textInputView isKindOfClass:[UITextView class]]){
    ((UITextView *)self.textInputView).text = string;
    }else if ([self.textInputView isKindOfClass:[UITextField class]]){
    ((UITextField *)self.textInputView).text = string;
    }
    }

    -(NSInteger)inputViewSelectRangeLocation{
    NSInteger location = 0;
    if ([self.textInputView isKindOfClass:[UITextView class]]){
    UITextView * textView = (UITextView *)self.textInputView;
    location = textView.selectedRange.location;
    }else if ([self.textInputView isKindOfClass:[UITextField class]]){
    UITextField *textField = (UITextField *)self.textInputView;
    UITextPosition* beginning = textField.beginningOfDocument;
    UITextRange* selectedRange = textField.selectedTextRange;
    UITextPosition* selectionStart = selectedRange.start;
    location = [textField offsetFromPosition:beginning toPosition:selectionStart];
    }
    return location;
    }

    -(void)freshReturnButtonEnabled{
    NSString *strValue = [self inputViewString];
    if (strValue.length == 0) {
    _returnButton.enabled = NO;
    _returnButton.alpha = 0.6;
    }else{
    _returnButton.enabled = YES;
    _returnButton.alpha = 1.f;
    }
    }

    #pragma mark -- Delegate
    #pragma mark - UICollectionViewDataSource

    - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
    return self.dataSource.count;
    }

    - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
    KFZKeyBoardCell * cell = [collectionView dequeueReusableCellWithReuseIdentifier:NSStringFromClass([KFZKeyBoardCell class]) forIndexPath:indexPath];
    NSNumber * number = self.dataSource[indexPath.row];
    if ([number isEqualToNumber:@(101)] && _hiddenPoint) {
    cell.textLabel.text = @"";
    }else{
    cell.textNumber = number;
    }
    return cell;
    }

    #pragma mark - UICollectionViewDelegate

    - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath{
    NSNumber * number = self.dataSource[indexPath.row];
    if ([number isEqualToNumber:@(102)]) {
    [_textInputView resignFirstResponder];
    return;
    }
    if ([number isEqualToNumber:@(101)] && _hiddenPoint) {
    return;
    }
    [self textInputWithNumber:number];
    }

    #pragma mark - init

    -(void)setHiddenPoint:(BOOL)hiddenPoint{
    _hiddenPoint = hiddenPoint;
    [_collectionView reloadData];
    }

    @end

    这个是里面cell的:

    @interface KFZKeyBoardCell : UICollectionViewCell
    /** 文字 */
    @property (nonatomic, strong) UILabel * textLabel;
    /** 图片 */
    @property (nonatomic, strong) UIImageView * imageIcon;

    /** 设置值 */
    @property (nonatomic, strong) NSNumber * textNumber;
    @end
    #import "KFZKeyBoardCell.h"

    @implementation KFZKeyBoardCell

    - (instancetype)initWithFrame:(CGRect)frame{
    self = [super initWithFrame:frame];
    if (self) {
    self.backgroundColor = [UIColor color_FAFAFA];

    [self.contentView addSubview:self.textLabel];
    [self.textLabel mas_makeConstraints:^(MASConstraintMaker *make) {
    make.top.bottom.left.right.mas_equalTo(0.f);
    }];

    self.imageIcon.hidden = YES;
    [self.contentView addSubview:self.imageIcon];
    [self.imageIcon mas_makeConstraints:^(MASConstraintMaker *make) {
    make.center.mas_equalTo(CGPointZero);
    make.size.mas_equalTo(CGSizeMake(24.f, 22.f));
    }];
    }
    return self;
    }
    -(void)prepareForReuse{
    self.textLabel.hidden = NO;
    self.imageIcon.hidden = YES;
    }
    - (void)setTextNumber:(NSNumber *)textNumber{
    _textNumber = textNumber;
    if ([textNumber isEqualToNumber:@(101)]) {
    self.textLabel.text = @"·";
    }
    else if ([textNumber isEqualToNumber:@(102)]){
    self.textLabel.hidden = YES;
    self.imageIcon.hidden = NO;
    self.imageIcon.image = [UIImage imageNamed:@"keyboard_icon_smallkb"];
    }
    else{
    self.textLabel.text = textNumber.stringValue;
    }
    }

    - (UILabel *)textLabel{
    if (!_textLabel) {
    _textLabel = [[UILabel alloc]init];
    _textLabel.font = [UIFont KFZSpecial_DINAlternateBoldWithFontSize:24.f];
    _textLabel.textAlignment = NSTextAlignmentCenter;
    _textLabel.userInteractionEnabled = NO;
    _textLabel.backgroundColor = UIColor.clearColor;
    }
    return _textLabel;
    }

    -(UIImageView *)imageIcon{
    if (!_imageIcon) {
    _imageIcon = [[UIImageView alloc]init];
    }
    return _imageIcon;
    }

    @end

    转自:https://www.jianshu.com/p/226f67166770

    收起阅读 »

    iOS 设备信息获取

    1.获取电池电量(一般用百分数表示,大家自行处理就好)-(CGFloat)getBatteryQuantity{ return [[UIDevice currentDevice] batteryLevel];}2.获取电池状态(UIDeviceBatte...
    继续阅读 »

    1.获取电池电量(一般用百分数表示,大家自行处理就好)

    -(CGFloat)getBatteryQuantity
    {
    return [[UIDevice currentDevice] batteryLevel];
    }

    2.获取电池状态(UIDeviceBatteryState为枚举类型)

    -(UIDeviceBatteryState)getBatteryStauts
    {
    return [UIDevice currentDevice].batteryState;
    }

    3.获取总内存大小

    -(long long)getTotalMemorySize
    {
    return [NSProcessInfo processInfo].physicalMemory;
    }

    4.获取当前可用内存

    -(long long)getAvailableMemorySize
    {
    vm_statistics_data_t vmStats;
    mach_msg_type_number_t infoCount = HOST_VM_INFO_COUNT;
    kern_return_t kernReturn = host_statistics(mach_host_self(), HOST_VM_INFO, (host_info_t)&vmStats, &infoCount);
    if (kernReturn != KERN_SUCCESS)
    {
    return NSNotFound;
    }
    return ((vm_page_size * vmStats.free_count + vm_page_size * vmStats.inactive_count));
    }

    5.获取已使用内存

    - (double)getUsedMemory
    {
    task_basic_info_data_t taskInfo;
    mach_msg_type_number_t infoCount = TASK_BASIC_INFO_COUNT;
    kern_return_t kernReturn = task_info(mach_task_self(),
    TASK_BASIC_INFO,
    (task_info_t)&taskInfo,
    &infoCount);

    if (kernReturn != KERN_SUCCESS
    ) {
    return NSNotFound;
    }

    return taskInfo.resident_size;
    }

    6.获取总磁盘容量

    include 
    -(long long)getTotalDiskSize
    {
    struct statfs buf;
    unsigned long long freeSpace = -1;
    if (statfs("/var", &buf) >= 0)
    {
    freeSpace = (unsigned long long)(buf.f_bsize * buf.f_blocks);
    }
    return freeSpace;
    }

    7.获取可用磁盘容量

    -(long long)getAvailableDiskSize
    {
    struct statfs buf;
    unsigned long long freeSpace = -1;
    if (statfs("/var", &buf) >= 0)
    {
    freeSpace = (unsigned long long)(buf.f_bsize * buf.f_bavail);
    }
    return freeSpace;
    }

    8.容量转换

    -(NSString *)fileSizeToString:(unsigned long long)fileSize
    {
    NSInteger KB = 1024;
    NSInteger MB = KB*KB;
    NSInteger GB = MB*KB;

    if (fileSize < 10) {
    return @"0 B";
    }else if (fileSize < KB) {
    return @"< 1 KB";
    }else if (fileSize < MB) {
    return [NSString stringWithFormat:@"%.1f KB",((CGFloat)fileSize)/KB];
    }else if (fileSize < GB) {
    return [NSString stringWithFormat:@"%.1f MB",((CGFloat)fileSize)/MB];
    }else {
    return [NSString stringWithFormat:@"%.1f GB",((CGFloat)fileSize)/GB];
    }
    }

    9.型号

    #import 

    + (NSString *)getCurrentDeviceModel:(UIViewController *)controller
    {
    int mib[2];
    size_t len;
    char *machine;

    mib[0] = CTL_HW;
    mib[1] = HW_MACHINE;
    sysctl(mib, 2, NULL, &len, NULL, 0);
    machine = malloc(len);
    sysctl(mib, 2, machine, &len, NULL, 0);

    NSString *platform = [NSString stringWithCString:machine encoding:NSASCIIStringEncoding];
    free(machine);

    if ([platform isEqualToString:@"iPhone3,1"]) return @"iPhone 4 (A1332)";
    if ([platform isEqualToString:@"iPhone3,2"]) return @"iPhone 4 (A1332)";
    if ([platform isEqualToString:@"iPhone3,3"]) return @"iPhone 4 (A1349)";
    if ([platform isEqualToString:@"iPhone4,1"]) return @"iPhone 4s (A1387/A1431)";
    if ([platform isEqualToString:@"iPhone5,1"]) return @"iPhone 5 (A1428)";
    if ([platform isEqualToString:@"iPhone5,2"]) return @"iPhone 5 (A1429/A1442)";
    if ([platform isEqualToString:@"iPhone5,3"]) return @"iPhone 5c (A1456/A1532)";
    if ([platform isEqualToString:@"iPhone5,4"]) return @"iPhone 5c (A1507/A1516/A1526/A1529)";
    if ([platform isEqualToString:@"iPhone6,1"]) return @"iPhone 5s (A1453/A1533)";
    if ([platform isEqualToString:@"iPhone6,2"]) return @"iPhone 5s (A1457/A1518/A1528/A1530)";
    if ([platform isEqualToString:@"iPhone7,1"]) return @"iPhone 6 Plus (A1522/A1524)";
    if ([platform isEqualToString:@"iPhone7,2"]) return @"iPhone 6 (A1549/A1586)";
    if ([platform isEqualToString:@"iPhone8,1"]) return @"iPhone 6s";
    if ([platform isEqualToString:@"iPhone8,2"]) return @"iPhone 6s Plus";
    if ([platform isEqualToString:@"iPhone8,4"]) return @"iPhone SE";
    if ([platform isEqualToString:@"iPhone9,1"]) return @"国行、日版、港行iPhone 7";
    if ([platform isEqualToString:@"iPhone9,2"]) return @"港行、国行iPhone 7 Plus";
    if ([platform isEqualToString:@"iPhone9,3"]) return @"美版、台版iPhone 7";
    if ([platform isEqualToString:@"iPhone9,4"]) return @"美版、台版iPhone 7 Plus";
    if ([platform isEqualToString:@"iPhone10,1"]) return @"国行(A1863)、日行(A1906)iPhone 8";
    if ([platform isEqualToString:@"iPhone10,4"]) return @"美版(Global/A1905)iPhone 8";
    if ([platform isEqualToString:@"iPhone10,2"]) return @"国行(A1864)、日行(A1898)iPhone 8 Plus";
    if ([platform isEqualToString:@"iPhone10,5"]) return @"美版(Global/A1897)iPhone 8 Plus";
    if ([platform isEqualToString:@"iPhone10,3"]) return @"国行(A1865)、日行(A1902)iPhone X";
    if ([platform isEqualToString:@"iPhone10,6"]) return @"美版(Global/A1901)iPhone X";

    if ([platform isEqualToString:@"iPod1,1"]) return @"iPod Touch 1G (A1213)";
    if ([platform isEqualToString:@"iPod2,1"]) return @"iPod Touch 2G (A1288)";
    if ([platform isEqualToString:@"iPod3,1"]) return @"iPod Touch 3G (A1318)";
    if ([platform isEqualToString:@"iPod4,1"]) return @"iPod Touch 4G (A1367)";
    if ([platform isEqualToString:@"iPod5,1"]) return @"iPod Touch 5G (A1421/A1509)";

    if ([platform isEqualToString:@"iPad1,1"]) return @"iPad 1G (A1219/A1337)";
    if ([platform isEqualToString:@"iPad2,1"]) return @"iPad 2 (A1395)";
    if ([platform isEqualToString:@"iPad2,2"]) return @"iPad 2 (A1396)";
    if ([platform isEqualToString:@"iPad2,3"]) return @"iPad 2 (A1397)";
    if ([platform isEqualToString:@"iPad2,4"]) return @"iPad 2 (A1395+New Chip)";
    if ([platform isEqualToString:@"iPad2,5"]) return @"iPad Mini 1G (A1432)";
    if ([platform isEqualToString:@"iPad2,6"]) return @"iPad Mini 1G (A1454)";
    if ([platform isEqualToString:@"iPad2,7"]) return @"iPad Mini 1G (A1455)";

    if ([platform isEqualToString:@"iPad3,1"]) return @"iPad 3 (A1416)";
    if ([platform isEqualToString:@"iPad3,2"]) return @"iPad 3 (A1403)";
    if ([platform isEqualToString:@"iPad3,3"]) return @"iPad 3 (A1430)";
    if ([platform isEqualToString:@"iPad3,4"]) return @"iPad 4 (A1458)";
    if ([platform isEqualToString:@"iPad3,5"]) return @"iPad 4 (A1459)";
    if ([platform isEqualToString:@"iPad3,6"]) return @"iPad 4 (A1460)";

    if ([platform isEqualToString:@"iPad4,1"]) return @"iPad Air (A1474)";
    if ([platform isEqualToString:@"iPad4,2"]) return @"iPad Air (A1475)";
    if ([platform isEqualToString:@"iPad4,3"]) return @"iPad Air (A1476)";
    if ([platform isEqualToString:@"iPad4,4"]) return @"iPad Mini 2G (A1489)";
    if ([platform isEqualToString:@"iPad4,5"]) return @"iPad Mini 2G (A1490)";
    if ([platform isEqualToString:@"iPad4,6"]) return @"iPad Mini 2G (A1491)";
    if ([platform isEqualToString:@"iPad4,7"]) return @"iPad Mini 3";
    if ([platform isEqualToString:@"iPad4,8"]) return @"iPad Mini 3";
    if ([platform isEqualToString:@"iPad4,9"]) return @"iPad Mini 3";
    if ([platform isEqualToString:@"iPad5,1"]) return @"iPad Mini 4 (WiFi)";
    if ([platform isEqualToString:@"iPad5,2"]) return @"iPad Mini 4 (LTE)";
    if ([platform isEqualToString:@"iPad5,3"]) return @"iPad Air 2";
    if ([platform isEqualToString:@"iPad5,4"]) return @"iPad Air 2";
    if ([platform isEqualToString:@"iPad6,3"]) return @"iPad Pro 9.7";
    if ([platform isEqualToString:@"iPad6,4"]) return @"iPad Pro 9.7";
    if ([platform isEqualToString:@"iPad6,7"]) return @"iPad Pro 12.9";
    if ([platform isEqualToString:@"iPad6,8"]) return @"iPad Pro 12.9";
    if ([platform isEqualToString:@"iPad6,11"]) return @"iPad 5 (WiFi)";
    if ([platform isEqualToString:@"iPad6,12"]) return @"iPad 5 (Cellular)";
    if ([platform isEqualToString:@"iPad7,1"]) return @"iPad Pro 12.9 inch 2nd gen (WiFi)";
    if ([platform isEqualToString:@"iPad7,2"]) return @"iPad Pro 12.9 inch 2nd gen (Cellular)";
    if ([platform isEqualToString:@"iPad7,3"]) return @"iPad Pro 10.5 inch (WiFi)";
    if ([platform isEqualToString:@"iPad7,4"]) return @"iPad Pro 10.5 inch (Cellular)";

    if ([platform isEqualToString:@"AppleTV2,1"]) return @"Apple TV 2";
    if ([platform isEqualToString:@"AppleTV3,1"]) return @"Apple TV 3";
    if ([platform isEqualToString:@"AppleTV3,2"]) return @"Apple TV 3";
    if ([platform isEqualToString:@"AppleTV5,3"]) return @"Apple TV 4";

    if ([platform isEqualToString:@"i386"]) return @"iPhone Simulator";
    if ([platform isEqualToString:@"x86_64"]) return @"iPhone Simulator";
    return platform;
    }

    10.IP地址

    #import 和#import 

    - (NSString *)deviceIPAdress {
    NSString *address = @"an error occurred when obtaining ip address";
    struct ifaddrs *interfaces = NULL;
    struct ifaddrs *temp_addr = NULL;
    int success = 0;

    success = getifaddrs(&interfaces);

    if (success == 0) { // 0 表示获取成功

    temp_addr = interfaces;
    while (temp_addr != NULL) {
    if( temp_addr->ifa_addr->sa_family == AF_INET) {
    // Check if interface is en0 which is the wifi connection on the iPhone
    if ([[NSString stringWithUTF8String:temp_addr->ifa_name] isEqualToString:@"en0"]) {
    // Get NSString from C String
    address = [NSString stringWithUTF8String:inet_ntoa(((struct sockaddr_in *)temp_addr->ifa_addr)->sin_addr)];
    }
    }

    temp_addr = temp_addr->ifa_next;
    }
    }

    freeifaddrs(interfaces);
    return address;
    }

    11.当前手机连接的WIFI名称(SSID)

    需要#import 

    - (NSString *)getWifiName
    {
    NSString *wifiName = nil;

    CFArrayRef wifiInterfaces = CNCopySupportedInterfaces();
    if (!wifiInterfaces) {
    return nil;
    }

    NSArray *interfaces = (__bridge NSArray *)wifiInterfaces;

    for (NSString *interfaceName in interfaces) {
    CFDictionaryRef dictRef = CNCopyCurrentNetworkInfo((__bridge CFStringRef)(interfaceName));

    if (dictRef) {
    NSDictionary *networkInfo = (__bridge NSDictionary *)dictRef;

    wifiName = [networkInfo objectForKey:(__bridge NSString *)kCNNetworkInfoKeySSID];

    CFRelease(dictRef);
    }
    }

    CFRelease(wifiInterfaces);
    return wifiName;
    }

    12.当前手机系統版本

    [[[UIDevice currentDevice] systemVersion] floatValue] ;


    摘自作者:Cooci_和谐学习_不急不躁
    原贴链接:https://www.jianshu.com/p/b25cdf09ece2

    收起阅读 »

    WKWebView的特性及原理

    WKWebView是在Apple的WWDC 2014随iOS 8和OS X 10.10出来的,是为了解决UIWebView加载速度慢、占用内存大的问题。使用UIWebView加载网页的时候,我们会发现内存会无限增长,还有内存泄漏的问题存在。WebKit中更新的...
    继续阅读 »

    WKWebView是在Apple的WWDC 2014随iOS 8和OS X 10.10出来的,是为了解决UIWebView加载速度慢、占用内存大的问题。

    使用UIWebView加载网页的时候,我们会发现内存会无限增长,还有内存泄漏的问题存在。

    WebKit中更新的WKWebView控件的新特性与使用方法,它很好的解决了UIWebView存在的内存、加载速度等诸多问题。

    一、WKWebView新特性

    在性能、稳定性、功能方面有很大提升(最直观的体现就是加载网页是占用的内存);

    允许JavaScript的Nitro库加载并使用(UIWebView中限制);

    支持了更多的HTML5特性;

    高达60fps的滚动刷新率以及内置手势;

    将UIWebViewDelegate与UIWebView重构成了14类与3个协议查看苹果官方文档

    二、WebKit框架概览


    如上图所示,WebKit框架中最核心的类应该属于WKWebView了,这个类专门用来渲染网页视图,其他类和协议都将基于它和服务于它。

    WKWebView:网页的渲染与展示,通过WKWebViewConfiguration可以进行自定义配置

    WKWebViewConfiguration:这个类专门用来配置WKWebView。

    WKPreference:这个类用来进行相关webView设置。

    WKProcessPool:这个类用来配置进程池,与网页视图的资源共享有关。

    WKUserContentController:这个类主要用来做native与JavaScript的交互管理。

    WKUserScript:用于进行JavaScript注入。

    WKScriptMessageHandler:这个类专门用来处理JavaScript调用native的方法。

    WKNavigationDelegate:网页跳转间的导航管理协议,这个协议可以监听网页的活动

    WKNavigationAction:网页某个活动的示例化对象。

    WKUIDelegate:用于交互处理JavaScript中的一些弹出框。

    WKBackForwardList:堆栈管理的网页列表。

    WKBackForwardListItem:每个网页节点对象。

    三、WKWebView的属性

    /// webView的自定义配置
    @property (nonatomic,readonly, copy) WKWebViewConfiguration *configuration;
    /// 导航代理
    @property (nullable, nonatomic, weak)id navigationDelegate;
    /// UI代理
    @property (nullable, nonatomic, weak)id UIDelegate;
    /// 访问过网页历史列表
    @property (nonatomic,readonly, strong) WKBackForwardList *backForwardList;

    /// 自定义初始化
    - (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration NS_DESIGNATED_INITIALIZER;- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER;
    /// url加载webView视图
    - (nullable WKNavigation *)loadRequest:(NSURLRequest *)request;
    /// 文件加载webView视图
    - (nullable WKNavigation *)loadFileURL:(NSURL *)URL allowingReadAccessToURL:(NSURL *)readAccessURL API_AVAILABLE(macosx(10.11), ios(9.0));
    /// HTMLString字符串加载webView视图
    - (nullable WKNavigation *)loadHTMLString:(NSString *)stringbaseURL:(nullable NSURL *)baseURL;
    /// NSData数据加载webView视图
    - (nullable WKNavigation *)loadData:(NSData *)data MIMEType:(NSString *)MIMEType characterEncodingName:(NSString *)characterEncodingName baseURL:(NSURL *)baseURL API_AVAILABLE(macosx(10.11), ios(9.0));
    /// 返回上一个网页节点
    - (nullable WKNavigation *)goToBackForwardListItem:(WKBackForwardListItem *)item;

    /// 网页的标题
    @property (nullable, nonatomic,readonly, copy) NSString *title;
    /// 网页的URL地址
    @property (nullable, nonatomic,readonly, copy) NSURL *URL;
    /// 网页是否正在加载
    @property (nonatomic,readonly, getter=isLoading) BOOL loading;
    /// 加载的进度 范围为[0, 1]
    @property (nonatomic,readonly)double estimatedProgress;
    /// 网页链接是否安全
    @property (nonatomic,readonly) BOOL hasOnlySecureContent;
    /// 证书服务
    @property (nonatomic,readonly, nullable) SecTrustRef serverTrust API_AVAILABLE(macosx(10.12), ios(10.0));
    /// 是否可以返回
    @property (nonatomic,readonly) BOOL canGoBack;
    /// 是否可以前进
    @property (nonatomic,readonly) BOOL canGoForward;

    /// 返回到上一个网页
    - (nullable WKNavigation *)goBack;
    /// 前进到下一个网页
    - (nullable WKNavigation *)goForward;
    /// 重新加载
    - (nullable WKNavigation *)reload;
    /// 忽略缓存 重新加载
    - (nullable WKNavigation *)reloadFromOrigin;
    /// 停止加载
    - (void)stopLoading;
    /// 执行JavaScript
    - (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void(^ _Nullable)(_Nullableid, NSError * _Nullable error))completionHandler;

    /// 是否允许左右滑动,返回-前进操作 默认是NO
    @property (nonatomic) BOOL allowsBackForwardNavigationGestures;
    /// 自定义代理字符串
    @property (nullable, nonatomic, copy) NSString *customUserAgent API_AVAILABLE(macosx(10.11), ios(9.0));
    /// 在iOS上默认为NO,标识不允许链接预览
    @property (nonatomic) BOOL allowsLinkPreview API_AVAILABLE(macosx(10.11), ios(9.0));
    /// 滚动视图
    @property (nonatomic,readonly, strong) UIScrollView *scrollView;
    /// 是否支持放大手势,默认为NO
    @property (nonatomic) BOOL allowsMagnification;
    /// 放大因子,默认为1
    @property (nonatomic) CGFloat magnification;
    /// 据设置的缩放因子来缩放页面,并居中显示结果在指定的点

    - (void)setMagnification:(CGFloat)magnification centeredAtPoint:(CGPoint)point;/// 证书列表@property (nonatomic,readonly, copy) NSArray *certificateChain API_DEPRECATED_WITH_REPLACEMENT("serverTrust", macosx(10.11,10.12), ios(9.0,10.0));

    四、WKWebView的使用
    简单使用,直接加载url地址

    WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.bounds];
    [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://developer.apple.com/reference/webkit"]]];
    [self.view addSubview:webView];

    自定义配置
    再WKWebView里面注册供JS调用的方法,是通过WKUserContentController类下面的方法:

    - (void)addScriptMessageHandler:(id )scriptMessageHandler name:(NSString *)name;

    // 创建配置
    WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];

    // 创建UserContentController(提供JavaScript向webView发送消息的方法)
    WKUserContentController* userContent = [[WKUserContentController alloc] init];

    // 添加消息处理,注意:self指代的对象需要遵守WKScriptMessageHandler协议,结束时需要移除
    [userContent addScriptMessageHandler:self name:@"NativeMethod"];

    // 将UserConttentController设置到配置文件
    config.userContentController = userContent;

    // 高端的自定义配置创建WKWebView
    WKWebView *webView = [[WKWebView alloc] initWithFrame:[UIScreen mainScreen].bounds configuration:config];
    // 设置访问的
    URLNSURL *url = [NSURL URLWithString:@"https://developer.apple.com/reference/webkit"];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    [webView loadRequest:request];
    [self.view addSubview:webView];

    // 实现WKScriptMessageHandler协议方法

    - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {

      // 判断是否是调用原生的
    if([@"NativeMethod" isEqualToString:message.name]) {
    // 判断message的内容,然后做相应的操作
    if([@"close" isEqualToString:message.body]) {
    }
    }
    }

    注意:上面将当前ViewController设置为MessageHandler之后需要在当前ViewController销毁前将其移除,否则会造成内存泄漏。

    [self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"NativeMethod"];

    五、WKNavigationDelegate代理方法
    如果实现了代理方法,一定要在decidePolicyForNavigationAction和decidePolicyForNavigationResponse方法中的回调设置允许跳转。

    typedef NS_ENUM(NSInteger, WKNavigationActionPolicy) {

    WKNavigationActionPolicyCancel, // 取消跳转

    WKNavigationActionPolicyAllow, // 允许跳转

    } API_AVAILABLE(macosx(10.10), ios(8.0));

    1.在发送请求之前,决定是否跳转

    - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void(^)(WKNavigationActionPolicy))decisionHandler {

    NSLog(@"1-------在发送请求之前,决定是否跳转 -->%@",navigationAction.request);

    decisionHandler(WKNavigationActionPolicyAllow);
    }

    2. 页面开始加载时调用

    - (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation {

    NSLog(@"2-------页面开始加载时调用");
    }

    3.在收到响应后,决定是否跳转

    - (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void(^)(WKNavigationResponsePolicy))decisionHandler {
    /// 在收到服务器的响应头,根据response相关信息,决定是否跳转。decisionHandler必须调用,来决定是否跳转,参数WKNavigationActionPolicyCancel取消跳转,WKNavigationActionPolicyAllow允许跳转    NSLog(@"3-------在收到响应后,决定是否跳转");

    decisionHandler(WKNavigationResponsePolicyAllow);

    4. 当内容开始返回时调用

    - (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation {

    NSLog(@"4-------当内容开始返回时调用");
    }

    5 页面加载完成之后调用

    - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {

    NSLog(@"5-------页面加载完成之后调用");
    }

    6 页面加载失败时调用

    - (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation {

    NSLog(@"6-------页面加载失败时调用");
    }

    7.接收到服务器跳转请求之后调用

    - (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation {

    NSLog(@"-------接收到服务器跳转请求之后调用");
    }

    8.数据加载发生错误时调用

    - (void)webView:(WKWebView *)webView didFailNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error {

    NSLog(@"----数据加载发生错误时调用");
    }

    9.需要响应身份验证时调用 同样在block中需要传入用户身份凭证

    - (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void(^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential * _Nullable credential))completionHandler {

    //用户身份信息 NSLog(@"----需要响应身份验证时调用 同样在block中需要传入用户身份凭证");

    NSURLCredential *newCred = [NSURLCredential credentialWithUser:@"" password:@"" persistence:NSURLCredentialPersistenceNone];

    // 为 challenge 的发送方提供 credential [[challenge sender] useCredential:newCred forAuthenticationChallenge:challenge];
    completionHandler(NSURLSessionAuthChallengeUseCredential,newCred);

    }

    10.进程被终止时调用

    - (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView {

    NSLog(@"----------进程被终止时调用");
    }

    六、WKUIDelegate代理方法

    /**
    * web界面中有弹出警告框时调用
    *
    * @param webView 实现该代理的webview
    * @param message 警告框中的内容
    * @param completionHandler 警告框消失调用
    */

    - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(void(^)())completionHandler {

    NSLog(@"-------web界面中有弹出警告框时调用");
    }


    * 创建新的webView时调用的方法

    - (nullable WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures {

    NSLog(@"-----创建新的webView时调用的方法");

    return webView;

    }

    // 关闭webView时调用的方法

    - (void)webViewDidClose:(WKWebView *)webView {

    NSLog(@"----关闭webView时调用的方法");

    }

    // 下面这些方法是交互JavaScript的方法

    // JavaScript调用confirm方法后回调的方法 confirm是js中的确定框,需要在block中把用户选择的情况传递进去

    -(void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void(^)(BOOL))completionHandler {

    NSLog(@"%@",message);

    completionHandler(YES);

    }

    // JavaScript调用prompt方法后回调的方法 prompt是js中的输入框 需要在block中把用户输入的信息传入

    -(void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void(^)(NSString * _Nullable))completionHandler{

    NSLog(@"%@",prompt);

    completionHandler(@"123");

    }

    // 默认预览元素调用

    - (BOOL)webView:(WKWebView *)webView shouldPreviewElement:(WKPreviewElementInfo *)elementInfo {

    NSLog(@"-----默认预览元素调用");

    return YES;

    }

    // 返回一个视图控制器将导致视图控制器被显示为一个预览。返回nil将WebKit的默认预览的行为。

    - (nullable UIViewController *)webView:(WKWebView *)webView previewingViewControllerForElement:(WKPreviewElementInfo *)elementInfo defaultActions:(NSArray> *)previewActions {

    NSLog(@"----返回一个视图控制器将导致视图控制器被显示为一个预览。返回nil将WebKit的默认预览的行为。");

    return self;

    }

    // 允许应用程序向它创建的视图控制器弹出

    - (void)webView:(WKWebView *)webView commitPreviewingViewController:(UIViewController *)previewingViewController {

    NSLog(@"----允许应用程序向它创建的视图控制器弹出");

    }

    // 显示一个文件上传面板。completionhandler完成处理程序调用后打开面板已被撤销。通过选择的网址,如果用户选择确定,否则为零。如果不实现此方法,Web视图将表现为如果用户选择了取消按钮。

    - (void)webView:(WKWebView *)webView runOpenPanelWithParameters:(WKOpenPanelParameters *)parameters initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void(^)(NSArray * _Nullable URLs))completionHandler {

    NSLog(@"----显示一个文件上传面板");

    }


    摘自作者:Cooci_和谐学习_不急不躁
    原贴链接:https://www.jianshu.com/p/1fd78ec144bb

    收起阅读 »

    EaseIMKit如何设置昵称、头像

    参考截图:
    1、聊天页面




    2、会话列表




    Fastlane 自动打包技术

    Fastlane是一套使用Ruby写的自动化工具集,旨在简化Android和iOS的部署过程,自动化你的工作流。它可以简化一些乏味、单调、重复的工作,像截图、代码签名以及发布AppGithub官网文档我认为我们在选择一些三方开源库或是工具的前提是:可以满足我们...
    继续阅读 »

    Fastlane是一套使用Ruby写的自动化工具集,旨在简化Android和iOS的部署过程,自动化你的工作流。它可以简化一些乏味、单调、重复的工作,像截图、代码签名以及发布App

    Github

    官网

    文档

    我认为我们在选择一些三方开源库或是工具的前提是:可以满足我们当下的需求并且提供好的扩展性, 无疑对我而言Fastlane做到了。我当前项目的需求主要是下面几方面:

    1.一行命令实现打包工作,不需要时时等待操作下一步,节省打包的时间去做其他的事。

    2.避免频繁修改配置导致可能出现的Release/Debug环境错误,如果没有检查机制,那将是灾难,即使有检查机制,我们也不得不重新打包,浪费了一次打包时间。毕竟人始终没有程序可靠,可以告别便利贴了。

    3.通过配置自动上传到蒲公英,fir.im内测平台进行测试分发,也可以直接上传到TestFlight,iTunes Connect

    4.证书的同步更新,管理,在新电脑能够迅速具备项目打包环境。

    如果你也有上述需求,那我相信Fastlane是一个好的选择。

    多说无益,开始上手

    一、安装xcode命令行工具
    xcode-select --install,如果没有安装,会弹出对话框,点击安装。

    如果提示xcode-select: error: command line tools are already installed, use "Software Update" to install updates表示已经安装

    二、安装Fastlane
    sudo gem install fastlane -NV或是brew cask install fastlane我这里使用gem安装的

    安装完了执行fastlane --version,确认下是否安装完成和当前使用的版本号。

    三、初始化Fastlane
    cd到你的项目目录执行

    fastlane init

    这里会弹出四个选项,问你想要用Fastlane做什么? 之前的老版本是不用选择的。选几都行,后续我们自行根据需求完善就可以,这里我选的是3。

    如果你的工程是用cocoapods的那么可能会提示让你勾选工程的Scheme,步骤就是打开你的xcode,点击Manage Schemes,在一堆三方库中找到你的项目Scheme,在后面的多选框中进行勾选,然后rm -rf fastlane文件夹,重新fastlane init一下就不会报错了。


    接着会提示你输入开发者账号和密码。

    [20:48:55]: Please enter your Apple ID developer credentials
    [20:48:55]: Apple ID Username:

    登录成功后会提示你是否需要下载你的App的metadata。点y等待就可以。

    如果报其他错的话,一般会带有github的相似的Issues的链接,里面一般都会有解决方案。

    四、文件系统

    初始化成功后会在当前工程目录生成一个fastlane文件夹,文件目录为下。

    其中metadata和screenshots分别对应App元数据和商店应用截图。

    Appfile主要存放App的apple_id team_id app_identifier等信息

    Deliverfile中为发布的配置信息,一般情况用不到。

    Fastfile是我们最应该关注的文件,也是我们的工作文件。

    Fastfile


    之前我们了解了action,那action的组合就是一个lane,打包到蒲公英是一个lane,打包到应用商店是一个lane,打包到testflight也是一个lane。可能理解为任务会好一些。

    打包到蒲公英
    这里以打包上传到蒲公英为例子,实现我们的一行命令自动打包。

    蒲公英在Fastlane是作为一个插件存在的,所以要打包到蒲公英必须先安装蒲公英的插件。

    打开终端输入fastlane add_plugin pgyer

    更多信息查看蒲公英文档

    新建一个lane

    desc "打包到pgy"
    lane :test do |options|
    gym(
    clean:true, #打包前clean项目
    export_method: "ad-hoc", #导出方式
    scheme:"shangshaban", #scheme
    configuration: "Debug",#环境
    output_directory:"./app",#ipa的存放目录
    output_name:get_build_number()#输出ipa的文件名为当前的build号
    )
    #蒲公英的配置 替换为自己的api_key和user_key
    pgyer(api_key: "xxxxxxx", user_key: "xxxxxx",update_description: options[:desc])
    end

    这样一个打包到蒲公英的lane就完成了。

    option用于接收我们的外部参数,这里可以传入当前build的描述信息到蒲公英平台

    执行

    在工作目录的终端执行

    fastlane test desc:测试打包


    然后等待就好了,打包成功后如果蒲公英绑定了微信或是邮箱手机号,会给你发通知的,当然如果是单纯的打包或是打包到其他平台, 你也可以使用fastlane的notification的action集进行自定义配置。

    其他的一些配置大家可以自己组合摸索一下,这样会让你对它更为了解

    其他的一些小提示

    1.可以在before_all中做一些前置操作,比如进行build号的更新,我个人建议不要对Version进行自动修改,可以作为参数传递进来。

    2.如果ipa包存放的文件夹为工作区,记得在.gitignore中进行忽略处理,我建议把fastlane文件也进行忽略,否则回退版本打包时缺失文件还需要手动打包。

    3.如果你的Apple ID在登录时进行了验证码验证,那么需要设置一个专业密码供fastlane上传使用,否则是上传不上去的。

    4.如果你们的应用截图和Metadata信息是运营人员负责编辑和维护的,那么在打包到AppStore时,记得要忽略截图和元数据,否则有可能因为不一致而导致覆盖。skip_metadata:true, #不上传元数据 skip_screenshots:true,#不上传屏幕截图

    关于fastlane的一些想法
    其实对于很多小团队来说,fastlane就可以简化很多操作,提升一些效率,但是还不够极致,因为我们没有打通Git环节,测试环节,反馈环节等,fastlane只是处于开发中的一环。许多团队在进行Jenkins或是其他的CI的尝试来摸索适合自己的工作流。但是也不要盲目跟风,从需求出发切合实际就好,找到痛点才能找到止痛药!

    摘自作者:Cooci_和谐学习_不急不躁
    原贴链接:https://www.jianshu.com/p/59725c52e0fa

    收起阅读 »

    iOS 常见面试题总结及答案(4)

    一.OC对象的内存管理机制?在iOS中,使用引用计数来管理OC对象的内存一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1内存管...
    继续阅读 »

    一.OC对象的内存管理机制?

    在iOS中,使用引用计数来管理OC对象的内存

    一个新创建的OC对象引用计数默认是1,当引用计数减为0,OC对象就会销毁,释放其占用的内存空间
    调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1
    内存管理的经验总结

    当调用alloc、new、copy、mutableCopy方法返回了一个对象,在不需要这个对象时,要调用release或者autorelease来释放它
    想拥有某个对象,就让它的引用计数+1;不想再拥有某个对象,就让它的引用计数-1
    可以通过以下私有函数来查看自动释放池的情况

    extern void _objc_autoreleasePoolPrint(void);

    二.内存区域分布

    在iOS开发过程中,为了合理的分配有限的内存空间,将内存区域分为五个区,由低地址向高地址分类分别是:代码区、常量区、全局静态区、堆、栈。

    代码段 -- 程序编译产生的二进制的数据
    常量区 -- 存储常量数据,通常程序结束后由系统自动释放
    全局静态区 -- 全局区又可分为未初始化全局区:.bss段和初始化全局区:data段。全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域,在程序结束后有系统释放。
    堆(heap) -- 程序运行过程中,动态分配的内存
    栈(stack) -- 存放局部变量,临时变量

    三.堆区和栈取的区别

    按管理方式分

    对于栈来讲,是由系统编译器自动管理,不需要程序员手动管理
    对于堆来讲,释放工作由程序员手动管理,不及时回收容易产生内存泄露
    按分配方式分

    堆是动态分配和回收内存的,没有静态分配的堆
    栈有两种分配方式:静态分配和动态分配
    静态分配是系统编译器完成的,比如局部变量的分配
    动态分配是有alloc函数进行分配的,但是栈的动态分配和堆是不同的,它的动 态分配也由系统编译器进行释放,不需要程序员手动管理

    四.怎么保证多人开发进行内存泄露的检查

    1.使用Analyze进行代码的静态分析
    2.使用leaks 进行内存泄漏检测
    3.使用一些三方工具(DoraemonKit/WithMLeaksFinder)

    五.内存泄漏可能会出现的几种原因?

    第一种可能:第三方框架不当使用;
    第二种可能:block循环引用;
    第三种可能:delegate循环引用;
    第四种可能:NSTimer循环引用
    第五种可能:非OC对象内存处理
    第六种可能:地图类处理
    第七种可能:大次数循环内存暴涨

    六.什么是Tagged Pointer?

    1.从64bit开始,iOS引入了Tagged Pointer技术,用于优化NSNumber、NSDate、NSString等小对象的存储
    在没有使用Tagged Pointer之前, NSNumber等对象需要动态分配内存、维护引用计数等,NSNumber指针存储的是堆中NSNumber对象的地址值
    2.使用Tagged Pointer之后,NSNumber指针里面存储的数据变成了:Tag + Data,也就是将数据直接存储在了指针中
    3.当指针不够存储数据时,才会使用动态分配内存的方式来存储数据

    七.copy和mutableCopy区别


    八.AutoreleasePoolPage的结构?以及如何 push 和 pop 的

    AutoreleasePool(自动释放池)其实并没有自身的结构,他是基于多个AutoreleasePoolPage(一个C++类)以双向链表组合起来的结构; 可以通过 push操作添加对象,pod 操作弹出对象,以及通过 release 操作释放对象;


    调用push方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址

    调用pop方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY

    id *next指向了下一个能存放autorelease对象地址的区域

    九.Autoreleasepool 与 Runloop 的关系

    主线程默认为我们开启 Runloop,Runloop 会自动帮我们创建Autoreleasepool,并进行Push、Pop 等操作来进行内存管理
    iOS在主线程的Runloop中注册了2个Observer

    第1个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush()
    第2个Observer 监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush()监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()

    十.什么是多线程?

    多线程是指实现多个线程并发执行的技术,进而提升整体处理性能。

    同一时间,CPU 只能处理一条线程,多线程并发执行,其实是 CPU 快速的在多条线程之间调度(切换)如果 CPU 调度线程的时间足够快, 就造成了多线程并发执行的假象

    主线程的栈区 空间大小为1M,非常非常宝贵

    子线程的栈区 空间大小为512K内存空间

    优势
    充分发挥多核处理器的优势,将不同线程任务分配给不同的处理器,真正进入“并行计算”状态

    弊端
    新线程会消耗内存控件和cpu时间,线程太多会降低系统运行性能。

    十一.iOS的多线程方案有哪几种?


    十二,讲一下GCD

    GCD(Grand Central Dispatch), 又叫做大中央调度, 它对线程操作进行了封装,加入了很多新的特性,内部进行了效率优化,提供了简洁的C语言接口, 使用更加高效,也是苹果推荐的使用方式.

    GCD 的队列

    1.并发队列(Concurrent Dispatch Queue)
    可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)
    并发功能只有在异步(dispatch_async)函数下才有效

    2.串行队列(Serial Dispatch Queue)
    让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务),按照FIFO顺序执行.

    同步和异步任务

    GCD多线程经常会使用 dispatch_sync和dispatch_async函数向指定队列添加任务,分别是同步和异步

    同步指阻塞当前线程,既要等待添加的耗时任务块Block完成后,函数才能返回,后面的代码才能继续执行

    异步指将任务添加到队列后,函数立即返回,后面的代码不用等待添加的任务完成后即可执行,异步提交无法确定任务执行顺序

    相关常用函数使用:

    1.dispatch_after使用 (通过该函数可以让提交的任务在指定时间后开始执行,也就是延迟执行;)

    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(10 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    NSLog(@"10秒后开始执行")
    });

    2.dispatch_group_t (组调度)的使用 (组调度可以实现等待一组操都作完成后执行后续任务.)

    dispatch_group_t group = dispatch_group_create();
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    //请求1
    });
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    //请求2
    });
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    //请求3
    });
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
    //界面刷新
    NSLog(@"任务均完成,刷新界面");
    });

    3.dispatch_semaphore (信号量)如何使用?

    用于控制最大并发数     可以防止资源抢夺

    与他相关的共有三个函数,分别是:

    dispatch_semaphore_create,  // 创建最大并发数
    dispatch_semaphore_wait。 // -1 开始执行 (0则等待)
    dispatch_semaphore_signal, // +1

    4.dispatch_barrier_(a)sync使用?

    一个dispatch barrier 允许在一个并发队列中创建一个同步点。当在并发队列中遇到一个barrier, 他会延迟执行barrier的block,等待所有在barrier之前提交的blocks执行结束。 这时,barrier block自己开始执行。 之后, 队列继续正常的执行操作。

    十三.什么是NSOperation?

    1.NSOperation是基于GCD的上封装,将线程封装成要执行的操作,不需要管理线程的生命周期和同步,比GCD可控性更强

    例如:
    可以加入操作依赖控制执行顺序,设置操作队列最大并发数,取消操作等

    2.NSOperation如何实现操作依赖

    通过任务间添加依赖,可以为任务设置执行的先后顺序。接下来通过一个案例来展示设置依赖的效果。

    NSOperationQueue *queue=[[NSOperationQueue alloc] init];
    //创建操作
    NSBlockOperation *operation1=[NSBlockOperation blockOperationWithBlock:^(){
    NSLog(@"执行第1次操作,线程:%@",[NSThread currentThread]);
    }];
    NSBlockOperation *operation2=[NSBlockOperation blockOperationWithBlock:^(){
    NSLog(@"执行第2次操作,线程:%@",[NSThread currentThread]);
    }];
    NSBlockOperation *operation3=[NSBlockOperation blockOperationWithBlock:^(){
    NSLog(@"执行第3次操作,线程:%@",[NSThread currentThread]);
    }];
    //添加依赖
    [operation1 addDependency:operation2];
    [operation2 addDependency:operation3];
    //将操作添加到队列中去
    [queue addOperation:operation1];
    [queue addOperation:operation2];
    [queue addOperation:operation3];

    十四.在项目什么时候选择使用 GCD,什么时候选 择 NSOperation

    项目中使用 NSOperation 的优点是 NSOperation 是对线程的高度抽象,在项目中使 用它,会使项目的程序结构更好,子类化 NSOperation 的设计思路,是具有面向对 象的优点(复用、封装),使得实现是多线程支持,而接口简单,建议在复杂项目中 使用。

    项目中使用 GCD 的优点是 GCD 本身非常简单、易用,对于不复杂的多线程操 作,会节省代码量,而 Block 参数的使用,会是代码更为易读,建议在简单项目中 使用。

    区别,以及各自的优势

    GCD是纯C语⾔言的API,NSOperationQueue是基于GCD的OC版本封装

    GCD只⽀支持FIFO的队列列,NSOperationQueue可以很⽅方便便地调整执⾏行行顺 序、设 置最⼤大并发数量量

    NSOperationQueue可以在轻松在Operation间设置依赖关系,⽽而GCD 需要写很 多的代码才能实现

    NSOperationQueue⽀支持KVO,可以监测operation是否正在执⾏行行 (isExecuted)、 是否结束(isFinished),是否取消(isCanceld)

    GCD的执⾏行行速度⽐比NSOperationQueue快 任务之间不不太互相依赖:GCD 任务之间 有依赖\或者要监听任务的执⾏行行情况:NSOperationQueue

    十五.线程安全的处理手段有哪些,线程锁都有哪些?

    1.加锁

    2.同步执行

    线程锁 (我们在使用多线程的时候多个线程可能会访问同一块资源,这样就很容易引发数据错乱和数据安全等问题,这时候就需要我们保证每次只有一个线程访问这一块资源,锁 应运而生。)

    1.OSSpinLock (自旋锁)

    注:苹果爸爸已经在iOS10.0以后废弃了这种锁机制,使用os_unfair_lock 替换,顾名思义能够保证不同优先级的线程申请锁的时候不会发生优先级反转问题.

    2.os_unfair_lock(自旋锁)

    3.dispatch_semaphore (信号量)

    4.pthread_mutex(互斥锁)

    5.NSLock(互斥锁、对象锁)

    6.NSCondition(条件锁、对象锁)

    7.NSConditionLock(条件锁、对象锁)

    8.NSRecursiveLock(递归锁、对象锁)

    9.@synchronized(条件锁)

    10.pthread_mutex(recursive)(递归锁) 

    注.递归锁可以被同一线程多次请求,而不会引起死锁。即在同一线程中在未解锁之前还可以上锁, 执行锁中的代码。这主要是用在循环或递归操作中

    性能图


    十六.HTTPS连接过程简述

    1.客户端向服务端发起 https 请求

    2.服务器(需要申请 ca 证书),返回证书(包含公钥)给客户端

    3.客户端使用根证书验证 服务器证书的有效性,进行身份确认

    4.客户端生成对称密钥,通过公钥进行密码,发送给服务器

    5.服务器使用私钥进行 解密,获取对称密钥

    6.双方使用对称加密的数据进行通信

    十七.http 与https区别

    HTTPS和HTTP的区别主要为以下四点:

    1.https协议需要到ca申请证书,一般免费证书很少,需要交费。

    2.http是超文本传输协议,信息是明文传输,https 则是具有安全性的ssl加密传输协议。

    3.http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。

    4.http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全

    十八.什么是DNS?DNS劫持问题?

    域名系统(DomainNameSystem,缩写:DNS)是[互联网]的一项服务。它作为将域名和IP地址相互映射的一个分布式数据库,能够使人更方便地访问[互联网]

    DNS劫持又称(域名劫持), 是指在劫持的网络范围内拦截域名解析的请求,分析请求的域名,把审查范围以外的请求放行,否则返回假的IP地址或者什么都不做使请求失去响应,其效果就是对特定的网络不能访问或访问的是假网址。

    解决办法: 使用HTTPDNS

    十九.网络七层是什么?

    OSI模型有7层结构,每层都可以有几个子层。 OSI的7层从上到下分别是 7 应用层 6 表示层 5 会话层 4 传输层 3 网络层 2 数据链路层 1 物理层 ;其中高层(即7、6、5、4层)定义了应用程序的功能,下面3层(即3、2、1层)主要面向通过网络的端到端的数据流。

    1.应用层
    网络服务与最终用户的一个接口。
    协议有:HTTP FTP TFTP SMTP SNMP DNS TELNET HTTPS POP3 DHCP

    2.表示层
    数据的表示、安全、压缩。(在五层模型里面已经合并到了应用层)
    格式有,JPEG、ASCll、DECOIC、加密格式等

    3 .会话层
    建立、管理、终止会话。(在五层模型里面已经合并到了应用层)
    对应主机进程,指本地主机与远程主机正在进行的会话

    4.传输层
    定义传输数据的协议端口号,以及流控和差错校验。
    协议有:TCP UDP,数据包一旦离开网卡即进入网络传输层

    5.网络层
    进行逻辑地址寻址,实现不同网络之间的路径选择。
    协议有:ICMP IGMP IP(IPV4 IPV6) ARP RARP

    6.数据链路层
    建立逻辑连接、进行硬件地址寻址、差错校验 [2] 等功能。(由底层网络定义协议)
    将比特组合成字节进而组合成帧,用MAC地址访问介质,错误发现但不能纠正。

    7.物理层
    建立、维护、断开物理连接。(由底层网络定义协议)

    二十.项目中网络层如何做安全处理

    1.尽量使用https

    2.不要传输明文密码

    3.Post并不比Get安全

    4.不要使用301跳转

    5.http请求都带上MAC

    6.http请求使用临时密钥

    7.AES使用CBC模式







    收起阅读 »

    ios加固,ios代码混淆,ios代码混淆工具, iOS源码混淆使用说明详解

    ios加固,ios代码混淆,ios代码混淆工具,iOS源码混淆产品是一款纯离线的源码加密工具,主要用于保护iOS项目中的核心代码,避免因逆向工程或破解,造成核心技术被泄漏、代码执行流程被分析等安全问题。该加密工具和普通编译器相似,基于项目源代码可将Object...
    继续阅读 »

    ios加固,ios代码混淆,ios代码混淆工具,iOS源码混淆产品是一款纯离线的源码加密工具,主要用于保护iOS项目中的核心代码,避免因逆向工程或破解,造成核心技术被泄漏、代码执行流程被分析等安全问题。该加密工具和普通编译器相似,基于项目源代码可将Objective-C、Swift、C、C++代码编译成二进制代码,不同之处在于,加密工具在编译时,能够对代码采取混淆、字符串加密等安全措施。从而避免攻击者通过IDA Pro等逆向工具反编译二进制代码,分析业务代码执行流程,进一步篡改或窃取核心技术。

    概述

    本文主要介绍iOS源码混淆产品之Xcode插件的使用方式,阅读者需具备iOS开发经验,否则使用可能存在困难。

    安装插件

    v13.0.2-20190703及其之前的版本为替换clang编译器的模式,之后版本为切换Xcode -> Toolchains的模式,后者可以在Xcode中快速切换编译器。

    Xcode插件通过执行python install.py 命令安装编译器,使用完成后执行 python uninstal.py 即可卸载编译器。如下图:

    (备注:如果有多个Xcode版本,请修改configuration.txt文件中Xcode默认的路径。)


    执行安装会提示输入密码,输入电脑开机密码即可,Xcode插件安装成功后会有Install Success提示,如下图:


    引入头文件

    将include目录下的KiwiOBF.h头文件拷贝到iOS项目中,并在需的地方进行引用即可。

    添加KIWIOBF标签

    对需要进行混淆保护的函数,添加KIWIOBF标签,以告知编译器该函数需要进行混淆编译。如下图:


    设置参数

    全编译器有默认混淆参数,如不能满足需求,可以自定义配置参数
    加密参数说明


    iOS项目的混淆参数在 Other C Flags,Other C++ Flags,Other Swift Flags中设置,如下图:


    卸载插件

    Xcode插件:执行 python uninstall.py 即可卸载编译器。

    友情告知地址,ios代码混淆,ios加固:https://www.kiwisec.com/product/compiler-ios.html

    转自:https://www.jianshu.com/p/7fdb4544c916

    收起阅读 »

    iOS 常见面试题总结及答案(3)

    一.列举出延迟调用的几种方法?1.performSelector方法 [self performSelector:@selector(Delay) withObject:nil afterDelay:3.0f];2.NSTimer定时器  [NSTimer s...
    继续阅读 »

    一.列举出延迟调用的几种方法?

    1.performSelector方法 

    [self performSelector:@selector(Delay) withObject:nil afterDelay:3.0f];

    2.NSTimer定时器  

    [NSTimer scheduledTimerWithTimeInterval:3.0f target:self selector:@selector(Delay) userInfo:nil repeats:NO];

    3.sleepForTimeInterval

    [NSThread sleepForTimeInterval:3.0f];

    4.GCD方式

    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC));
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
    [self Delay];
    });
    - (void)Delay {
    NSLog(@"执行");
    }

    二.NSCache 和NSDictionary 区别?

    NSCache可以提供自动删减缓存功能,而且保证线程安全,与字典不同,不会拷贝键。
    NSCache可以设置缓存上限,限制对象个数和总缓存开销。定义了删除缓存对象的时机。这个机制只对NSCache起到指导作用,不会一定执行。
    NSPurgeableData搭配NSCache使用,可以自动清除数据。
    只有那种“重新计算很费劲”的数据才值得放入缓存。

    三.NSArray 和 NSSet区别

    NSSet和NSArray功能性质一样,用于存储对象,属于集合。
    NSSet属于 “无序集合”,在内存中存储方式是不连续
    NSArray是 “有序集合” 它内存中存储位置是连续的。
    NSSet,NSArray都是类,只能添加对象,如果需要加入基本数据类型(int,float,BOOL,double等),需要将数据封装成NSNumber类型。
    由于NSSet是用hash实现的所以就造就了它查询速度比较快,但是我们不能把某某对象存在第几个元素后面之类的有关下标的操作。

    四.什么是分类?

    分类: 在不修改原有类代码的情况下,可以给类添加方法
    Categroy 给类扩展方法,或者关联属性, Categroy底层结构也是一个结构体:内部存储这结构体的名字,那个类的分类,以及对象和类方法列表,协议,属性信息
    通过Runtime加载某个类的所有Category数据
    把所有Category的方法、属性、协议数据,合并到一个大数组中后面参与编译的Category数据,会在数组的前面
    将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面

    五.为什么说OC是一门动态语言?

    动态语言:是指程序在运行时可以改变其结构,新的函数可以被引进,已有的函数可以被删除等在结构上的变化
    动态类型语言: 就是类型的检查是在运行时做的。
    OC的动态特性可从三方面:

    动态类型(Dynamic typing):最终判定该类的实例类型是在运行期间
    动态绑定(Dynamic binding):在运行时确定调用的方法
    动态加载(Dynamic loading):在运行期间加载需要的资源或可执行代码

    六.什么是动态绑定?

    动态绑定 将调用方法的确定也推迟到运行时。OC可以先跳过编译,到运行的时候才动态地添加函数调用,在运行时才决定要调用什么方法,需要传什么参数进去,这就是动态绑定。
    在编译时,方法的 调用并不和代码绑定在一起,只有在消实发送出来之后,才确定被调用的代码。通过动态类型和动态绑定技术,

    七.什么是谓词?

    谓词(NSPredicate)是OC针对数据集合的一种逻辑帅选条件,类似一个过滤器,简单实实用代码如下:

    Person * p1 = [Person personWithName:@"alex" Age:20];
    Person * p2 = [Person personWithName:@"alex1" Age:30];
    Person * p3 = [Person personWithName:@"alex2" Age:10];
    Person * p4 = [Person personWithName:@"alex3" Age:40];
    Person * p5 = [Person personWithName:@"alex4" Age:80];

    NSArray * persons = @[p1, p2, p3, p4, p5];
    //定义谓词对象,谓词对象中包含了过滤条件
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"age < 30"];
    //使用谓词条件过滤数组中的元素,过滤之后返回查询的结果
    NSArray *array = [persons filteredArrayUsingPredicate:predicate];

    八.什么是类工厂方法?

    类工厂方法就是用来快速创建对象的类方法, 他可以直接返回一个初始化好的对象,具备以下特征:

    一定是类方法
    返回值需要是 id/instancetype 类型
    规范的方法名说说明类工厂方法返回的是一个什么对象,一般以类名首字母小写开始;
    比如系统 UIButton 的buttonWithType 就是一个类工厂方法:

    // 类工厂方法
    + (instancetype)buttonWithType:(UIButtonType)buttonType;
    // 使用
    + UIButton * button = [UIButton buttonWithType:UIButtonTypeCustom];

    九.简要说明const,宏,static,extern区分以及使用?

    1.const

    const常量修饰符,经常使用的字符串常量,一般是抽成宏,但是苹果不推荐我们抽成宏,推荐我们使用const常量。

    - const 作用:限制类型
    - 使用const修饰基本变量, 两种写法效果一致 , b都是只读变量
    const int b = 5;
    int const b = 5;
    - 使用const修饰指针变量的变量
    第一种: const int *p = &a 和 int const *q = &a; 效果一致,*p 的值不能改,p 的指向可以改;
    第二种: int * const p = &a; 表示 p 的指向不能改,*p 的值可以改
    第三种:
    const int * const p = &a; *p 值和 p 的指向都不能改

    const 在*左边, 指向可变, 值不可变
    const 在*的右边, 指向不可变, 值可变
    const 在*的两边, 都不可变

    2.

    * 基本概念:宏是一种批量处理的称谓。一般说来,宏是一种规则或模式,或称语法替换 ,用于说明某一特定输入(通常是字符串)如何根据预定义的规则转换成对应的输出(通常也是字符串)。这种替换在预编译时进行,称作宏展开。编译器会在编译前扫描代码,如果遇到我们已经定义好的宏那么就会进行代码替换,宏只会在内存中copy一份,然后全局替换,宏一般分为对象宏和函数宏。 宏的弊端:如果代码中大量的使用宏会使预编译时间变长。

    const与宏的区别?

    * 编译检查 宏没有编译检查,const有编译检查;
    * 宏的好处 定义函数,方法 const不可以;
    * 宏的坏处 大量使用宏,会导致预编译时间过长

    3.static

    * 修饰局部变量: 被static修饰局部变量,延长生命周期,跟整个应用程序有关,程序结束才会销毁,被 static 修饰局部变量,只会分配一次内存
    * 修饰全局变量: 被static修饰全局变量,作用域会修改,也就是只能在当前文件下使用

    4.extern

    声明外部全局变量(只能用于声明,不能用于定义)

    常用用法(.h结合extern联合使用)
    如果在.h文件中声明了extern全局变量,那么在同一个类中的.m文件对全局变量的赋值必须是:数据类型+变量名(与声明一致)=XXXX结构。并且在调用的时候,必须导入.h文件。代码如下:

    .h
    @interface ExternModel : NSObject
    extern NSString *lhString;
    @end
    .m
    @implementation ExternModel
    NSString *lhString=@"hello";
    @end

    调用的时候:例如:在viewController.m中调用,则可以引入:ExternModel.h,否则无法识别全局变量。当然也可以通过不导入头文件的方式进行调用(通过extern调用)。

    十.id类型, nil , Nil ,NULL和NSNULL的区别?

    id类型: 是一个独特的数据类型,可以转换为任何数据类型,id类型的变量可以存放任何数据类型的对象,在内部处理上,这种类型被定义为指向对象的指针,实际上是一个指向这种对象的实例变量的指针; id 声明的对象具有运行时特性,既可以指向任意类型的对象
    nil 是一个实例对象值;如果我们要把一个对象设置为空的时候,就用nil
    Nil 是一个类对象的值,如果我们要把一个class的对象设置为空的时候,就用Nil
    NULL 指向基本数据类型的空指针(C语言的变量的指针为空)
    NSNull 是一个对象,它用在不能使用nil的场合

    十一.C和 OC 如何混编&&Swift 和OC 如何调用?

    1.xcode可以识别一下几种扩展名文件:

    .m文件,可以编写 OC语言 和 C 语言代码
    .cpp: 只能识别C++ 或者C语言(C++兼容C)
    .mm: 主要用于混编 C++和OC代码,可以同时识别OC,C,C++代码

    2.Swift 调用 OC代码

    需要创建一个 Target-BriBridging-Header.h 的桥文件,在乔文件导入需要调用的OC代码头文件即可

    3.OC 调用 Swift代码
    直接导入 Target-Swift.h文件即可, Swift如果需要被OC调用,需要使用@objc 对方法或者属性进行修饰

    十二.OC与 JS交互方式有哪些?

    1.通过拦截URL

    - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    NSString *url = request.URL.absoluteString;
    if ([url rangeOfString:@"需要跳转源生界面的URL判断"].location != NSNotFound) {
    //跳转原生界面
    return NO;
    }
    return YES;
    }

    2.使用MessageHandler(WKWebView)

    当JS端想传一些数据给iOS.那它们会调用下方方法来发送.
    window.webkit.messageHandlers.<方法名>.postMessage(<数据>)上方代码在JS端写会报错,导致网页后面业务不执行.可使用try-catch执行.
    那么在OC中的处理方法如下.它是WKScriptMessageHandler的代理方法.name和上方JS中的方法名相对应.

    - (void)addScriptMessageHandler:(id )scriptMessageHandler name:(NSString *)name;

    3.JavaScriptCore (UIWebView)
    使用三方库WebViewJavascriptBridge,可提供 js 调OC,以及OC掉JS

    1. 设置 webViewBridge
    _bridge = [WKWebViewJavascriptBridge bridgeForWebView:self.webView];
    [_bridge setWebViewDelegate:self];
    2. 注册handler方法,需要和 前段协商好 方法名字,是供 JS调用Native 使用的。
    [_bridge registerHandler:@"scanClick" handler:^(id data, WVJBResponseCallback responseCallback) {
    // OC调用
    NSString *scanResult = @"http://www.baidu.com";
    // js 回调传参
    responseCallback(scanResult);
    }];
    3. OC掉用JS
    [_bridge callHandler:@"testJSFunction" data:@"一个字符串" responseCallback:^(id responseData) {
    NSLog(@"调用完JS后的回调:%@",responseData);
    }];

    4.OC调用JS代码

    // 直接运行 使用 
    NSString *jsStr = @"执行的JS代码";
    [webView stringByEvaluatingJavaScriptFromString:jsStr];

    // 使用JavaScriptCore框架
    #import
    - (void)webViewDidFinishLoad:(UIWebView *)webView {
    //获取webview中的JS内容
    JSContext *context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
    NSString *runJS = @"执行的JS代码";
    //准备执行的JS代码
    [context evaluateScript:runJS];
    }

    十三.编译过程做了哪些事情

    Objective,Swift都是编译语言。编译语言在执行的时候,必须先通过编译器生成机器码,机器码可以直接在CPU上执行,所以执行效率较高。Objective,Swift二者的编译都是依赖于Clang + LLVM. OC和Swift因为原理上大同小异,知道一个即可!
    1.iOS编译 不管是OC还是Swift,都是采用Clang作为编译器前端,LLVM(Low level vritual machine)作为编译器后端。
    2.编译器前端 :编译器前端的任务是进行:语法分析,语义分析,生成中间代码(intermediate representation )。在这个过程中,会进行类型检查,如果发现错误或者警告会标注出来在哪一行
    3.编译器后端 :编译器后端会进行机器无关的代码优化,生成机器语言,并且进行机器相关的代码优化。LVVM优化器会进行BitCode的生成,链接期优化等等,LLVM机器码生成器会针对不同的架构,比如arm64等生成不同的机器码。

    十四.Category的实现原理&&使用场合&&Class Extension的区别

    1.Category编译之后的底层结构是struct category_t,里面存储着分类的对象方法、类方法、属性、协议信息
    2.在程序运行的时候,runtime会将Category的数据,合并到类信息中(类对象、元类对象中)

    使用场合:

    在不修改原有类代码的情况下,为类添对象方法或者类方法
    或者为类关联新的属性
    分解庞大的类文件

    添加实例方法
    添加类方法
    添加协议
    添加属性
    关联成员变量

    区别

    Class Extension在编译的时候,它的数据就已经包含在类信息中
    Category是在运行时,才会将数据合并到类信息中。

    十五.Category能否添加成员变量?如果可以,如何给Category添加成员变量?

    不能直接给Category添加成员变量,但是可以间接实现Category有成员变量的效果
    Category是发生在运行时,编译完毕,类的内存布局已经确定,无法添加成员变量(Category的底层数据结构也没有成员变量的结构)
    可以通过 runtime 动态的关联属性

    十六.Category中有load方法吗?load方法是什么时候调用的?load 方法能继承吗?

    有load方法
    load方法在runtime加载类、分类的时候调用
    load方法可以继承,但是一般情况下不会主动去调用load方法,都是让系统自动调用

    十七.initialize方法如何调用,以及调用时机

    当类第一次收到消息的时候会调用类的initialize方法
    是通过 runtime 的消息机制 objc_msgSend(obj,@selector()) 进行调用的
    优先调用分类的 initialize, 如果没有分类会调用 子类的,如果子类未实现则调用 父类的

    十八.load、initialize方法的区别什么?它们在category中的调用的顺序?以及出现继承时他们之间的调用过程?

    load 是类加载到内存时候调用, 优先父类->子类->分类
    initialize 是类第一次收到消息时候调用,优先分类->子类->父类
    同级别和编译顺序有关系
    load 方法是在 main 函数之前调用的

    十九.什么是Runtime?平时项目中有用过么?

    Objective-C runtime是一个运行时库,它为Objective-C语言的动态特性提供支持,我们所写的OC代码在运行时都转成了runtime相关的代码,类转换成C语言对应的结构体,方法转化为C语言对应的函数,发消息转成了C语言对应的函数调用。通过了解runtime以及源码,可以更加深入的了解OC其特性和原理

    OC是一门动态性比较强的编程语言,允许很多操作推迟到程序运行时再进行

    OC的动态性就是由Runtime来支撑和实现的,Runtime是一套C语言的API,封装了很多动态性相关的函数

    平时编写的OC代码,底层都是转换成了Runtime API进行调用

    具体应用

    利用关联对象(AssociatedObject)给分类添加属性
    遍历类的所有成员变量(修改textfield的占位文字颜色、字典转模型、自动归档解档)
    交换方法实现(交换系统的方法)
    利用消息转发机制解决方法找不到的异常问题

    二十.讲一下 OC 的消息机制

    1.OC中的方法调用其实都是转成了objc_msgSend函数的调用,给receiver(方法调用者)发送了一条消息(selector方法名)
    2.objc_msgSend底层有3大阶段   消息发送(当前类、父类中查找)、动态方法解析、消息转发

    消息发送流程

    当我们的一个 receiver(实例对象)收到消息的时候, 会通过 isa 指针找到 他的类对象, 然后在类对象方法列表中查找 对应的方法实现,如果 未找到,则会通过 superClass 指针找到其父类的类对象, 找到则返回,未找打则会一级一级往上查到,最终到NSObject 对象, 如果还是未找到就会进行动态方法解析
    类方法调用同上,只不过 isa 指针找到元类对象;

    动态方法解析机制&&消息转发机制流程

    当我们发送消息未找到方法实现,就会进入第二步,动态方法解析: 代码实现如下

    //  动态方法绑定- 实例法法调用
    + (BOOL)resolveInstanceMethod:(SEL)sel{
    if (sel == @selector(run)) {
    Method method = class_getInstanceMethod(self, @selector(test));
    class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method));
    return YES;
    }
    return [super resolveInstanceMethod:sel];
    }
    // 类方法调用
    +(BOOL) resolveClassMethod:(SEL)sel....

    未找到动态方法绑定,就会进行消息转发阶段

    // 快速消息转发- 指定消息处理对象
    - (id)forwardingTargetForSelector:(SEL)aSelector{
    if (aSelector == @selector(run)) {
    return [Student new];
    }
    return [super forwardingTargetForSelector:aSelector];
    }

    // 标准消息转发-消息签名
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if(aSelector == @selector(run))
    {
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
    }
    - (void)forwardInvocation:(NSInvocation *)anInvocation{
    //内部逻辑自己处理
    }

    答案摘自作者:iOS猿_员

    原贴链接:https://www.jianshu.com/p/4aaf45c11082

    收起阅读 »

    TCP、UDP协议和IP协议

    一、TCP定义TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议。面向连接意味着两个使用TCP的进程(一个客户和一个服务器)在交换数据之前必须先建立好连接,然后才能开始传输数据。建立连接时采用客户服务器模式,其中主动发起连接建立的进程叫做客户(Clie...
    继续阅读 »
    一、TCP
    1. 定义

      TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议。面向连接意味着两个使用TCP的进程(一个客户和一个服务器)在交换数据之前必须先建立好连接,然后才能开始传输数据。建立连接时采用客户服务器模式,其中主动发起连接建立的进程叫做客户(Client),被动等待连接建立的进程叫做服务器(Server)。

    2. 端对端

      TCP提供全双工的数据传输服务,这意味着建立了TCP连接的主机双方可以同时发送和接收数据。这样,接收方收到发送方消息后的确认可以在反方向的数据流中进行捎带。“端到端”的TCP通信意味着TCP连接发生在两个进程之间,一个进程发送数据,只有一个接收方,因此TCP不支持广播和组播。

    3. 面向字节

      TCP连接面向字节流,字节流意味着用户数据没有边界,例如,发送进程在TCP连接上发送了2个512字节的数据,接收方接收到的可能是2个512字节的数据,也可能是1个1024字节的数据。因此,接收方若要正确检测数据的边界,必须由发送方和接收方共同约定,并且在用户进程中按这些约定来实现。

    4. 位于传输层
      TCP接收到数据包后,将信息送到更高层的应用程序,如FTP的服务程序和客户程序。应用程序处理后,再轮流将信息送回传输层,传输层再将它们向下传送到网际层,最后到接收方。


    二、UDP

    UDP与TCP位于同一层,但与TCP不同

    • UDP协议提供的是一种无连接的、不可靠的传输层协议,只提供有限的差错检验功能。

    • 它在IP层上附加了简单的多路复用功能,提供端到端的数据传输服务。

    • 设计UDP的目的是为了以最小的开销在可靠的或者是对数据可靠性要求不高的环境中进行通信,

    • 由于无连接,UDP支持广播和组播,这在多媒体应用中是非常有用的。


    三、IP协议

    1. 定义

      IP(网际)协议是TCP/IP模型的核心,也是网络层最重要的协议。

    2. 功能

      网际层接收来自网络接口层的数据包,并将数据包发送到传输层;相反,也将传输层的数据包传送到网络接口层。
      IP协议主要包括无连接数据报传送,数据报路由器选择以及差错处理等功能。

    3. 局限及对策

      由于网络拥挤、网络故障等问题可能导致数据报无法顺利通过传输层。IP协议具有有限的报错功能,不能有效处理数据报延迟,不按顺序到达和数据报出错,所以IP协议需要与另外的协议配套使用,包括地址解析协议ARP、逆地址解析协议RARP、因特网控制报文协议ICMP、因特网组管理协议IGMP等。
      IP数据包中含有源地址(发送它的主机地址)和目的地址(接收它的主机地址)。

    4. 意义

      IP协议对于网络通信而言有着重要的意义。由于网络中的所有计算机都安装了IP软件,使得许许多多的局域网构成了庞大而严密的通信系统,才形成了如今的Internet。其实,Internet并非一个真实存在的网络,而是一个虚拟网络,只不过是利用IP协议把世界上所有愿意接入Internet的计算机局域网络连接起来,使之能够相互通信。

      链接:https://www.jianshu.com/p/b8b2220a8bd0

    收起阅读 »

    iOS 一键返回首页

    在APP的开发中,我们难免会遇到这种情况,一层层的打开下一级控制,这时,我们再想回到原始控制器时,一级级返回不太现实,所以我们需要一种方法,来一次性返回首页从App的rootViewController开始,找到所有presentedController,然后...
    继续阅读 »

    在APP的开发中,我们难免会遇到这种情况,一层层的打开下一级控制,这时,我们再想回到原始控制器时,一级级返回不太现实,所以我们需要一种方法,来一次性返回首页

    从App的rootViewController开始,找到所有presentedController,然后逆序dismiss这些Controller,最后pop to rootViewController就可以了。

    - (void)backToHomePage
    {
    UIWindow *window = [(AppDelegate *)[UIApplication sharedApplication].delegate window];
    UIViewController *presentedController = nil;

    UIViewController *rootController = [window rootViewController];
    if ([rootController isKindOfClass:[UITabBarController class]]) {
    rootController = [(UITabBarController *)rootController selectedViewController];
    }
    presentedController = rootController;
    //找到所有presented的controller,包括UIViewController和UINavigationController
    NSMutableArray *presentedControllerArray = [[NSMutableArray alloc] init];
    while (presentedController.presentedViewController) {
    [presentedControllerArray addObject:presentedController.presentedViewController];
    presentedController = presentedController.presentedViewController;
    }
    if (presentedControllerArray.count > 0) {
    //把所有presented的controller都dismiss掉
    [self dismissControllers:presentedControllerArray topIndex:presentedControllerArray.count - 1 completion:^{
    [self popToRootViewControllerFrom:rootController];
    }];
    } else {
    [self popToRootViewControllerFrom:rootController];
    }
    }
    - (void)dismissControllers:(NSArray *)presentedControllerArray topIndex:(NSInteger)index completion:(void(^)(void))completion
    {
    if (index < 0) {
    completion();
    } else {
    [presentedControllerArray[index] dismissViewControllerAnimated:NO completion:^{
    [self dismissControllers:presentedControllerArray topIndex:index - 1 completion:completion];
    }];
    }
    }
    - (void)popToRootViewControllerFrom:(UIViewController *)fromViewController
    {
    //pop to root
    if ([fromViewController isKindOfClass:[UINavigationController class]]) {
    [(UINavigationController *)fromViewController popToRootViewControllerAnimated:YES];
    }
    if (fromViewController.navigationController) {
    [fromViewController.navigationController popToRootViewControllerAnimated:YES];
    }
    }

    参考这个思路可以做一些其他非常规页面跳转,跳转到我们想要跳转的指定界面去

    原文链接:https://blog.csdn.net/yinyignfenlei/article/details/86167245

    收起阅读 »

    iOS面试题(二)

    数据结构:objc_object,objc_class,isa,class_data_bits_t,cache_t,method_t 对象,类对象,元类对象 消息传递 消息转发 一、数据结构:objc_object,objc_class,isa,class...
    继续阅读 »
    • 数据结构:objc_object,objc_class,isa,class_data_bits_t,cache_t,method_t

    • 对象,类对象,元类对象

    • 消息传递

    • 消息转发



    一、数据结构:objc_object,objc_class,isa,class_data_bits_t,cache_t,method_t







    • objc_object(id)
      isa_t,关于isa操作相关,弱引用相关,关联对象相关,内存管理相关

    • objc_class (class) 继承自objc_object

    • isa指针,共用体isa_t


    • isa指向

      关于对象,其指向类对象。

      关于类对象,其指向元类对象。

      实例--(isa)-->class--(isa)-->MetaClass

    • cache_t

      用于快速查找方法执行函数,是可增量扩展的哈希表结构,是局部性原理的最佳运用


     struct cache_t {
    struct bucket_t *_buckets;//一个散列表,用来方法缓存,bucket_t类型,包含key以及方法实现IMP
    mask_t _mask;//分配用来缓存bucket的总数
    mask_t _occupied;//表明目前实际占用的缓存bucket的个数

    struct bucket_t {
    private:
    cache_key_t _key;
    IMP _imp;

    复制代码


    • class_data_bits_t:对class_rw_t的封装


    struct class_rw_t {
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;

    复制代码

    Objc的类的属性、方法、以及遵循的协议都放在class_rw_t中,class_rw_t代表了类相关的读写信息,是对class_ro_t的封装,而class_ro_t代表了类的只读信息,存储了 编译器决定了的属性、方法和遵守协议


    struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
    #ifdef __LP64__
    uint32_t reserved;
    #endif

    const uint8_t * ivarLayout;

    const char * name;
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;

    method_list_t *baseMethods() const {
    return baseMethodList;
    }
    };
    复制代码


    • method_t

      函数四要素:名称,返回值,参数,函数体


    struct method_t {
    SEL name; //名称
    const char *types;//返回值和参数
    IMP imp; //函数体

    复制代码

    二、 对象,类对象,元类对象



    • 类对象存储实例方法列表等信息。

    • 元类对象存储类方法列表等信息。


    • superClass是一层层集成的,到最后NSObject的superClass是nil.而NSObject的isa指向根元类,这个根元类的isa指向它自己,而它的superClass是NSObject,也就是最后形成一个环,

      三、消息传递


      void objc_msgSend(void /* id self, SEL op, ... */ )

      void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )

      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 */
      };
      复制代码



    消息传递的流程:缓存查找-->当前类查找-->父类逐级查找



    • 调用方法之前,先去查找缓存,看看缓存中是否有对应选择器的方法实现,如果有,就去调用函数,完成消息传递(缓存查找:给定值SEL,目标是查找对应bucket_t中的IMP,哈希查找)

    • 如果缓存中没有,会根据当前实例的isa指针查找当前类对象的方法列表,看看是否有同样名称的方法 ,如果找到,就去调用函数,完成消息传递(当前类中查找:对于已排序好的方法列表,采用二分查找,对于没有排序好的列表,采用一般遍历)

    • 如果当前类对象的方法列表没有,就会逐级父类方法列表中查找,如果找到,就去调用函数,完成消息传递(父类逐级查找:先判断父类是否为nil,为nil则结束,否则就继续进行缓存查找-->当前类查找-->父类逐级查找的流程)

    • 如果一直查到根类依然没有查找到,则进入到消息转发流程中,完成消息传递


    四、消息转发


    + (BOOL)resolveInstanceMethod:(SEL)sel;//为对象方法进行决议
    + (BOOL)resolveClassMethod:(SEL)sel;//为类方法进行决议
    - (id)forwardingTargetForSelector:(SEL)aSelector;//方法转发目标
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
    - (void)forwardInvocation:(NSInvocation *)anInvocation;
    复制代码





    那么最后消息未能处理的时候,还会调用到

    - (void)doesNotRecognizeSelector:(SEL)aSelector这个方法,我们也可以在这个方法中做处理,避免掉crash,但是只建议在线上环境的时候做处理,实际开发过程中还要把异常抛出来

    方法交换(Method-Swizzling)
    + (void)load
    {
    Method test = class_getInstanceMethod(self, @selector(test));

    Method otherTest = class_getInstanceMethod(self, @selector(otherTest));

    method_exchangeImplementations(test, otherTest);
    }

    应用场景:替换系统的方法,比如viewDidLoad,viewWillAppear以及一些响应方法,来进行统计信息

    动态添加方法

    class_addMethod(self, sel, testImp, "v@:");

    void testImp (void)
    {
    NSLog(@"testImp");
    }

    • @dynamic 动态方法解析

      动态运行时语言将函数决议推迟到运行时

      编译时语言在编译期进行函数决议


    • [obj foo]和objc_msgSend()函数之间有什么关系?

      objc_msgSend()是[obj foo]的具体实现。在runtime中,objc_msgSend()是一个c函数,[obj foo]会被翻译成这样的形式objc_msgSend(obj, foo)。


    • runtime是如何通过selector找到对应的IMP地址的?

      缓存查找-->当前类查找-->父类逐级查找


    • 能否向编译后的类中增加实例变量?

      不能。 编译后,该类已经完成了实例变量的布局,不能再增加实例变量。

      但可以向动态添加的类中增加实例变量。


    链接:https://juejin.cn/post/6844904039004504072 收起阅读 »