注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

环信FAQ

环信FAQ

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

iOS面试题(一)

字符串反转链表反转有序数组合并Hash算法查找两个子视图的共同父视图求无序数组当中的中位数一、字符串反转给定字符串 "hello,world",实现将其反转。输出结果:dlrow,olleh- (void)charReverse { NSString ...
继续阅读 »
  • 字符串反转
  • 链表反转
  • 有序数组合并
  • Hash算法
  • 查找两个子视图的共同父视图
  • 求无序数组当中的中位数

一、字符串反转
给定字符串 "hello,world",实现将其反转。输出结果:dlrow,olleh

- (void)charReverse
{
NSString * string = @"hello,world";

NSLog(@"%@",string);

NSMutableString * reverString = [NSMutableString stringWithString:string];

for (NSInteger i = 0; i < (string.length + 1)/2; i++) {

[reverString replaceCharactersInRange:NSMakeRange(i, 1) withString:[string substringWithRange:NSMakeRange(string.length - i - 1, 1)]];

[reverString replaceCharactersInRange:NSMakeRange(string.length - i - 1, 1) withString:[string substringWithRange:NSMakeRange(i, 1)]];
}

NSLog(@"reverString:%@",reverString);

//C
char ch[100];

memcpy(ch, [string cStringUsingEncoding:NSUTF8StringEncoding], [string length]);

//设置两个指针,一个指向字符串开头,一个指向字符串末尾
char * begin = ch;

char * end = ch + strlen(ch) - 1;

//遍历字符数组,逐步交换两个指针所指向的内容,同时移动指针到对应的下个位置,直至begin>=end
while (begin < end) {

char temp = *begin;

*(begin++) = *end;

*(end--) = temp;
}

NSLog(@"reverseChar[]:%s",ch);
}
复制代码

二、链表反转
反转前:1->2->3->4->NULL
反转后:4->3->2->1->NULL

/**  定义一个链表  */
struct Node {

NSInteger data;

struct Node * next;
};

- (void)listReverse
{
struct Node * p = [self constructList];

[self printList:p];

//反转后的链表头部
struct Node * newH = NULL;
//头插法
while (p != NULL) {

//记录下一个结点
struct Node * temp = p->next;
//当前结点的next指向新链表的头部
p->next = newH;
//更改新链表头部为当前结点
newH = p;
//移动p到下一个结点
p = temp;
}

[self printList:newH];
}
/**
打印链表

@param head 给定链表
*/

- (void)printList:(struct Node *)head
{
struct Node * temp = head;

printf("list is : ");

while (temp != NULL) {

printf("%zd ",temp->data);

temp = temp->next;
}

printf("\n");
}


/** 构造链表 */
- (struct Node *)constructList
{
//头结点
struct Node *head = NULL;
//尾结点
struct Node *cur = NULL;

for (NSInteger i = 0; i < 10; i++) {

struct Node *node = malloc(sizeof(struct Node));

node->data = i;

//头结点为空,新结点即为头结点
if (head == NULL) {

head = node;

}else{
//当前结点的next为尾结点
cur->next = node;
}

//设置当前结点为新结点
cur = node;
}

return head;
}

复制代码

三、有序数组合并
将有序数组 {1,4,6,7,9} 和 {2,3,5,6,8,9,10,11,12} 合并为
{1,2,3,4,5,6,6,7,8,9,9,10,11,12}

- (void)orderListMerge
{
int aLen = 5,bLen = 9;

int a[] = {1,4,6,7,9};

int b[] = {2,3,5,6,8,9,10,11,12};

[self printList:a length:aLen];

[self printList:b length:bLen];

int result[14];

int p = 0,q = 0,i = 0;//p和q分别为a和b的下标,i为合并结果数组的下标

//任一数组没有达到s边界则进行遍历
while (p < aLen && q < bLen) {

//如果a数组对应位置的值小于b数组对应位置的值,则存储a数组的值,并移动a数组的下标与合并结果数组的下标
if (a[p] < b[q]) result[i++] = a[p++];

//否则存储b数组的值,并移动b数组的下标与合并结果数组的下标
else result[i++] = b[q++];
}

//如果a数组有剩余,将a数组剩余部分拼接到合并结果数组的后面
while (++p < aLen) {

result[i++] = a[p];
}

//如果b数组有剩余,将b数组剩余部分拼接到合并结果数组的后面
while (q < bLen) {

result[i++] = b[q++];
}

[self printList:result length:aLen + bLen];
}
- (void)printList:(int [])list length:(int)length
{
for (int i = 0; i < length; i++) {

printf("%d ",list[i]);
}

printf("\n");
}
复制代码

四、HASH算法

  • 哈希表
    例:给定值是字母a,对应ASCII码值是97,数组索引下标为97。
    这里的ASCII码,就算是一种哈希函数,存储和查找都通过该函数,有效地提高查找效率。
  • 在一个字符串中找到第一个只出现一次的字符。如输入"abaccdeff",输出'b'

    字符(char)是一个长度为8的数据类型,因此总共有256种可能。每个字母根据其ASCII码值作为数组下标对应数组种的一个数字。数组中存储的是每个字符出现的次数。
- (void)hashTest
{
NSString * testString = @"hhaabccdeef";

char testCh[100];

memcpy(testCh, [testString cStringUsingEncoding:NSUTF8StringEncoding], [testString length]);

int list[256];

for (int i = 0; i < 256; i++) {

list[i] = 0;
}

char *p = testCh;

char result = '\0';

while (*p != result) {

list[*(p++)]++;
}

p = testCh;

while (*p != result) {

if (list[*p] == 1) {

result = *p;

break;
}

p++;
}

printf("result:%c",result);
}
复制代码

五、查找两个子视图的共同父视图
思路:分别记录两个子视图的所有父视图并保存到数组中,然后倒序寻找,直至找到第一个不一样的父视图。

- (void)findCommonSuperViews:(UIView *)view1 view2:(UIView *)view2
{
NSArray * superViews1 = [self findSuperViews:view1];

NSArray * superViews2 = [self findSuperViews:view2];

NSMutableArray * resultArray = [NSMutableArray array];

int i = 0;

while (i < MIN(superViews1.count, superViews2.count)) {

UIView *super1 = superViews1[superViews1.count - i - 1];

UIView *super2 = superViews2[superViews2.count - i - 1];

if (super1 == super2) {

[resultArray addObject:super1];

i++;

}else{

break;
}
}

NSLog(@"resultArray:%@",resultArray);

}
- (NSArray *)findSuperViews:(UIView *)view
{
UIView * temp = view.superview;

NSMutableArray * result = [NSMutableArray array];

while (temp) {

[result addObject:temp];

temp = temp.superview;
}

return result;
}
复制代码

六、求无序数组中的中位数
中位数:当数组个数n为奇数时,为(n + 1)/2,即是最中间那个数字;当n为偶数时,为(n/2 + (n/2 + 1))/2,即是中间两个数字的平均数。
首先要先去了解一些几种排序算法:iOS排序算法
思路:

  • 1.排序算法+中位数
    首先用冒泡排序、快速排序、堆排序、希尔排序等排序算法将所给数组排序,然后取出其中位数即可。
  • 2.利用快排思想
链接:https://juejin.cn/post/6844904038996279309
收起阅读 »

iOS基础之Category(一)

一、简介 我们可以利用 category 把类的实现分开在几个不同的文件中,这样可以减少单个文件的体积。可以把不同的功能组织到不同的 category 里使功能单一化。可以由多个开发者共同完成一个类,只需各自创建该类的 category 即可。可以按需加载想要...
继续阅读 »

一、简介


  1. 我们可以利用 category 把类的实现分开在几个不同的文件中,这样可以减少单个文件的体积。可以把不同的功能组织到不同的 category 里使功能单一化。可以由多个开发者共同完成一个类,只需各自创建该类的 category 即可。可以按需加载想要的 category,比如 SDWebImage 中 UIImageView+WebCache 和 UIButton+WebCache,根据不同需求加载不同的 category。


二、Extension 和 Category 对比



  • extension 是在编译器决定的,它就是类的一部分,在编译期和头文件里的 @interface 和 实现文件里的 @implementation形成一个完整的类,它伴随类的的产生而产生,随着类的消亡而消亡。extension 一般用来隐藏类的私有信息,必须有类的源码才可以为一个类添加 extension。所以无法为系统的类添加 extension

  • category 是在运行期决定的,category 是无法添加实例变量的,extension 是可以添加的。


三、Category 的本质

3.1 Category的基本使用



我们首先来看以下 category的基本使用:


// Person+Eat.h

#import "Person.h"

@interface Person (Eat)

- (void)eatBread;

+ (void)eatFruit;

@property (nonatomic, assign) int count;

@end

// Person+Eat.m

#import "Person+Eat.h"

@implementation Person (Eat)

- (void)eatBread {
NSLog(@"eatBread");
}

+ (void)eatFruit {
NSLog(@"eatFruit");
}

@end
复制代码


  • 创建了一个 Person 的分类,专门实现吃这个功能

  • 这个分类遵守了2个协议,分别为 NSCopyingNSCoding

  • 声明了2个方法,一个实例方法,一个类方法

  • 定义一个 count 属性


3.2 编译期的 Category


我们通过 clang 编译器来观察一下在编译期这些代码的本质是什么?


xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc MyClass.m -o MyClass-arm64.cpp
复制代码

编译之后,我们可以发现 category 的本质是结构体 category_t,无论我们创建了多少个 category 最终都会生成 category_t 这个结构体,并且 category 中的方法、属性、协议都是存储在这个结构体里的。也就是说在编译期,分类中成员是不会和类合并在一起的


struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t *classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
};
复制代码


  • name:类的名字

  • cls:类

  • instanceMethodscategory 中所有给类添加的实例方法的列表

  • classMethodscategory 中所有给类添加的类方法的列表

  • protocolscategory 中实现的所有协议的列表

  • instancePropertiescategory 中添加的所有属性


category 的定义中可以看到我们可以 添加实例方法,添加类方法,可以实现协议,可以添加属性。


不可以添加实例变量


我们继续研究下面的编译后的代码:


static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"eatBread", "v16@0:8", (void *)_I_Person_Eat_eatBread}}
};

static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"eatFruit", "v16@0:8", (void *)_C_Person_Eat_eatFruit}}
};

static struct /*_protocol_list_t*/ {
long protocol_count; // Note, this is 32/64 bit
struct _protocol_t *super_protocols[2];
} _OBJC_CATEGORY_PROTOCOLS_$_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
2,
&_OBJC_PROTOCOL_NSCopying,
&_OBJC_PROTOCOL_NSCoding
};

static struct /*_prop_list_t*/ {
unsigned int entsize; // sizeof(struct _prop_t)
unsigned int count_of_properties;
struct _prop_t prop_list[1];
} _OBJC_$_PROP_LIST_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_prop_t),
1,
{{"count","Ti,N"}}
};

static struct _category_t _OBJC_$_CATEGORY_Person_$_Eat __attribute__ ((used, section ("__DATA,__objc_const"))) =
{

"Person",
0, // &OBJC_CLASS_$_Person,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Eat,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Eat,
(const struct _protocol_list_t *)&_OBJC_CATEGORY_PROTOCOLS_$_Person_$_Eat,
(const struct _prop_list_t *)&_OBJC_$_PROP_LIST_Person_$_Eat,
};
复制代码


  • 首先看一下 _OBJC_$_CATEGORY_Person_$_Eat 结构体变量中的值,就是分别对应 category_t 的成员,第1个成员就是类名,因为我们声明了实例方法,类方法,遵守了协议,定义了属性,所以我们的结构体变量中这些都会有值。

  • _OBJC_$_CATEGORY_INSTANCE_METHODS_Person_$_Eat 结构体表示实例方法列表,里面包含了 eatBread 实例方法

  • _OBJC_$_CATEGORY_CLASS_METHODS_Person_$_Eat 结构体包含了 eatFruit 类方法

  • _OBJC_CATEGORY_PROTOCOLS_$_Person_$_Eat 结构体包含了 NSCopingNSCoding 协议

  • _OBJC_$_PROP_LIST_Person_$_Eat 结构体包含了 count 属性


3.3 运行期的 Category


在研究完编译时期的 category 后,我们进而研究运行时期的 category


objc-runtime-new.mm 的源码中,我们可以最终找到如何将 category 中的方法列表,属性列表,协议列表添加到类中。


static void
attachCategories(Class cls, const locstamped_category_t *cats_list, uint32_t cats_count,
int flags)

{
if (slowpath(PrintReplacedMethods)) {
printReplacements(cls, cats_list, cats_count);
}
if (slowpath(PrintConnecting)) {
_objc_inform("CLASS: attaching %d categories to%s class '%s'%s",
cats_count, (flags & ATTACH_EXISTING) ? " existing" : "",
cls->nameForLogging(), (flags & ATTACH_METACLASS) ? " (meta)" : "");
}

/*
* Only a few classes have more than 64 categories during launch.
* This uses a little stack, and avoids malloc.
*
* Categories must be added in the proper order, which is back
* to front. To do that with the chunking, we iterate cats_list
* from front to back, build up the local buffers backwards,
* and call attachLists on the chunks. attachLists prepends the
* lists, so the final result is in the expected order.
*/

constexpr uint32_t ATTACH_BUFSIZ = 64;
method_list_t *mlists[ATTACH_BUFSIZ];
property_list_t *proplists[ATTACH_BUFSIZ];
protocol_list_t *protolists[ATTACH_BUFSIZ];

uint32_t mcount = 0;
uint32_t propcount = 0;
uint32_t protocount = 0;
bool fromBundle = NO;
bool isMeta = (flags & ATTACH_METACLASS);
auto rwe = cls->data()->extAllocIfNeeded();

for (uint32_t i = 0; i < cats_count; i++) {
auto& entry = cats_list[i];

method_list_t *mlist = entry.cat->methodsForMeta(isMeta);
if ( ) {
if (mcount == ATTACH_BUFSIZ) {
prepareMethodLists(cls, mlists, mcount, NO, fromBundle);
rwe->methods.attachLists(mlists, mcount);
mcount = 0;
}
mlists[ATTACH_BUFSIZ - ++mcount] = mlist;
fromBundle |= entry.hi->isBundle();
}

property_list_t *proplist =
entry.cat->propertiesForMeta(isMeta, entry.hi);
if (proplist) {
if (propcount == ATTACH_BUFSIZ) {
rwe->properties.attachLists(proplists, propcount);
propcount = 0;
}
proplists[ATTACH_BUFSIZ - ++propcount] = proplist;
}

protocol_list_t *protolist = entry.cat->protocolsForMeta(isMeta);
if (protolist) {
if (protocount == ATTACH_BUFSIZ) {
rwe->protocols.attachLists(protolists, protocount);
protocount = 0;
}
protolists[ATTACH_BUFSIZ - ++protocount] = protolist;
}
}

if (mcount > 0) {
prepareMethodLists(cls, mlists + ATTACH_BUFSIZ - mcount, mcount, NO, fromBundle);
rwe->methods.attachLists(mlists + ATTACH_BUFSIZ - mcount, mcount);
if (flags & ATTACH_EXISTING) flushCaches(cls);
}

rwe->properties.attachLists(proplists + ATTACH_BUFSIZ - propcount, propcount);

rwe->protocols.attachLists(protolists + ATTACH_BUFSIZ - protocount, protocount);
}
复制代码


  • rwe->methods.attachLists(mlists, mcount);

  • rwe->protocols.attachLists(protolists, protocount);

  • rwe->properties.attachLists(proplists, propcount);


以上三个函数就是把 category 中的方法、属性和协议列表添加到类中的函数。


继续查看 attchLists 函数的实现:


void attachLists(List* const * addedLists, uint32_t addedCount) {
if (addedCount == 0) return;

if (hasArray()) {
// 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]));
}
else if (!list && addedCount == 1) {
// 0 lists -> 1 list
list = addedLists[0];
}
else {
// 1 list -> many lists
List* oldList = list;
uint32_t oldCount = oldList ? 1 : 0;
uint32_t newCount = oldCount + addedCount;
setArray((array_t *)malloc(array_t::byteSize(newCount)));
array()->count = newCount;
if (oldList) array()->lists[addedCount] = oldList;
memcpy(array()->lists, addedLists,
addedCount * sizeof(array()->lists[0]));
}
}
复制代码


  • 在这段源码中,主要关注2个函数 memmovememcpy

  • memmove 函数的作用是移动内存,将之前的内存向后移动,将原来的方法列表往后移

  • memcpy 函数的作用是内存的拷贝,将 category 中的方法列表复制到上一步移出来的位置。


从上述源码中,可以发现 category 的方法并没有替换原来类已有的方法,如果 category 和原来类中都有某个同名方法,只不过 category 中的方法被放到了新方法列表的前面,在运行时查找方法的时候是按照顺序查找的,一旦找到该方法,就不会向下继续查找了,产生了 category 会覆盖原类方法的假象。



所以我们在 category 定义方法的时候都要加上前缀,以避免意外的重名把类本身的方法”覆盖“掉。




  • 如果多个 category 中存在同名的方法,运行时最终调用哪个方法是由编译器决定的,最后一个参与编译的方法将会先被调用

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

收起阅读 »

iOS 常见面试题总结及答案(2)

一.App启动过慢,你可能想到的因素有哪些?1.解析Info.plist   加载相关信息,例如如闪屏 沙箱建立、权限检查2.Mach-O加载 如果是胖二进制文件,寻找合适当前CPU类别的部分加载所有依赖的Mach-O...
继续阅读 »

一.App启动过慢,你可能想到的因素有哪些?

1.解析Info.plist  

 加载相关信息,例如如闪屏 沙箱建立、权限检查

2.Mach-O加载 

如果是胖二进制文件,寻找合适当前CPU类别的部分
加载所有依赖的Mach-O文件(递归调用Mach-O加载的方法)
定位内部、外部指针引用,例如字符串、函数等
执行声明为attribute((constructor))的C函数
加载类扩展(Category)中的方法

3.程序执行

调用main()
调用UIApplicationMain()
调用applicationWillFinishLaunching

影响启动性能的因素

main()函数之前耗时的影响因素
动态库加载越多,启动越慢。
ObjC类越多,启动越慢
C的constructor函数越多,启动越慢
C++静态对象越多,启动越慢
ObjC的+load越多,启动越慢
main()函数之后耗时的影响因素
执行main()函数的耗时
执行applicationWillFinishLaunching的耗时
rootViewController及其childViewController的加载、view及其subviews的加载

优化

纯代码方式而不是storyboard加载首页UI。
对didFinishLaunching里的函数考虑能否挖掘可以延迟加载或者懒加载,需要与各个业务方pm和rd共同check 对于一些已经下线的业务,删减冗余代码。
对于一些与UI展示无关的业务,如微博认证过期检查、图片最大缓存空间设置等做延迟加载。
对实现了+load()方法的类进行分析,尽量将load里的代码延后调用。
上面统计数据显示展示feed的导航控制器页面(NewsListViewController)比较耗时,对于viewDidLoad以及viewWillAppear方法中尽量去尝试少做,晚做,不做。


二.单例的利弊

优点:
1:一个类只被实例化一次,提供了对唯一实例的受控访问。
2:节省系统资源
3:允许可变数目的实例。

缺点:
1:一个类只有一个对象,可能造成责任过重,在一定程度上违背了“单一职责原则”。
2:由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
3:滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。


三.TCP和UDP的区别于联系

TCP为传输控制层协议,为面向连接、可靠的、点到点的通信;
UDP为用户数据报协议,非连接的不可靠的点到多点的通信;
TCP侧重可靠传输,UDP侧重快速传输


四.TCP连接的三次握手

第一次握手:客户端发送 syn 包(syn=j)到服务器,并进入 SYN_SEND 状态,等待服务器确认;

第二次握手:服务器收到 syn 包,必须确认客户的 SYN(ack=j+1),同时自己也发送一个 SYN 包(syn=k),即 SYN+ACK 包,此时服务器进入 SYN_RECV 状态;

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=k+1),此包发送完毕,客户端和服务器进入 ESTABLISHED 状态,完成三次握手。

握手过程中传送的包里不包含数据,三次握手完毕后,客户端与服务器才正式开始传送数据。理想状态下,TCP 连接一旦建立,在通信双方中的任何一方主动关闭连接之前,TCP 连接都将被一直保持下去。断开连接时服务器和客户端均可以主动发起断开 TCP 连接的请求,断开过程需要经过“四次握手”(过程就不细写了,就是服务器和客户端交互,最终确定断开)



五.假如Controller太臃肿,如何优化?

1.将网络请求抽象到单独的类中

方便在基类中处理公共逻辑;
方便在基类中处理缓存逻辑,以及其它一些公共逻辑;
方便做对象的持久化。

2.将界面的封装抽象到专门的类中
构造专门的 UIView 的子类,来负责这些控件的拼装。这是最彻底和优雅的方式,不过稍微麻烦一些的是,你需要把这些控件的事件回调先接管,再都一一暴露回 Controller。

3.构造 ViewModel
借鉴MVVM。具体做法就是将 ViewController 给 View 传递数据这个过程,抽象成构造 ViewModel 的过程

4.专门构造存储类
专门来处理本地数据的存取。

5.整合常量


六.对程序性能的优化你有什么建议?

1.使用复用机制

2.尽可能设置 View 为不透明

3.避免臃肿的 XIB 文件

4.不要阻塞主线程

5.图片尺寸匹配 UIImageView

6.选择合适的容器

7.启用 GZIP 数据压缩

8.View 的复用和懒加载机制

9、缓存

服务器的响应信息(response)。图片。计算值。比如:UITableView 的 row heights。

10.关于图形绘制

11.处理 Memory Warnings

在 AppDelegate 中实现 - [AppDelegate applicationDidReceiveMemoryWarning:] 代理方法。
在 UIViewController 中重载 didReceiveMemoryWarning 方法。
监听 UIApplicationDidReceiveMemoryWarningNotification 通知。

12.复用高开销的对象

13.减少离屏渲染(设置圆角和阴影的时候可以选用绘制的方法)

14.优化 UITableView

通过正确的设置 reuseIdentifier 来重用 Cell。
尽量减少不必要的透明 View。
尽量避免渐变效果、图片拉伸和离屏渲染。
当不同的行的高度不一样时,尽量缓存它们的高度值。
如果 Cell 展示的内容来自网络,确保用异步加载的方式来获取数据,并且缓存服务器的 response。
使用 shadowPath 来设置阴影效果。
尽量减少 subview 的数量,对于 subview 较多并且样式多变的 Cell,可以考虑用异步绘制或重写 drawRect。
尽量优化 - [UITableView tableView:cellForRowAtIndexPath:] 方法中的处理逻辑,如果确实要做一些处理,可以考虑做一次,缓存结果。
选择合适的数据结构来承载数据,不同的数据结构对不同操作的开销是存在差异的。
对于 rowHeight、sectionFooterHeight、sectionHeaderHeight 尽量使用常量。

15.选择合适的数据存储方式

在 iOS 中可以用来进行数据持有化的方案包括:
NSUserDefaults。只适合用来存小数据。
XML、JSON、Plist 等文件。JSON 和 XML 文件的差异在「选择正确的数据格式」已经说过了。
使用 NSCoding 来存档。NSCoding 同样是对文件进行读写,所以它也会面临必须加载整个文件才能继续的问题。
使用 SQLite 数据库。可以配合 FMDB 使用。数据的相对文件来说还是好处很多的,比如可以按需取数据、不用暴力查找等等。
使用 CoreData。也是数据库技术,跟 SQLite 的性能差异比较小。但是 CoreData 是一个对象图谱模型,显得更面向对象;SQLite 就是常规的 DBMS。

16.减少应用启动时间

快速启动应用对于用户来说可以留下很好的印象。尤其是第一次使用时。
保证应用快速启动的指导原则:
尽量将启动过程中的处理分拆成各个异步处理流,比如:网络请求、数据库访问、数据解析等等。
避免臃肿的 XIB 文件,因为它们会在你的主线程中进行加载。重申:Storyboard 没这个问题,放心使用。
注意:在测试程序启动性能的时候,最好用与 Xcode 断开连接的设备进行测试。因为 watchdog 在使用 Xcode 进行调试的时候是不会启动的。

17.使用 Autorelease Pool (内存释放池)

18.imageNamed 和 imageWithContentsOfFile


七.使用drawRect有什么影响?

drawRect方法依赖Core Graphics框架来进行自定义的绘制

缺点:它处理touch事件时每次按钮被点击后,都会用setNeedsDisplay进行强制重绘;而且不止一次,每次单点事件触发两次执行。这样的话从性能的角度来说,对CPU和内存来说都是欠佳的。特别是如果在我们的界面上有多个这样的UIButton实例,那就会很糟糕了

这个方法的调用机制也是非常特别. 当你调用 setNeedsDisplay 方法时, UIKit 将会把当前图层标记为dirty,但还是会显示原来的内容,直到下一次的视图渲染周期,才会将标记为 dirty 的图层重新建立Core Graphics上下文,然后将内存中的数据恢复出来, 再使用 CGContextRef 进行绘制


八.基于CTMediator的组件化方案,有哪些核心组成?

假如主APP调用某业务A,那么需要以下组成部分:

CTMediator类,该类提供了函数 - (id)performTarget:(NSString *)targetName action:(NSString *)actionName params:(NSDictionary *)params shouldCacheTarget:(BOOL)shouldCacheTarget;
这个函数可以根据targetName生成对象,根据actionName构造selector,然后可以利用performSelector:withObject:方法,在目标上执行动作。

业务A的实现代码,另外要加一个专门的类,用于执行Target Action
类的名字的格式:Target_%@,这里就是Target_A。
这个类里面的方法,名字都以Action_开头,需要传参数时,都统一以NSDictionary*的形式传入。
CTMediator类会创建Target类的对象,并在对象上执行方法。

业务A的CTMediator扩展
扩展里声明了所有A业务的对外接口,参数明确,这样外部调用者可以很容易理解如何调用接口。
在扩展的实现里,对Target, Action需要通过硬编码进行指定。由于扩展的负责方和业务的负责方是相同的,所以这个不是问题。


九.为什么CTMediator方案优于基于Router的方案?

Router的缺点:

在组件化的实施过程中,注册URL并不是充分必要条件。组件是不需要向组件管理器注册URL的,注册了URL之后,会造成不必要的内存常驻。注册URL的目的其实是一个服务发现的过程,在iOS领域中,服务发现的方式是不需要通过主动注册的,使用runtime就可以了。另外,注册部分的代码的维护是一个相对麻烦的事情,每一次支持新调用时,都要去维护一次注册列表。如果有调用被弃用了,是经常会忘记删项目的。runtime由于不存在注册过程,那就也不会产生维护的操作,维护成本就降低了。 由于通过runtime做到了服务的自动发现,拓展调用接口的任务就仅在于各自的模块,任何一次新接口添加,新业务添加,都不必去主工程做操作,十分透明。

在iOS领域里,一定是组件化的中间件为openURL提供服务,而不是openURL方式为组件化提供服务。如果在给App实施组件化方案的过程中是基于openURL的方案的话,有一个致命缺陷:非常规对象(不能被字符串化到URL中的对象,例如UIImage)无法参与本地组件间调度。

在本地调用中使用URL的方式其实是不必要的,如果业务工程师在本地间调度时需要给出URL,那么就不可避免要提供params,在调用时要提供哪些params是业务工程师很容易懵逼的地方。

为了支持传递非常规参数,蘑菇街的方案采用了protocol,这个会侵入业务。由于业务中的某个对象需要被调用,因此必须要符合某个可被调用的protocol,然而这个protocol又不存在于当前业务领域,于是当前业务就不得不依赖public Protocol。这对于将来的业务迁移是有非常大的影响的

CTMediator的优点:

调用时,区分了本地应用调用和远程应用调用。本地应用调用为远程应用调用提供服务。

组件仅通过Action暴露可调用接口,模块与模块之间的接口被固化在了Target-Action这一层,避免了实施组件化的改造过程中,对Business的侵入,同时也提高了组件化接口的可维护性。

方便传递各种类型的参数。


十.内存的使用和优化的注意事项

重用问题:如UITableViewCells、UICollectionViewCells、UITableViewHeaderFooterViews设置正确的reuseIdentifier,充分重用;

尽量把views设置为不透明:当opque为NO的时候,图层的半透明取决于图片和其本身合成的图层为结果,可提高性能;

不要使用太复杂的XIB/Storyboard:载入时就会将XIB/storyboard需要的所有资源,包括图片全部载入内存,即使未来很久才会使用。那些相比纯代码写的延迟加载,性能及内存就差了很多;

选择正确的数据结构:学会选择对业务场景最合适的数组结构是写出高效代码的基础。比如,数组: 有序的一组值。使用索引来查询很快,使用值查询很慢,插入/删除很慢。字典: 存储键值对,用键来查找比较快。集合: 无序的一组值,用值来查找很快,插入/删除很快。
gzip/zip压缩:当从服务端下载相关附件时,可以通过gzip/zip压缩后再下载,使得内存更小,下载速度也更快。

延迟加载:对于不应该使用的数据,使用延迟加载方式。对于不需要马上显示的视图,使用延迟加载方式。比如,网络请求失败时显示的提示界面,可能一直都不会使用到,因此应该使用延迟加载。

数据缓存:对于cell的行高要缓存起来,使得reload数据时,效率也极高。而对于那些网络数据,不需要每次都请求的,应该缓存起来,可以写入数据库,也可以通过plist文件存储。

处理内存警告:一般在基类统一处理内存警告,将相关不用资源立即释放掉
重用大开销对象:一些objects的初始化很慢,比如NSDateFormatter和NSCalendar,但又不可避免地需要使用它们。通常是作为属性存储起来,防止反复创建。

避免反复处理数据:许多应用需要从服务器加载功能所需的常为JSON或者XML格式的数据。在服务器端和客户端使用相同的数据结构很重要;

使用Autorelease Pool:在某些循环创建临时变量处理数据时,自动释放池以保证能及时释放内存;

正确选择图片加载方式:UIImage加载方式


摘自作者:iOS猿_员
原贴链接:https://www.jianshu.com/p/4b4bd4e3feff

收起阅读 »

iOS 常见面试题总结及答案(1)

一.    Runloop和线程的关系?1.一一对应的关系,主线程的runloop已经创建,默认开启,子线程的runloop需要手动创建2.runloop在第一次获取时创建,在线程结束时销毁.1.NSTimer在子线程开启一个定时器;控制定...
继续阅读 »

一.    Runloop和线程的关系?

1.一一对应的关系,主线程的runloop已经创建,默认开启,子线程的runloop需要手动创建

2.runloop在第一次获取时创建,在线程结束时销毁.

runloop 的运行逻辑就是 do-while 循环下运用观察者模式(或者说是消息发送),根据7种状态的变化,处理事件输入源和定时器。如下图


runloop的应用:

1.NSTimer在子线程开启一个定时器;控制定时器在特定模式下执行

2.imageView的显示

3.performSelector

4.常驻线程(让一个子线程不进入消亡状态,等待其他线程发来消息,处理其他事件)

5.自动释放池

二.自动释放池什么时候释放?

第一次创建:启动runloop时候

最后一次销毁:runloop退出的时候

其他时候的创建和销毁:当runloop即将睡眠时销毁之前的释放池,重建一个新的

三.什么时候使用weak关键字,和assign的区别?

1.arc中有可能出现循环引用的地方,比如delegate属性

2.自定义IBOutlet空间属性一般也是使用weak

区别:weak表明一种非持有关系,必须用于oc对象;assign用于基本数据类型

weak修饰的指针默认是nil,如果用assign修饰对象,在对象被销毁时,会产生野指针,容易发生崩溃

四.objc中向一个nil对象发送消息将会发生什么?

在oc中向nil发送消息是完全有效的,只是在运行时不会有任何作用,如果一个方法返回值是一个对象,那么发送给nil的消息将返回0(nil),如果向一个nil对象发送消息,首先寻找对象的isa指针时就是0地址返回了,所以不会出现任何错误.

五.runtime如何实现weak变量的自动置nil

runtime对注册的类,会进行布局,对于weak对象会放入一个hash表中,用weak指向的对象内存地址作为key,当此对象的引用计数为0的时候会dealloc,假如weak指向的对象内存地址是a,那么就会以a为键,在这个weak表中搜索,找到所有以a为键的weak对象,从而设置为nil

六.runtime如何通过selector找到对应的IMP地址?

每一个类对象都有一个方法列表,方法列表中记录着方法名称.方法实现.参数类型,其实selector本质就是方法名称,通过这个方法名称就可以在方法列表中找到对应的方法实现

七.能否向编译后得到的类中增加实例变量?能否向运行时创建的类中添加实例变量?为什么?

1.不能向编译后得到的类中增加实例变量,可以向运行时创建的类中添加实例变量

原因: (1)因为编译后的类已经注册在runtime中,类结构中的objc_ivar_list 实例变量的链表和instance_size实例变量的内存大小已经确定,同事runtime会调用class_setIvarLayout 或者class_setWeakIvarLayout来处理strong weak引用,所以不能向存在的类中添加实例变量

(2)运行时创建的的类可以添加实例变量,调用class_addIvar函数,但是得在调用objc_allocateClassPair之后,objc_registerClassPair之前,原因同上

八.kvo的实现原理?

当你观察一个对象时,一个新的类会被动态创建,这个类继承自该对象的原本的类,并重写了被观察属性的setter方法,重写setter方法会负责在调用原setter方法之前和之后,通知所有观察对象:值得更改,最后通过isa混写,把这个对象的isa指针(isa指针告诉runtime系统这个对象的类是什么)指向这个新创建的子类,对象就神奇的变成了新创建子类的实例

实现原理如下图:


九.谈谈你对kvc的理解

kvc可以通过key直接访问对象属性,或者给对象的属性赋值,这样可以在运行时动态访问或者改变对象的属性值

当调用setValue:属性值forKey:@"name"的代码时,底层执行机制如下:

1.程序优先调用set:属性值方法,代码通过setter方法完成设置,注意,这里的是指成员变量名,首字母大小写要符合kvc的命名规则

2.如果没有找到setName:方法,kvc机制会检查+(Bool)accessInstanceVariablesDiretly方法,有没有返回yes,默认是返回yes,如果重写了该方法切返回NO的话,那么这一步会直接执行setValue:forUnderfindKey:方法, 如果返回Yes,那么kvc机制会搜索该类里面有没有名为的成员变量,不论该变量是在类接口处定义,还是在类实现处定义,也无论用什么样的访问修饰符只要存在以命名的变量,kvc都可以对该变量赋值

3.如果该类即没有set方法,也没有成员变量,kvc机制会搜索_is的成员变量

4.和上面一样,如果该类即没有set:方法,也没有_和_is成员变量,kvc机制会继续搜索和is的成员变量,再给它赋值

5.如果以上列出的方法和成员变量都不存在,系统将执行setValue:forUnderfindKey:方法,默认是抛出异常.

十.Notification和KVO的区别

1.KVO提供一种机制,当指定的被观察对象属性被修改后,kvo会自动通知响应的观察者

2.通知:是一种广播机制,在事件发生的时候,通过通知中心对象,一个对象能够为所关心这个事件发生的对象发送消息,两者都是观察者模式,不同在于kvo是被观察者直接发送消息给观察者,是对象间的直接交互.通知则是两者都和通知中心对象交互,对象之间不知道彼此

3.本质区别,底层原理不一样,kvo基于runtime,通知则有个通知中心来进行通知

十一.如果让你设计一个通知中心,设计思路

1.创建通知中心单例类,并在里面有一个保存全局NSDictionary

2.对于注册通知的类,将注册通知名作为key,执行的方法和类,以及一些参数作为一个数组为值

3.发送通知可以调用通知中心,通过字典key(通知名)找到对应的类和方法进行调用传值

十二.atomic和nonatomic区别,以及作用?

atomic与nonatom的主要区别就是系统自动生成的getter/setter方法不一样 ,atomic系统自动生成的getter/setter方法会进行加锁操作,nonatomic系统自动生成的getter/setter方法不会进行加锁操作

atomic不是线程安全的

系统生成的getter/setter方法会进行加锁操作,注意:这个锁仅仅保证了getter和setter存取方法的线程安全.因为getter/setter方法有加锁的缘故,故在别的线程来读写这个属性之前,会先执行完当前操作

atomic可以保证多线程访问时候,对象是未被其他线程销毁(比如:如果当一个线程正在get或set时,又有另一个线程同时在进行release操作,可能会直接crash)

十三.说一下静态库和动态库之间的区别

静态库:以.a 和 .framework为文件后缀名。链接时会被完整的复制到可执行文件中,被多次使用就有多份拷贝。

动态库:以.tbd(之前叫.dylib) 和 .framework 为文件后缀名。链接时不复制,程序运行时由系统动态加载到内存,系统只加载一次,多个程序共用(如系统的UIKit.framework等),节省内存 

静态库.a 和 framework区别.a 主要是二进制文件,不包含资源,需要自己添加头文件.framework 可以包含头文件+资源信息

十四.遇到过BAD_ACCESS的错误吗?你是怎样调试的?

BAD_ACCESS 报错属于内存访问错误,会导致程序崩溃,错误的原因是访问了野指针(悬挂指针)。

常规操作如下:

设置全局断点快速定位问题代码所在行。

开启僵尸对象诊断
Analyze分析
重写object的respondsToSelector方法,现实出现EXEC_BAD_ACCESS前访问的最后一个object。
Xcode 7 已经集成了BAD_ACCESS捕获功能:Address Sanitizer。 用法如下:在配置中勾选✅Enable Address Sanitizer

十五.说一下iOS 中的APNS,远程推送原理?

Apple push Notification Service,简称 APNS,是苹果的远程消息推送,原理如下:
iOS 系统向APNS服务器请求手机端的deviceToken
App 接收到手机端的 deviceToken,然后传给 App 对应的服务器.
App 服务端需要发送推送消息时, 需要先通过 APNS 服务器
然后根据对应的 deviceToken 发送给对应的手机

十六.UITableView的优化

1.重用cell

2.缓存行高(在请求到数据的时候提前计算好行高,用字典缓存好高度)

3.加载网络图片,使用异步加载,并缓存,下载的图片根据显示大小切成合适大小的图,查看大图时再显示大图,服务端最好处理好大图和小图,延时加载,当滚动很快时避免频繁请求,可通过runloop设置defultMode状态下渲染请求

4.局部刷新,减少全局刷新

5.渲染,尽量不要使用透明图层,将cell的opaque值设为Yes,背景色和子View不要使用透明色,减少阴影渐变,圆角等

6.少用addSubview给cell动态添加子View,初始化时直接设置好,通过hidden控制显示隐藏,布局在初始化直接布局好,避免cell的重新布局

7.按需加载cell,滚动很快时,只加载范围内的cell,如果目标行与当前行相差超过指定行数,只在目标滚动范围的前后定制n行加载,按需加载,提高流畅性方法如下


8.遇到复杂界面,需要异步绘制,给自定义的cell添加draw方法,在方法中利用GCD异步绘制,或者直接重写drawRect方法,此外,绘制cell不建议使用UIView,建议使用CALayer,UIView的绘制是建立在CoreGraphic上的,使用的是cpu,CALayer使用的是core Animation,CPU.GPU通吃.由系统决定使用哪一个,view的绘制使用的自下向上的一层层的绘制,然后渲染layer处理的是Texure,利用GPU的Texure Cache和独立的浮点数计算单元加速纹理的处理,GPU不喜欢透明,所以绘图一定要弄成不透明,对于圆角和阴影截一个伪透明的小图绘制上去,在layer回调里一定只做绘图,不做计算

cell被重用时,内部绘制的内容并不会自动清除,因此需要调用setNeedsDisplay或者setNeedsDisplayLayInRect:方法

十七.离屏渲染

下面的情况或操作会引发离屏渲染:

1.为图层设置遮罩(layer.mask)
2.将图层的layer.masksToBounds/view.clipsToBounds属性设置为ture
3.将图层layer,allowsGroupOpacity属性设置为Yes和layer.opacity小于1.0
4.给图层设置阴影(layer.shadow)
5.为图层设置layer.shouldRasterize=Yes(光栅化)
6.具有layer.cornerRadius,layer.edgeAntialiasingMask,layer.allowsEdgeAntialiasing的图层(圆角,抗锯齿)
7.使用CGContext在drawRect:方法中绘制大部分情况会导致离屏渲染,甚至是一个空的实现

优化方案

圆角优化:使用CAShapeLayer和UIBezierPath设置圆角;直接中间覆盖一张为圆形的透明图片
shadow优化:使用shadowPath指定layer阴影效果路径,优化性能
使用异步进行layer渲染(Facebook开源异步绘制框架AsncDisplayKit)
设置layer的opaque值为Yes,减少复杂图层合成
尽量使用不包含透明(alpha)通道的图片资源
尽量设置layer的大小为整型值

十八.UIView和CALayer区别

1.UIView可以响应事件,CALayer不可以,UIView继承自UIResponder,在UIResponder中定义了处理各种事件的事件传递接口。而CALayer直接继承NSObject,并没有相应的处理事件接口。
2.一个CALayer的frame是由它的anchorPoint(锚点),position,bounds,和transform共同决定,而一个view的frame只是简单的返回layer的frame,同样view的center和bounds也是返回layer的一些属性
3..UIView主要是对显示内容的管理,而CALayer主要是侧重显示内容的绘制。UIView是CALayer的CALayerDelegate。
4.每一个view内部都有一个CALayer在背后提供内容的绘制和显示,并且UIView的尺寸样式都由内部的CALayer提供,两者都有树状层级结构,layer内部有subLayers,view内部有subviews。
5.两者最明显的区别是view可以接受并处理事件,而Layer不可以。View是Layer的代理Delegate。

十九.iOS应用程序生命周期

ios程序启动原理(过程):如下:


二十.view视图生命周期



























收起阅读 »

Flutter踩坑:Android sdkmanager tool not found

今天因为升级了Mac系统,不知道怎么回事flutter开发环境突然报错,最终决定重新安装。正常安装了flutter,然后下载安装了AndroidStudio和VS(平时也会用用VS),然后运行flutter doctor的时候出现了如下错误: Android...
继续阅读 »

今天因为升级了Mac系统,不知道怎么回事flutter开发环境突然报错,最终决定重新安装。正常安装了flutter,然后下载安装了AndroidStudio和VS(平时也会用用VS),然后运行flutter doctor的时候出现了如下错误:



Android sdkmanager tool not found

(/Users/xx/android-sdk/tools/bin/sdkmanager).

Try re-installing or updating your Android SDK,

visit https://flutter.io/setup/#android-setup for detailed instructions.



解决步骤:

看字面意思问题应该是在“/Users/xx/android-sdk/tools/bin/sdkmanager”,但是我尝试了一下发现根本SDK文件夹下根本没有Tools文件夹
百度了一圈,网上给的解决方案,都是将emulator目录下的sdkmanager移动到 tools目录下,可是我根本就没有这个文件夹。
后来在Stack Overflow上找到了原因:Android Studio最新版本中,默认情况下是不会安装Android SDK Tools的,我的版本是3.6。



找到了原因就好解决了:




  • 在窗口左上角andriod studio-偏好设置中找到SDKTools






按图操作就好.png

继续在终端执行

flutter doctor --android-licenses (之后一路选Y就行了)






PS:VScode和AS都要记得装flutter插件,AS还要另外装dart插件


链接:https://www.jianshu.com/p/3237ea28793c 收起阅读 »

UITableViewCell嵌套WKWebView

     今天一直在网上找如何在UITableViewCell嵌套WKWebView,问题还挺多了,最后还是在找到了解决方案,废话不多说,直接看解决方案。正文1. 构建WKWebViewself.webView = [[WKWeb...
继续阅读 »
前言

     今天一直在网上找如何在UITableViewCell嵌套WKWebView,问题还挺多了,最后还是在stackoverflow找到了解决方案,废话不多说,直接看解决方案。

正文

1. 构建WKWebView

self.webView = [[WKWebView alloc] init];
// 创建请求
NSURLRequest *request =[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://www.jianshu.com"]];
// 加载网页
[self.webView loadRequest:request];

self.webView.scrollView.scrollEnabled = NO;
self.webView.scrollView.bounces = NO;
self.webView.scrollView.showsVerticalScrollIndicator = NO;
self.webView.autoresizingMask = UIViewAutoresizingFlexibleHeight;

// 将webView添加到界面
[self.contentView addSubview:self.webView];

2. cell高度适应WKWebView的内容

cell.webView.navigationDelegate = self;

#pragma mark - WKNavigationDelegate
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {

[webView evaluateJavaScript:@"document.body.offsetHeight" completionHandler:^(id _Nullable result, NSError * _Nullable error) {
// 计算webView高度
self.webViewCellHeight = [result doubleValue];
// 刷新tableView
[self.tableView reloadData];
}];
}

3. 解决加载空白问题
原因:由于WKWebView采用的lazy加载模式,所在的scrollView的滚动被禁用,导致被嵌套的WKCompositingView不进行数据加载。
详细细节请参考:WKWebView刷新机制小探

#pragma mark - UIScrollViewDelegate
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
// 判断webView所在的cell是否可见,如果可见就layout
NSArray *cells = self.tableView.visibleCells;
for (UITableViewCell *cell in cells) {
if ([cell isKindOfClass:[TraitWebViewCell class]]) {
TraitWebViewCell *webCell = (TraitWebViewCell *)cell;

[webCell.webView setNeedsLayout];
}
}

}


链接:https://www.jianshu.com/p/8cdad2282d24
收起阅读 »

iOS 一个OC对象在内存中的布局&&占用多少内存

一.先来看看我们平时接触的NSObject NSObject *objc = [[NSObject alloc]init]的本质 在内存中,这行代码就把objc转在底层实现中转成了一个结构体,其底层C++编译成结构体为: struct NSObject_I...
继续阅读 »

一.先来看看我们平时接触的NSObject



  • NSObject *objc = [[NSObject alloc]init]的本质

    在内存中,这行代码就把objc转在底层实现中转成了一个结构体,其底层C++编译成结构体为:


struct NSObject_IMPL {
Class isa;
};

在64位机中,一个isa占8个字节,在32位机中,一个isa占4个字节(当然苹果后面的机型都是64位的,这里我们着重讲解64位机)

  • 我们先来看看这个创建好的objc占多少个字节


int main(int argc, char * argv[]) {

@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
//定义一个objc
NSObject *objc = [[NSObject alloc]init];
//打印内存
NSLog(@"tu-%zd",class_getInstanceSize([NSObject class]));
NSLog(@"tu-%zd",malloc_size((__bridge const void *)(objc)));
}

}




其打印结果为:



objc打印结果





  • 为什么一个是8一个是16
    • 我们先来认识一下class_getInstanceSize、malloc_size的区别

      1.class_getInstanceSize:是一个函数(调用时需要开辟额外的内存空间),程序运行时才获取,计算的是类的大小(至少需要的大小)即实例对象的大小->结构体内存对齐

      2.创建的对象【至少】需要的内存大小不考虑malloc函数的话,内存对齐一般是以【8】对齐

      3.#import <objc/runtime.h>使用这个函数时倒入runtime运行时



    • malloc_size:堆空间【实际】分配给对象的内存大小 -系统内存对齐



      1. 在Mac、iOS中的malloc函数分配的内存大小总是【16】的倍数 即指针指向的内存大小

      2. import <malloc/malloc.h>使用时倒入这个框架





  • sizeof:是一个运算符,获取的是类型的大小(int、size_t、结构体、指针变量等),这些数值在程序编译时就转成常数,程序运行时是直接获取的
  • 看到上面对两个函数的认识,应该知道为什么输出的一个是8,一个是16了吧,当内存申请<16时,在底层分配的时候,系统会默认最低16个字节,系统给objc16个字节,而objc用到的是8个字节(没添加任何成员变量之前)

二.内存对齐



  • 在上面的基础上我们新建一个类Student继承NSObject,那么对于student的底层C++编译实现就变成了:


struct Student {
struct NSObject_IMPL NSOBJECT_IVARS;
};


也就是说,继承关系,子类直接将父类的isa引用进来




  • 对于class_getInstanceSize(也就是类本质的内存对其)

    1.在student中创建成员变量:
@interface Student : NSObject
{
@public
int _age;
int _no;
int _tc;
}
@end

其底层C++编译结构体就变成了


struct Student {
struct NSObject_IMPL NSOBJECT_IVARS;
int _age;
int _no;
int _tc;
};



  • 打印结果:


 //定义一个objc
Student *objc = [[Student alloc]init];
//打印内存
NSLog(@"tu-%zd",class_getInstanceSize([Student class]));
NSLog(@"tu-%zd",malloc_size((__bridge const void *)(objc)));

2020-09-08 12:35:27.158568+0800 OC底层[1549:79836] tu-24

2020-09-08 12:35:27.159046+0800 OC底层[1549:79836] tu-32




  • 先来说说24的由来





由于创建对象的时候,内存是以8对齐,上面我们讲到一个对象里面包含了一个isa占8个字节,对于student来说它有四个成员变量,isa,age,no,tc,共占8+4+4+4=20字节,但是由于内存以8对齐的原因,我们看到的输出是24,

所以class_getInstanceSize在计算实例大小的时候就是24,其白色区域表示空出了四个字节

再来看看32的由来
上面我们说到malloc_size指的是实际堆分配的空间,它以16字节对齐
可以看到,空白的区域为空出了12个字节,总共为32个字节

三.添加属性


  • 添加属性


@interface Student : NSObject
{
@public
int _age;
int _no;
int _tc;

}
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSArray *array;
@end

其在底层C++编译就变成了


struct Student {
struct NSObject_IMPL NSOBJECT_IVARS;
int _age;
int _no;
int _tc;
NSString _name;
NSArray _array;
};


默认的会将属性生成的_name添加进结构体中,计算相应的大小



总结:所以在实际计算类的占用空间大小的时候,根据添加的成员变量就可以计算出一个实例占用的内存大小(即计算出结构体的大小24,然后告诉系统,系统调用calloc分配内存的时候按照16对齐原则分配)

收起阅读 »

iOS之解决崩溃Collection <__NSArrayM: 0xb550c30> was mutated while being enumerated.

崩溃提示:Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <CALayerArray: 0x14df0bd0> was ...
继续阅读 »

崩溃提示:Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <CALayerArray: 0x14df0bd0> was mutated while being enumerated.'



当程序出现这个提示的时候,是因为你一边便利数组,又同时修改这个数组里面的内容,导致崩溃,网上的方法如下:

NSMutableArray * arrayTemp = xxx;

NSArray * array = [NSArray arrayWithArray: arrayTemp];

for (NSDictionary * dic in array) {

if (condition){

[arrayTemp removeObject:dic];

}

}

这种方法就是在定义一个一模一样的数组,便利数组A然后操作数组B

今天终于找到了一个更快接的删除数组里面的内容以及修改数组里面的内容的方法:

NSMutableArray *tempArray = [[NSMutableArray alloc]initWithObjects:@"12",@"23",@"34",@"45",@"56", nil];

[tempArray enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {

if ([obj isEqualToString:@"34"]) {

*stop = YES;

if (*stop == YES) {

[tempArray replaceObjectAtIndex:idx withObject:@"3333333"];

}

}

if (*stop) {

NSLog(@"array is %@",tempArray);

}

}];



利用block来操作,根据查阅资料,发现block便利比for便利快20%左右,这个的原理是这样的:

找到符合的条件之后,暂停遍历,然后修改数组的内容

转自:https://www.cnblogs.com/rglmuselily/p/6249015.html

收起阅读 »

检测项目中是否包含UIWebView

苹果最近废弃了UIWebView的使用,所以要把工程中引用UIWebView的地方全换掉,不然每次提交审核都会发警告邮件,如下: ITMS-90809: Deprecated API Usage - App updates that use UIWebView...
继续阅读 »

苹果最近废弃了UIWebView的使用,所以要把工程中引用UIWebView的地方全换掉,不然每次提交审核都会发警告邮件,如下:

ITMS-90809: Deprecated API Usage - App updates that use UIWebView will no longer be accepted as of December 2020\. Instead, use WKWebView for improved security and reliability. Learn more https://developer.apple.com/documentation/uikit/uiwebview

After you’ve corrected the issues, you can upload a new binary to App Store Connect.


用到UIWebView的场景如下(包括字符串):


1.自己代码中使用了UIWebView控件。
2.第三方库中使用:
1). README.md等资源文件中使用。
这些文件是没有引入项目的,要在pod库里找到相应的库文件夹,然后 show in finder便能找到。
2). 第三方库的注释里有使用UIWebView字眼。
3). 第三方framewok、.a文件等包含UIWebView,都是二进制文件(Binary file),这种情况只能等第三方库更新SDK了。
3.工程的一些本地配置里包含了UIWebView
搜索结果: UserInterfaceState.xcuserstate matches
1.UserInterfaceState.xcuserstate是什么?
该文件为xcode默认自带文件,是xcode的配置信息,git会用这个文件记录下来。 比如:手动删除此文件,退出xcode后重启xcode,此文件会自动创建并跟踪, git push的时候一般忽略此文件



解决:
场景1:
直接搜索替换成WKWebView即可
场景2:
注释和README文件里使用的UIWebView字眼应该是没影响的。至于frameword和.a文件中包含的引用只能等第三方库更新了,例如Twitter的SDK。
场景3:
xcode的配置信息文件,对于打出来的包应该页没啥影响。为了保险起见,还是删掉此文件,然后让Xcode重新生成一个新的。


检测项目中是否包含UIWebView
1.打开终端,cd + 把项目的工程文件所在文件夹拖入终端(即 得到项目的工程文件所在的路径)
2.输入以下命令(注意最后有个点号,而且点号和 UIWebView 之间必须有一个空格):
grep -r UIWebView .
3.以上操作都正确的话,会马上出现工程中带有 UIWebView 的文件的列表(包括在工程中无法搜索到的 .a 文件中对UIWebView 的引用),如下:



替换TwitterKit
在pod文件中,把 pod 'TwitterKit' 替换为 pod 'TwitterKit5'
进入TwitterSDK的github地址 https://github.com/twitter-archive/twitter-kit-ios/issues/120,可以看到如下信息:



链接:https://www.jianshu.com/p/9c1507509896
收起阅读 »

iOS之切换UITabBar再次加载网络数据

我们在开发中,常常遇到这样的问题,点击某一个TabBar后,本TabBar上的控制器页面数据不刷新,原因是因为在App启动之后,第一次点击本TabBar后页面已经走了viewDidLoad,所以除了重新启动不会再次走viewDidLoad,如果把请求方法写在v...
继续阅读 »

我们在开发中,常常遇到这样的问题,点击某一个TabBar后,本TabBar上的控制器页面数据不刷新,原因是因为在App启动之后,第一次点击本TabBar后页面已经走了viewDidLoad,所以除了重新启动不会再次走viewDidLoad,如果把请求方法写在viewDidLoad中,当然不会再次触发啦,但是,苹果早考虑到这个问题,不用咱们写通知事件什么的,废话有点多了,看代码详解:

首先需要在本控制器签订TabBar的协议

UITabBarControllerDelegate

一定要看清楚协议,如果警报 Assigning to 'id<UITabBarControllerDelegate> _Nullable' from incompatible type 'RCFollowOrderViewController *const __strong'那么就证明你的协议签成了UITabBarDelegate

在viewDidLoad 请求一次 

[self requestdata];

协议方法:


//点击的时候触发的方法


-(void)tabBarController:(UITabBarController *)tabBarController didSelectViewController:(UIViewController *)viewController{


    if (self.tabBarController.selectedIndex==1) {


        [self requestdata];


    }


}



//防止同一个页面一直点击tabbar 的方法


-(BOOL)tabBarController:(UITabBarController *)tabBarController shouldSelectViewController:(UIViewController *)viewController{


    UIViewController *tbselect=tabBarController.selectedViewController;


    if([tbselect isEqual:viewController]){


        returnNO;


    }


    returnYES;


}


如果想要点击TabBar一次,就刷新一次界面,就不写防止重复点击的代理方法,试下效果,搞定!


转自:https://www.jianshu.com/p/f52013ef1eea
收起阅读 »

iOS app唤起微信进行分享时出现“未验证应用”

昨天领导反馈app微信分享到朋友圈出现“未验证应用”的提示信息。通过追踪找到了解决办法。问题的原因由于苹果iOS 13系统版本安全升级,为此openSDK在1.8.6版本进行了适配。 1.8.6版本支持Universal Links方式跳转,对openSDK分...
继续阅读 »

昨天领导反馈app微信分享到朋友圈出现“未验证应用”的提示信息。通过追踪找到了解决办法。

问题的原因

由于苹果iOS 13系统版本安全升级,为此openSDK在1.8.6版本进行了适配。 1.8.6版本支持Universal Links方式跳转,对openSDK分享进行合法性校验。

PS:现在openSDK出了最新的版本1.8.7,新增了自检函数checkUniversalLinkReady:,可以帮助开发者排查SDK接入过程中遇到的问题,在哪一步出错了(共7步,见接入文档https://developers.weixin.qq.com/doc/oplatform/Mobile_App/Access_Guide/iOS.html#jump4)。

问题的解决办法

1.配置Universal Links

1)常见并编辑一个名为apple-app-site-association,无需后缀名,务必符合标准的json格式,格式如下:

{

  "applinks": {

"apps":[],

"details": [

  {

"appID": "你的app的teamID + Bundle Identifier",

"paths": ["*"]

  }

]

  }

}

2)将apple-app-site-association文件发给服务器端的同事,让他上传到域名的根目录下或者.well-known的子目录下(这里的域名必须要是可访问的域名,由服务器端的同事给到)。

2.在app里面配置通用链接

1)首先检查一下Xcode-Targets-Signing&Capabilities 是否有Associated Domains,如果没有,需要去开发者账号在identifer里选择跟当前Xcode所用bundle identifier相同的那一组,进去之后,将Associated Domains前面的方框打上勾,如果已经打勾了,配置如下:




2)实现AppDelegate里支持通用链接的实现方法

- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray> * _Nullable))restorationHandler {

  return [WXApi handleOpenUniversalLink:userActivity delegate:self];

}

3)修改微信注册方法

由[WXApi registerApp:kAppid]改为[WXApi registerApp:kAppid universalLink:kUniversalLinks],这里的universal links为第一步第2)里服务器同事给的链接地址

4)配置info.plist

这里如果是从旧版更新WechatOpenSDK1.8.6/7版本的话,需要在这个里面调用微信里的这个方法,并且在Xcode中,选择你的工程设置项,选中“TARGETS”一栏,在 “info”标签栏的“LSApplicationQueriesSchemes“添加weixin和weixinULAPI

5) 微信开放平台配置Universal links

需要把服务器同事给的地址填写到app iOS信息Universal Links,同时app的下载地址也一定要填写app在app store的地址,填写好后保存,开放平台需要审核,可能要等一段时间Universal Links才能生效,我就是昨天下午设置好没生效,今天早上来才生效。

3.检查是否配置好的Universal links是否生效

生效的标准结合这两个文档https://docs.qq.com/doc/DZHNvSGJLS3pqbHNl的步骤和https://developers.weixin.qq.com/doc/oplatform/Mobile_App/Access_Guide/iOS.html#jump4提供的自检过程

4.如果以上步骤都已经完成,并且自检正确,再分享出去,就不会再出现“未验证的应用”字样了。

PS:上周五(8-7)按照上述步骤,不会出现“未验证的应用”字样,到了周一(8-11)分享,发现又会出现,本人手机系统iOS13.5.1,微信7.0.14;换了同事的手机iOS13.1.2,微信7.0.14发现她的就正常,这个问题需要持续关注,个人觉得是微信sdk的bug;打包上线后,等到用户用一段时间后,新版本放量上去让整体错误率下降到90%以下才会从未验证应用中移除,问题得到彻底解决。


转自:https://www.jianshu.com/p/8e2f06d8f45a 收起阅读 »

iOS 一个标签自动布局的view

最近在做一个关于标签事件统计功能的view ,网上看了一些别人的demo感觉都不合适,于是想着自己造一个轮子探探水。 事件告警统计关注-事件列表.jpg 主要实现图中所示的功能,话不多少搞起! 第一次写大神们多包涵呀 下面这个方法计算传进来的字符...
继续阅读 »

最近在做一个关于标签事件统计功能的view ,网上看了一些别人的demo感觉都不合适,于是想着自己造一个轮子探探水。









事件告警统计关注-事件列表.jpg



主要实现图中所示的功能,话不多少搞起!



第一次写大神们多包涵呀

下面这个方法计算传进来的字符串数组,实现每个字符串长度的计算,并做换行判断,每一行存进统计数组中

//将标签数组根据type以及其他参数进行分组装入数组
- (void)disposeTags:(NSArray *)aryName aryCount:(NSArray *)aryCount{
NSMutableArray *tags = [NSMutableArray new];//纵向数组
NSMutableArray *subTags = [NSMutableArray new];//横向数组

float originX = _tagOriginX;
for (NSString *tagTitle in aryName) {
NSUInteger index = [aryName indexOfObject:tagTitle];

//计算每个tag的宽度
CGSize contentSize = [tagTitle fdd_sizeWithFont:[UIFont systemFontOfSize:14] constrainedToSize:CGSizeMake(self.frame.size.width-_tagOriginX*2, MAXFLOAT)];

NSMutableDictionary *dict = [NSMutableDictionary new];
dict[@"tagTitle"] = tagTitle;//标签标题
dict[@"tagCount"] =aryCount[index];
dict[@"viewWith"] = [NSString stringWithFormat:@"%f",contentSize.width+_tagSpace+30];//标签的宽度

if (index == 0) {
dict[@"originX"] = [NSString stringWithFormat:@"%f",originX];//标签的X坐标
[subTags addObject:dict];
} else {
if (originX + contentSize.width > self.frame.size.width-_tagOriginX*2) {
//当前标签的X坐标+当前标签的长度>屏幕的横向总长度则换行
[tags addObject:subTags];
//换行标签的起点坐标初始化
originX = _tagOriginX;
dict[@"originX"] = [NSString stringWithFormat:@"%f",originX];//标签的X坐标
subTags = [NSMutableArray new];
[subTags addObject:dict];
} else {
//如果没有超过屏幕则继续加在前一个数组里
dict[@"originX"] = [NSString stringWithFormat:@"%f",originX];//标签的X坐标
[subTags addObject:dict];
}
}

if (index +1 == aryName.count) {
//最后一个标签加完将横向数组加到纵向数组中
[tags addObject:subTags];
disposeAry = tags;
}

//标签的X坐标每次都是前一个标签的宽度+标签左右空隙+标签距下个标签的距离
originX += contentSize.width+_tagHorizontalSpace+_tagSpace+30;
}
}



下面这个方法是计算字符串长度的封装方法,只限字符串计算,用的话可以直接搬走

#pragma mark - 扩展方法


@implementation NSString (FDDExtention)

- (CGSize)fdd_sizeWithFont:(UIFont *)font constrainedToSize:(CGSize)size {
CGSize resultSize;
if ([self respondsToSelector:@selector(boundingRectWithSize:options:attributes:context:)]) {
NSMethodSignature *signature = [[self class] instanceMethodSignatureForSelector:@selector(boundingRectWithSize:options:attributes:context:)];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setTarget:self];
[invocation setSelector:@selector(boundingRectWithSize:options:attributes:context:)];
NSDictionary *attributes = @{ NSFontAttributeName:font };
NSStringDrawingOptions options = NSStringDrawingUsesLineFragmentOrigin;
NSStringDrawingContext *context;
[invocation setArgument:&size atIndex:2];
[invocation setArgument:&options atIndex:3];
[invocation setArgument:&attributes atIndex:4];
[invocation setArgument:&context atIndex:5];
[invocation invoke];
CGRect rect;
[invocation getReturnValue:&rect];
resultSize = rect.size;
} else {
NSMethodSignature *signature = [[self class] instanceMethodSignatureForSelector:@selector(sizeWithFont:constrainedToSize:)];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setTarget:self];
[invocation setSelector:@selector(sizeWithFont:constrainedToSize:)];
[invocation setArgument:&font atIndex:2];
[invocation setArgument:&size atIndex:3];
[invocation invoke];
[invocation getReturnValue:&resultSize];
}
return resultSize;
}

字符串长度计算完成了,下面就可以进行所有字符串排列的高度了。其实这个蛮简单的 ,统计数组有多少个元素就代表标签排布有多少行。


//获取处理后的tagsView的高度根据标签的数组




  • (float)getDisposeTagsViewHeight:(NSArray *)ary {


    float height = 0;

    if (disposeAry.count > 0) {

    height = _tagOriginY+disposeAry.count*(_tagHeight+_tagVerticalSpace);

    }

    return height;

    }




下面这个将标签加载到view上了,并实现赋值。

-(void)setTagAryName:(NSArray *)tagAryName aryCount:(NSArray *)aryCount delegate:(id)delegate{
_tagDelegate=delegate;
[self disposeTags:tagAryName aryCount:aryCount];
UILabel *label=[[UILabel alloc]initWithFrame:CGRectMake(_tagOriginX, 0, 200, 30)];
label.text=@"事件类别统计:";
label.textColor=_titleColor;
label.textAlignment=NSTextAlignmentLeft;
label.adjustsFontSizeToFitWidth=YES;
[self addSubview:label];
//遍历标签数组,将标签显示在界面上,并给每个标签打上tag加以区分
for (NSArray *iTags in disposeAry) {
NSUInteger i = [disposeAry indexOfObject:iTags];

for (NSDictionary *tagDic in iTags) {
NSUInteger j = [iTags indexOfObject:tagDic];

NSString *tagTitle = tagDic[@"tagTitle"];
float originX = [tagDic[@"originX"] floatValue];
float viewWith = [tagDic[@"viewWith"] floatValue];
NSString *count = tagDic[@"tagCount"];
UIView *NCView=[[UIView alloc]initWithFrame:CGRectMake(originX, _tagOriginY+i*(_tagHeight+_tagVerticalSpace), viewWith, _tagHeight)];
[self addSubview:NCView];
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
button.frame=CGRectMake(0, 0, viewWith-30, _tagHeight);
button.layer.borderColor = _borderColor.CGColor;
button.layer.borderWidth = _borderWidth;
button.layer.masksToBounds = _masksToBounds;
button.layer.cornerRadius = _cornerRadius;
button.titleLabel.font = [UIFont systemFontOfSize:_titleSize];
[button setTitle:tagTitle forState:UIControlStateNormal];
[button setTitleColor:_titleColor forState:UIControlStateNormal];
[button setBackgroundImage:_normalBackgroundImage forState:UIControlStateNormal];
[button setBackgroundImage:_highlightedBackgroundImage forState:UIControlStateHighlighted];
button.tag = i*iTags.count+j;
[button addTarget:self action:@selector(buttonAction:) forControlEvents:UIControlEventTouchUpInside];
UILabel *label=[[UILabel alloc]initWithFrame:CGRectMake(viewWith-30, 0, 30, _tagHeight)];
label.text=[NSString stringWithFormat:@"X%@",count];
label.textAlignment=NSTextAlignmentCenter;
label.adjustsFontSizeToFitWidth=YES;
label.textColor=_titleColor;
[NCView addSubview:button];
[NCView addSubview:label];
}
}
self.countLabel=[[UILabel alloc]initWithFrame:CGRectMake(_tagOriginX, [self getDisposeTagsViewHeight:disposeAry], 100, 30)];
self.countLabel.text=[NSString stringWithFormat:@"总计:%ld次",_times];
self.countLabel.textAlignment=NSTextAlignmentLeft;
self.countLabel.adjustsFontSizeToFitWidth=YES;
self.countLabel.textColor=_titleColor;
[self addSubview:self.countLabel];
if (disposeAry.count > 0) {
float contentSizeHeight = _tagOriginY+disposeAry.count*(_tagHeight+_tagVerticalSpace);
self.contentSize = CGSizeMake(self.frame.size.width,contentSizeHeight);
}

if (self.frame.size.height <= 0) {
self.frame = CGRectMake(CGRectGetMinX([self frame]), CGRectGetMinY([self frame]), CGRectGetWidth([self frame]), [self getDisposeTagsViewHeight:disposeAry]+30);
}


}


说了这么多,如何调用呢? 那么亮点来了 只需要传进来数组就实现图中的功能。

//计算出全部展示的高度,让maxHeight等于计算出的高度即可,初始化不需要设置高度
NSArray *tagAryCount=@[@"10",@"8",@"7",@"9",@"2",@"4",@"5",@"3",@"4"];
NSArray* tagAryName = @[@"黑色玫瑰",@"比尔沃吉特",@"钢铁烈阳",@"德玛西亚",@"祖安",@"巨神峰",@"雷瑟守备祖安",@"诺克萨斯暗影岛",@"弗雷尔卓德"];
ZHLbView * tagsView = [[ZHLbView alloc] initWithFrame:CGRectMake(0, 200, self.view.frame.size.width, 0)];
[tagsView setTagAryName:tagAryName aryCount:tagAryCount delegate:self];
[self.view addSubview:tagsView];


补充,在demo编写的过程中 ,遇到了一些小问题,就是scrollView如果是VC的第一个子视图的话 其总是要有一个64高度的空白区。
转自:https://www.jianshu.com/p/860af31a03aa 收起阅读 »

iOS 实例对象,类对象,元类对象的关联---isa/superclass指针(二)

总的来说,isa,superclass的的关系可以用一副经典的图来表示class的isa指向meta-classclass的superclass指向父类的class作者:枫紫_6174原贴链接:https://www.jianshu.com/p/26f37fb...
继续阅读 »

  • 一.isa指针

  • 上篇文章我们提到了实例对象,类对象和元类对象的存储结构里面都包含了一个isa指针,今天我们来看看它的作用,以及实例对象类对象元类对象之间的关联


  • 实例对象的isa指针


    • 当实例对象(instance)调用对象方法的时候,实例对象的isa指针指向类对象(class),在类对象中,查找对象方法并调用



  • 类对象的isa指针


    • 类对象(class)的isa指针指向元类对象(meta-class),当调用类方法时,类对象的isa指针指向元类对象,并在元类里面找到类方法并调用

      二.类对象的supercl

      • 先两个类,一个Person继承自NSObject,一个类继承自Person
      /// Person继承自NSObject
      @interface Person : NSObject
      -(void)perMethod;
      +(void)perEat;
      @end
      @implementation Person
      -(void)perMethod{
      }
      +(void)perEat{
      }

      @end

      /// student继承自Person
      @interface Student : Person
      -(void)StudentMethod;
      +(void)StudentEat;
      @end
      @implementation Student

      -(void)StudentMethod{
      }
      +(void)StudentEat{
      }



      • 当实例对象调用自身的对象方法时,它在自身的class对象中找到StudentMethod方法
      •         Student *student = [[Student alloc]init];
        [student StudentMethod]



        • 当实例对象调用父类的方法的时候


                Student *student = [[Student alloc]init];
        [student perMethod];当子类调用父类的实例方法的时候,子类的class类对象的superclass指针指向父类,直至基类(NSObject)找到方法并执行(注意,这里指的是实例方法,也就是减号方法)

        三.元类对象的superclass 指针
        当子类调用父类的类方法的时候,子类的superclass指向父类,并查找到相应的类方法,调用

      • [Student perEat];

      • 总的来说,isa,superclass的的关系可以用一副经典的图来表示


      • instance的isa指向class

        class的isa指向meta-class

        meta-class的isa指向基类的meta-class

        class的superclass指向父类的class



      • 作者:枫紫_6174
        原贴链接:https://www.jianshu.com/p/26f37fb21151
收起阅读 »

iOS 实例对象,类对象,元类对象(一)

OC对象的分类 OC对象主要分为三类:instance(实例对象),class (类对象),meta-class(元类对象) 实例对象: 实例对象就是通过类调用alloc来产生的instance,每一次调用的alloc都是产生新的实例对象,内存地址都是不一...
继续阅读 »

OC对象的分类


OC对象主要分为三类:instance(实例对象),class (类对象),meta-class(元类对象)




  • 实例对象:


    实例对象就是通过类调用alloc来产生的instance,每一次调用的alloc都是产生新的实例对象,内存地址都是不一样的,占据着不同的内存 eg:


        NSObject *objc1 = [[NSObject alloc]init];
NSObject *objc2 = [[NSObject alloc]init];

NSLog(@"instance----%p %p",objc1,objc2);



输出结果:


instance实例对象存储的信息:
1.isa指针

2.其他成员变量
  • 我们平时说打印出来的实例对象的地址开始就是指的是isa的地址,即isa的地址排在最前面,就是我们实例对象的地址


  • 类对象



  • 类对象的获取


        Class Classobjc1 = [objc1 class];
Class Classobjc2 = [objc2 class];
Class Classobjc3 = object_getClass(objc1);
Class Classobjc4 = object_getClass(objc2);
Class Classobjc5 = [NSObject class];
NSLog(@"class---%p %p %p %p %p ",Classobjc1,Classobjc2,Classobjc3,Classobjc4,Classobjc5);

打印结果

2020-09-22 15:48:00.125034+0800 OC底层[1095:69869] class---0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140

从打印的结果我们可以看到,所有指针指向的类对象的地址是一样的,也就是说一个类的类对象只有唯一的一个




  • 类对象的作用




类对象存储的信息:

1.isa指针

2.superclass指针

3.类的方法(method,即减号方法),类的属性(@property),协议信息,成员变量信息(这里的成员变量不是指的值,因为每个对象的值是由每个实例对象所决定的,这里指的是成员变量的类型,比如整形,字典,字符串,以及成员变量的名字)


  • 元类对象




1.元类对象的获取


        Class metaObjc1 = object_getClass([NSObject class]);
Class metaObjc2 = object_getClass(Classobjc1);
Class metaObjc3 = object_getClass(Classobjc3);
Class metaObjc4 = object_getClass(Classobjc5);

打印指针地址

NSLog(@"meta---%p %p %p %p",metaObjc1,metaObjc2,metaObjc3,metaObjc4);
2020-09-22 16:12:10.191008+0800 OC底层[1131:77555] instance----0x60000000c2e0 0x60000000c2f0
2020-09-22 16:12:10.191453+0800 OC底层[1131:77555] class---0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140 0x7fff9381e140
2020-09-22 16:12:10.191506+0800 OC底层[1131:77555] meta---0x7fff9381e0f0 0x7fff9381e0f0 0x7fff9381e0f0 0x7fff9381e0f0

获取元类对象的方法就是利用runtime方法,传入类对象,就可以获取该类的元类对象,从打印的结果可以看出,所有的指针地址一样,也就是说一个类的元类只有唯一的一个


特别注意一点:


Class objc = [[NSObject class] class];
Class objcL = [[[NSObject class] class] class];

无论class几次,它返回的始终是类对象

2020-09-22 16:21:11.065008+0800 OC底层[1163:81105] objcClass---0x7fff9381e140--0x7fff9381e140

元类存储结构:

元类的存储结构和类存储结构是一样的,但是存储的信息和用途不一样,元类的存储信息主要包括:

1.isa指针

2.superclass指针

3.类方法(即加号方法)


从图中我们可以看出元类的存储结构和类存储结构一样,只是有一些值为空



  • 判断是否为元类

    class_isMetaClass(objcL);

作者:枫紫_6174
原贴链接:https://www.jianshu.com/p/26f37fb21151
收起阅读 »

iOS Cateogry的深入理解&&initialize方法调用理解(二)

上一篇文章我们讲到了load方法,今天我们来看看initialize新建项目,新建类(和上一篇文章所建的类相同,方便大家理解,具体的类相关关系可以看上一篇文章我的介绍)类结构图如下将原来的load方法换成initialize先告诉大家initialize方法调...
继续阅读 »
  • 上一篇文章我们讲到了load方法,今天我们来看看initialize

新建项目,新建类(和上一篇文章所建的类相同,方便大家理解,具体的类相关关系可以看上一篇文章我的介绍)类结构图如下


将原来的load方法换成initialize




先告诉大家initialize方法调用的时间,以便大家带着答案去理解initialize:在类第一次接收到消息的时候调用,它区别于load(运行时加载类的时候调用),下面我们来深入理解initialize


相信大家在想什么叫第一次接收消息了,我们回到main()


运行程序,输出结果:

说明:NSLog(@"---")是由于我建的是命令行工程,不写这个,貌似不能显示控制台,Xcode版本是12,当然你们的要显示控制台,直接去掉这行代码


从输出结果可以看到没有任何关于initialize的打印,程序直接退出

  • 2.initialize的打印
int main(int argc, const char * argv[]) {
@autoreleasepool {

[TCPerson alloc];
}
return 0;
}

运行结果:


2020-12-04 14:59:17.417072+0800 TCCateogry[1616:79391] TCPerson (TCtest2) +initialize
Program ended with exit code: 0

从上面的输出结果我们可以看到,TCPerson (TCtest2) +initialize打印


load是直接函数指针直接调用,类,分类,继承等等


[TCPerson alloc]就是相当于该类发送消息,但是它只会调用类,分类的其中一个(取决于编译顺序,从输出结果可以看出,initialize走的是objc_msgSend,而load直接通过函数指针直接调用,所以initialize通过isa方法查找调用.


多次向TCPerson发送消息的输出结果

int main(int argc, const char * argv[]) {
@autoreleasepool {

[TCPerson alloc];
[TCPerson alloc];
[TCPerson alloc];
[TCPerson alloc];
}
return 0;
}

输出结果:

2020-12-04 15:11:12.246442+0800 TCCateogry[1659:85317] TCPerson (TCtest2) +initialize
Program ended with exit code: 0

initialize只会调用一次


我们再来看看继承关系中,initialize的调用


int main(int argc, const char * argv[]) {
@autoreleasepool {
[TCStudent alloc];

}
return 0;
}

输出结果:

2020-12-04 15:14:58.705423+0800 TCCateogry[1705:87507] TCPerson (TCtest2) +initialize
2020-12-04 15:14:58.705750+0800 TCCateogry[1705:87507] TCStudent (TCStudentTest2) +initialize
Program ended with exit code: 0

从输出结果来看,子类调用initialize之前,会先调用父类的initialize,再调用自己的initialize,当然无论父类调用initialize,还是子类调用initialize,如果有多个分类(这里指的是父类调用父类的分类,子类调用子类的分类),调用initialize取决于分类的编译顺序(调用后编译分类中的initialize,类似于压栈,先进后出),值得注意的是,无论父类子类的initialize,都只调用一次

int main(int argc, const char * argv[]) {
@autoreleasepool {
[TCPerson alloc];
[TCPerson alloc];
[TCStudent alloc];
[TCStudent alloc];
}
return 0;
}

输出结果:


020-12-04 15:23:27.168243+0800 TCCateogry[1731:91248] TCPerson (TCtest2) +initialize
2020-12-04 15:23:27.168601+0800 TCCateogry[1731:91248] TCStudent (TCStudentTest2) +initialize
Program ended with exit code: 0

如果子类(子类的分类也不实现)不实现initialize,则父类的initialize就调用多次

#import "TCStudent.h"

@implementation TCStudent
//+ (void)initialize{
// NSLog(@"TCStudent +initialize");
//}
@end

int main(int argc, const char * argv[]) {
@autoreleasepool {
[TCPerson alloc];
[TCStudent alloc];
}
return 0;
}


输出结果:


2020-12-04 15:37:09.055459+0800 TCCateogry[1822:98237] TCPerson (TCtest2) +initialize
2020-12-04 15:37:09.055775+0800 TCCateogry[1822:98237] TCPerson (TCtest2) +initialize
Program ended with exit code: 0

如果子类(子类的分类实现initialize)不实现initialize,则子类的initialize不会调用,调用子类分类的initialize(当然多个分类的话,调用哪个的initialize取决于编译顺序)

#import "TCStudent.h"

@implementation TCStudent
+ (void)initialize{
NSLog(@"TCStudent +initialize");
}
@end
#import "TCStudent+TCStudentTest1.h"

@implementation TCStudent (TCStudentTest1)
+ (void)initialize{
NSLog(@"TCStudent (TCStudentTest1) +initialize");
}
@end#import "TCStudent+TCStudentTest2.h"

@implementation TCStudent (TCStudentTest2)
+ (void)initialize{
NSLog(@"TCStudent (TCStudentTest2) +initialize");
}
@end

输出结果:


2020-12-04 15:41:21.863260+0800 TCCateogry[1868:100750] TCPerson (TCtest2) +initialize
2020-12-04 15:41:21.863568+0800 TCCateogry[1868:100750] TCStudent (TCStudentTest2) +initialize
Program ended with exit code: 0


作者:枫紫_6174
原贴链接:https://www.jianshu.com/p/f0150edc0f42
收起阅读 »

iOS Cateogry的深入理解&&load方法调用&&分类重写方法的调用顺序(一)

首先先看几个面试问题 类别里面有负载方法么?load方法什么时候调用?load方法有继承么?  1.新建一个项目,并添加TCPerson类,并给TCPerson添加两个分类 2.新建一个TCStudent类继承自TCPerson,并且给TCStuden...
继续阅读 »

首先先看几个面试问题



  • 类别里面有负载方法么?load方法什么时候调用?load方法有继承么?


 1.新建一个项目,并添加TCPerson类,并给TCPerson添加两个分类




2.新建一个TCStudent类继承自TCPerson,并且给TCStudent也添加两个分类


类别里面有负载方法么?


  • 答:分类里面肯定有负载


#import "TCPerson.h"

@implementation TCPerson
(void)load{

}
@end

#import "TCPerson TCtest1.h"

@implementation TCPerson (TCtest1)
(void)load{

}
@end

#import "TCPerson TCTest2.h"

@implementation TCPerson (TCTest2)
(void)load{

}
@end

load方法什么时候调用?




  • load方法在运行时加载类和分类的时候调用load

  • #import 

    int main(int argc, const char * argv[]) {
    @autoreleasepool {

    }
    return 0;
    }


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


    @implementation TCPerson (TCtest1)
    + (void)load{
    NSLog(@"TCPerson (TCtest1) +load");
    }
    @end
    @implementation TCPerson (TCTest2)
    + (void)load{
    NSLog(@"TCPerson (TCtest2) +load");
    }
    @end

    可以看到我们在main里面不导入任何的头文件,也不引用任何的类,直接运行,控制台输出结果:


    从输出结果我们可以抛光,三个负载方法都被调用



    如果不是,到底分类的方法是怎么调用的?




    • 首先我们在TCPerson申明一个方法+(void)test和在它的两个分类都重写+(void)test

    #import 

    NS_ASSUME_NONNULL_BEGIN

    @interface TCPerson : NSObject
    + (void)test;
    @end

    NS_ASSUME_NONNULL_END


    #import "TCPerson.h"

    @implementation TCPerson
    + (void)load{
    NSLog(@"TCPerson +load");
    }
    + (void)test{
    NSLog(@"TCPerson +test");
    }
    @end

    分类重写测试

    #import "TCPerson+TCtest1.h"

    @implementation TCPerson (TCtest1)
    + (void)load{
    NSLog(@"TCPerson (TCtest1) +load");
    }
    + (void)test{
    NSLog(@"TCPerson (TCtest1) +test1");
    }
    @end

    #import "TCPerson+TCTest2.h"

    @implementation TCPerson (TCTest2)
    + (void)load{
    NSLog(@"TCPerson (TCtest2) +load");
    }
    + (void)test{
    NSLog(@"TCPerson (TCtest2) +test2");
    }
    @end

    在主要里面我们调用测试

    #import 
    #import "TCPerson.h"
    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    [TCPerson test];
    }
    return 0;
    }

    输出结果:

    从输出结果中我们可以看到,只有分类2中的测试被调用,为什么只调用分类2中的测试了?
    因为编译顺序是分类2在后,1在前,这个时候我们改变编译顺序(进行文件就行了)
    其输出结果为:

    细心的老铁会看到,为什么load方法一直都在调用,这是为什么了?它和test方法到底有什么不同了?真的是我们理解中的load不覆盖,test coverage了,所以才出现这种情况么?


    我们打印TCPerson的类方法

    void printMethodNamesOfClass(Class cls)
    {
    unsigned int count;
    // 获得方法数组
    Method *methodList = class_copyMethodList(cls, &count);

    // 存储方法名
    NSMutableString *methodNames = [NSMutableString string];

    // 遍历所有的方法
    for (int i = 0; i < count; i++) {
    // 获得方法
    Method method = methodList[I];
    // 获得方法名
    NSString *methodName = NSStringFromSelector(method_getName(method));
    // 拼接方法名
    [methodNames appendString:methodName];
    [methodNames appendString:@", "];
    }

    // 释放
    free(methodList);

    // 打印方法名
    NSLog(@"%@ %@", cls, methodNames);
    }
    int main(int argc, const char * argv[]) {
    @autoreleasepool {
    [TCPerson test];
    printMethodNamesOfClass(object_getClass([TCPerson class]));
    }
    return 0;
    }



    输出结果:

    可以看到,TCPerson的所有类方法名,并非覆盖,三个负载,三个测试,方法都在

    载入原始码分析:查看objc长期原始码我们可以看到:

  • 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;
    }

    load方法它是先调用 while (loadable_classes_used > 0) {call_class_loads(); }类的load,再调用more_categories = call_category_loads()分类的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, SEL_load);
    }

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

    其通过的是load_method_t函数指针直接调用

    函数指针直接调用

    typedef void(*load_method_t)(id, SEL);

    其分类load方法调用也是一样

    static bool call_category_loads(void)
    {
    int i, shift;
    bool new_categories_added = NO;

    // Detach current loadable list.
    struct loadable_category *cats = loadable_categories;
    int used = loadable_categories_used;
    int allocated = loadable_categories_allocated;
    loadable_categories = nil;
    loadable_categories_allocated = 0;
    loadable_categories_used = 0;

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

    cls = _category_getClass(cat);
    if (cls && cls->isLoadable()) {
    if (PrintLoading) {
    _objc_inform("LOAD: +[%s(%s) load]\n",
    cls->nameForLogging(),
    _category_getName(cat));
    }
    (*load_method)(cls, SEL_load);
    cats[i].cat = nil;
    }
    }

    为什么test不一样了

    因为test是因为消息机制调用的,objc_msgSend([TCPerson class], @selector(test));消息机制就牵扯到了isa方法查找,test在元类方法里面顺序查找的(关于isa,可以查看我的实例对象,类对象,元类对象的关联---isa/superclass指针(2))里面有详细的关于test的方法调用原理


    load只在加载类的时候调用一次,且先调用类的load,再调用分类的


    load的继承关系调用

    首先我们先看TCStudent


    #import "TCStudent.h"

    @implementation TCStudent

    @end

    不写load方法调用

    TCStudent写上load

  • 从中可以看出子类不写load的方法,调用父类的load,当子类调用load时,先调用父类的load,再调用子类的load,父类子类load取决于你写load方法没有,如果都写了,先调用父类的,再调用子类的


    总结:先调用类的load,如果有子类,则先看子类是否写了load,如果写了,则先调用父类的load,再调用子类的load,当类子类调用完了,再是分类,分类的load取决于编译顺序,先编译,则先调用,test的方法调用走的是消息发送机制,其底层原理和load方法有着本质的区别,消息发送主要取决于isa的方法查找顺序



    作者:枫紫_6174
    原贴链接:https://www.jianshu.com/p/f66921e24ffe
收起阅读 »

iOS UITableView左滑删除功能

一、概述 UITbableView作为列表展示信息,除了展示的功能,有时会用到删除的功能,比如购物车,视频收藏等。删除功能可以直接使用系统自带的删除功能,当横向向左轻扫cell时,右侧出现红色的删除按钮,点击删除当前cell。 二、效果图 效果图.g...
继续阅读 »
一、概述

UITbableView作为列表展示信息,除了展示的功能,有时会用到删除的功能,比如购物车视频收藏等。删除功能可以直接使用系统自带的删除功能,当横向向左轻扫cell时,右侧出现红色的删除按钮,点击删除当前cell。


二、效果图







效果图.gif


三、技术分析


  1. 让tableView进入编辑状态,即tableView.editing = YES

// 取消
[self.tableView setEditing:YES animated:NO];



  1. 返回编辑模式,即实现UITableViewDelegate中的- tableview:editingStyleForRowAtIndexPath:方法,在里面返回删除模式。如果不实现,默认返回的就是删除模式。

-(UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath
{
// 删除
return UITableViewCellEditingStyleDelete;
}



  1. 提交删除操作,即实现UITableViewDelegate中的- tableview:commitEditingStyle:editing StyleForRowAtIndexPath:方法。只要实现此方法,即默认实现了系统横扫出现删除按钮的删除方法。

-(void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
//只要实现这个方法,就实现了默认滑动删除!!!!!
if (editingStyle == UITableViewCellEditingStyleDelete)
{
// 删除数据
[self _deleteSelectIndexPath:indexPath];
}
}



  1. 如果想把删除按钮改为中文,可以实现-tableView:titleForDeleteConfirmationButtonForRowAtIndexPath:方法。

-(NSString *)tableView:(UITableView *)tableView titleForDeleteConfirmationButtonForRowAtIndexPath:(NSIndexPath *)indexPath
{
return @"删除";
}


四、细节处理


  1. 以下属性必须设置为NO,默认为NO,否则会导致删除模式无效,反而成为多选模式

tableView.allowsMultipleSelection = NO;
tableView.allowsSelectionDuringEditing = NO;
tableView.allowsMultipleSelectionDuringEditing = NO;



  1. 侧滑状态下点击编辑按钮的bug。
// 编辑按钮被点击
- (
void)_rightBarButtonItemDidClicked:(UIButton *)sender
{
sender.selected = !sender.isSelected;
if (sender.isSelected) {

// 这个是fix掉:当你左滑删除的时候,再点击右上角编辑按钮, cell上的删除按钮不会消失掉的bug。且必须放在 设置tableView.editing = YES;的前面。
[
self.tableView reloadData];

// 取消
[
self.tableView setEditing:YES animated:NO];
}
else{
// 编辑
[
self.tableView setEditing:NO animated:NO];
}
}

转自:https://www.jianshu.com/p/4c53901062eb 收起阅读 »

OpenGL基础点

一、OpenGL中的坐标系1. 2D笛卡尔坐标系(X轴,Y轴),平面图形,视口(显示窗口区域系数)2. 3D笛卡尔坐标系(X轴,Y轴,Z轴),立体图形3. OpenGL的投影方式有:透视投影:用来渲染立体图形,有远小近大的效果。具有更加逼真的效果正投影:只能用...
继续阅读 »

一、OpenGL中的坐标系

1. 2D笛卡尔坐标系(X轴,Y轴),平面图形,视口(显示窗口区域系数)

2. 3D笛卡尔坐标系(X轴,Y轴,Z轴),立体图形

3. OpenGL的投影方式有:
透视投影:用来渲染立体图形,有远小近大的效果。具有更加逼真的效果
正投影:只能用来设置平面图形

4.坐标系的分类:

① 惯性坐标系:没有什么参考价值,只要用来平移到世界坐标系

② 世界坐标系:大环境中的位置,系统的绝对坐标

③ 物体空间坐标系(局部空间):局部空间中的位置

④ 摄像机坐标系:观察空间(观察者)

5.坐标系之间的变换

① 物体坐标系,模型变换

② 转换到世界坐标系,视变换

③ 转换到观察者坐标系/摄像机坐标系,投影变换

④ 转换到裁剪坐标系,透视除法

⑤ 转换到规范化设备坐标,视口变换

⑥ 转换到屏幕坐标

注:① - ④ 是可以由开发者自定义完成的,⑤ - ⑥是有系统OpenGL来完成的

二、着色器的渲染流程

1.顶点数据

2.顶点着色器:接收顶点数据,单独处理每个顶点

3.细分着色器:
  ① 可选:描述物体形状,在管线中生成新的饿几何平面模型,生成最终形态
  ② 组分控制着色器/细分计算着色器:对所有的图形进行修改几何图元类型或者放弃所有图像

4.几何着色器

5.图元设置

6.剪切:剪切窗口之外的绘制

7.光栅化:输入图元的数学描述,转化为与屏幕对应位置像素片元,简称光栅化

8.片元着色器:片元着色器以及深度值,然后传递到片元测试和混合模块

9.效果

三、补充几个知识点

1.图片的渲染流程:
  ① GPU解码图片
  ② GPU纹理混合、顶点计算、像素点填充计算、渲染到帧缓冲区
  ③ 时钟信号:垂直同步、水平同步
  ④ iOS设备双缓冲机制:显示系统通常会引入两个帧缓冲区,双缓冲机制

2.MVP矩阵:
  ① model:物体的一些变化
  ② view:观察
  ③ projection:3D矩阵

3.强制解压缩:对图片进行重新绘制,得到一张新的压缩后的位图,其中用到的最核心的函数是:GCBitmapContextCreate

收起阅读 »

iOS打印导入字体名称

通常在开发中,我们的APP中会使用到一些自己定义的字体,关于引用那部分就不细说了,网上百度一堆,下边说一个容易忽略的点。在iOS中,使用字体,不是使用字体包的名称,而是需要导入包体在iOS中对应的名称,打印字体名称如下:for (NSString *fontf...
继续阅读 »

通常在开发中,我们的APP中会使用到一些自己定义的字体,关于引用那部分就不细说了,网上百度一堆,下边说一个容易忽略的点。

在iOS中,使用字体,不是使用字体包的名称,而是需要导入包体在iOS中对应的名称,打印字体名称如下:

for (NSString *fontfamilyname in [UIFont familyNames]) { // font:'Avenir-Heavy
NSLog(@"family:'%@'",fontfamilyname);
for(NSString *fontName in [UIFont fontNamesForFamilyName:fontfamilyname])
{
NSLog(@"\tfont:'%@'",fontName);
}
NSLog(@"-------------");
}

对字体的使用

self.proNameLab.font = [UIFont fontWithName:@"Avenir-Heavy" size:13*ScaleSize];
收起阅读 »

iOS本地数据持久化

在iOS开发中,有很多数据持久化的方案,本文章将介绍以下6种方案: plist文件(序列化) preference(偏好设置) NSKeyedArchiver(归档) SQLite3 FMDB CoreData 沙盒 每个APP的沙盒下面都有相似目录结构,...
继续阅读 »

在iOS开发中,有很多数据持久化的方案,本文章将介绍以下6种方案:



plist文件(序列化)

preference(偏好设置)

NSKeyedArchiver(归档)

SQLite3

FMDB

CoreData



沙盒


每个APP的沙盒下面都有相似目录结构,如图







下面的代码得到的是应用程序目录的路径,在该目录下有三个文件夹:Documents、Library、temp以及一个.app包!该目录下就是应用程序的沙盒,应用程序只能访问该目录下的文件夹!!!



NSString *path = NSHomeDirectory();




1、Documents 目录:您应该将所有的应用程序数据文件写入到这个目录下。这个目录用于存储用户数据。该路径可通过配置实现iTunes共享文件。可被iTunes备份。


2、AppName.app 目录:这是应用程序的程序包目录,包含应用程序的本身。由于应用程序必须经过签名,所以您在运行时不能对这个目录中的内容进行修改,否则可能会使应用程序无法启动。


3、Library 目录:这个目录下有两个子目录:

Preferences 目录:包含应用程序的偏好设置文件。您不应该直接创建偏好设置文件,而是应该使用NSUserDefaults类来取得和设置应用程序的偏好.

Caches 目录:用于存放应用程序专用的支持文件,保存应用程序再次启动过程中需要的信息。

可创建子文件夹。可以用来放置您希望被备份但不希望被用户看到的数据。该路径下的文件夹,除Caches以外,都会被iTunes备份。


4、tmp 目录:这个目录用于存放临时文件,保存应用程序再次启动过程中不需要的信息。该路径下的文件不会被iTunes备份。

// 获取沙盒主目录路径
NSString *homeDir = NSHomeDirectory();
// 获取Documents目录路径
NSString *docDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject];
// 获取Library的目录路径
NSString *libDir = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) lastObject];
// 获取Caches目录路径
NSString *cachesDir = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
// 获取tmp目录路径
NSString *tmpDir = NSTemporaryDirectory();

//获取应用程序程序包中资源文件路径的方法
NSString *imagePath = [[NSBundle mainBundle] pathForResource:@"apple" ofType:@"png"];
UIImage *appleImage = [[UIImage alloc] initWithContentsOfFile:imagePath];


plist文件(序列化)


可以被序列化的类型只有如下几种:

NSArray; //数组
NSMutableArray; //可变数组
NSDictionary; //字典
NSMutableDictionary; //可变字典
NSData; //二进制数据
NSMutableData; //可变二进制数据
NSString; //字符串
NSMutableString; //可变字符串
NSNumber; //基本数据
NSDate; //日期


数据存储与读取的实例:

/**
写入数据到plist
*/
- (void)writeToPlist{
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES).firstObject;
NSLog(@"写入数据地址%@",path);

NSString *fileName = [path stringByAppendingPathComponent:@"123.plist"];
NSArray *array = @[@"123", @"王佳佳", @"iOS"];
//序列化,把数组存入plist文件
[array writeToFile:fileName atomically:YES];
NSLog(@"写入成功");
}

/**
从plist读取数据

@return 读出数据
*/
- (NSArray *)readFromPlist{
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES).firstObject;
NSLog(@"读取数据地址%@",path);

NSString *fileName = [path stringByAppendingPathComponent:@"123.plist"];
//反序列化,把plist文件数据读取出来,转为数组
NSArray *result = [NSArray arrayWithContentsOfFile:fileName];
NSLog(@"%@", result);
return result;
}



存储时使用writeToFile:atomically:方法。 其中atomically表示是否需要先写入一个辅助文件,再把辅助文件拷贝到目标文件地址。这是更安全的写入文件方法,一般都写YES。



Preference(偏好设置)


Preference通常用来保存应用程序的配置信息的,一般不要在偏好设置中保存其他数据。


数据存储与读取的实例:

- (void)writeToPreference{

//1.获得NSUserDefaults文件
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];
//2.向偏好设置中写入内容
[userDefaults setObject:@"wangjiajia" forKey:@"name"];
[userDefaults setBool:YES forKey:@"sex"];
[userDefaults setInteger:21 forKey:@"age"];
//2.1立即同步
[userDefaults synchronize];

NSString *path = NSHomeDirectory();
}

- (void)readFromPreference{
//获得NSUserDefaults文件
NSUserDefaults *userDefaults = [NSUserDefaults standardUserDefaults];

//读取偏好设置
NSString *name = [userDefaults objectForKey:@"name"];
BOOL sex = [userDefaults boolForKey:@"sex"];
NSInteger age = [userDefaults integerForKey:@"age"];
}


使用偏好设置对数据进行保存,它保存的时间是不确定的,会在将来某一时间自动将数据保存到 Preferences 文件夹下,如果需要即刻将数据存储,使用 [defaults synchronize]。



Preference(偏好设置)plist文件(序列化)都是保存在 plist 文件中,但是plist文件(序列化)操作读取时需要把整个plist文件都进行读取,而Preference(偏好设置) 可以直接通过 key-value单个读取。



归档解归档


要使用归档,其归档对象必须实现NSCoding协议



NSCoding协议声明的两个方法都必须实现。

encodeWithCoder:用来说明如何将对象编码到归档中。

initWithCoder:用来说明如何进行解档来获取一个新对象。



数据存储与读取的实例:

/**
归档
*/
- (void)keyedArchiver{
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES).firstObject;
NSString *file = [path stringByAppendingPathComponent:@"person.data"];
Person *person = [[Person alloc] init];
person.name = @"wangjiajia";
[NSKeyedArchiver archiveRootObject:person toFile:file];
}

/**
解档
*/
- (void)keyedUnarchiver{
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES).firstObject;
NSString *file = [path stringByAppendingPathComponent:@"person.data"];
Person *person = [NSKeyedUnarchiver unarchiveObjectWithFile:file];
if (person) {
NSLog(@"name:%@",person.name);
}
}


SQLite3


以下代码块将会介绍sqlite3.h中主要的API。

/* 打开数据库 */
int sqlite3_open(
const char *filename, /* 数据库路径(UTF-8) */
sqlite3 **pDb /* 返回的数据库句柄 */
);

/* 执行没有返回的SQL语句 */
int sqlite3_exec(
sqlite3 *db, /* 数据库句柄 */
const char *sql, /* SQL语句(UTF-8) */
int (*callback)(void*,int,char**,char**), /* 回调的C函数指针 */
void *arg, /* 回调函数的第一个参数 */
char **errmsg /* 返回的错误信息 */
);

/* 执行有返回结果的SQL语句 */
int sqlite3_prepare_v2(
sqlite3 *db, /* 数据库句柄 */
const char *zSql, /* SQL语句(UTF-8) */
int nByte, /* SQL语句最大长度,-1表示SQL支持的最大长度 */
sqlite3_stmt **ppStmt, /* 返回的查询结果 */
const char **pzTail /* 返回的失败信息*/
);

/* 关闭数据库 */
int sqlite3_close(sqlite3 *db);


处理SQL返回结果的一些API

#pragma mark - 定位记录的方法
/* 在查询结果中定位到一条记录 */
int sqlite3_step(sqlite3_stmt *stmt);
/* 获取当前定位记录的字段名称数目 */
int sqlite3_column_count(sqlite3_stmt *stmt);
/* 获取当前定位记录的第几个字段名称 */
const char * sqlite3_column_name(sqlite3_stmt *stmt, int iCol);
# pragma mark - 获取字段值的方法
/* 获取二进制数据 */
const void * sqlite3_column_blob(sqlite3_stmt *stmt, int iCol);
/* 获取浮点型数据 */
double sqlite3_column_double(sqlite3_stmt *stmt, int iCol);
/* 获取整数数据 */
int sqlite3_column_int(sqlite3_stmt *stmt, int iCol);
/* 获取文本数据 */
const unsigned char * sqlite3_column_text(sqlite3_stmt *stmt, int iCol);


由于其他API相对来说比较简单,这里就只给出执行有返回结果的SQL语句的实例

/* 执行有返回值的SQL语句 */
- (NSArray *)executeQuery:(NSString *)sql{
NSMutableArray *array = [NSMutableArray array];
sqlite3_stmt *stmt; //保存查询结果
//执行SQL语句,返回结果保存在stmt中
int result = sqlite3_prepare_v2(_database, sql.UTF8String, -1, &stmt, NULL);
if (result == SQLITE_OK) {
//每次从stmt中获取一条记录,成功返回SQLITE_ROW,直到全部获取完成,就会返回SQLITE_DONE
while( SQLITE_ROW == sqlite3_step(stmt)) {
//获取一条记录有多少列
int columnCount = sqlite3_column_count(stmt);
//保存一条记录为一个字典
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
for (int i = 0; i < columnCount; i++) {
//获取第i列的字段名称
const char *name = sqlite3_column_name(stmt, i);
//获取第i列的字段值
const unsigned char *value = sqlite3_column_text(stmt, i);
//保存进字典
NSString *nameStr = [NSString stringWithUTF8String:name];
NSString *valueStr = [NSString stringWithUTF8String:(const char *)value];
dict[nameStr] = valueStr;
}
[array addObject:dict];//添加当前记录的字典存储
}
sqlite3_finalize(stmt);//stmt需要手动释放内存
stmt = NULL;
NSLog(@"Query Stmt Success");
return array;
}
NSLog(@"Query Stmt Fail");
return nil;
}


在使用数据库存储时主要存在以下步骤。



1、创建数据库

2、创建数据表

3、数据的“增删改查”操作

4、关闭数据库



在使用sqlite3时,数据表的创建及数据的增删改查都是通过sql语句实现的。下面是一些常用的SQL语句。



创建表:

create table 表名称(字段1,字段2,……,字段n,[表级约束])[TYPE=表类型];

插入记录:

insert into 表名(字段1,……,字段n) values (值1,……,值n);

删除记录:

delete from 表名 where 条件表达式;

修改记录:

update 表名 set 字段名1=值1,……,字段名n=值n where 条件表达式;

查看记录:

select 字段1,……,字段n from 表名 where 条件表达式;



FMDB


FMDB是一种第三方的开源库,FMDB就是对SQLite的API进行了封装,加上了面向对象的思想,让我们不必使用繁琐的C语言API函数,比起直接操作SQLite更加方便。


FMDB主要是使用以下三个类


FMDatabase : 一个单一的SQLite数据库,用于执行SQL语句。

FMResultSet :执行查询一个FMDatabase结果集。

FMDatabaseQueue :在多个线程来执行查询和更新时会使用这个类。



一般的FMDB数据库操作4个:


创建数据库

打开数据库、关闭数据库

执行更新的SQL语句

执行查询的SQL语句



创建数据库

/**
创建数据库
*/
- (void)createDatabase{
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES).firstObject;
NSString *filePath = [path stringByAppendingPathComponent:@"FMDB.db"];
NSLog(@"数据库路径:%@",filePath);
/**
1. 如果该路径下已经存在该数据库,直接获取该数据库;
2. 如果不存在就创建一个新的数据库;
3. 如果传@"",会在临时目录创建一个空的数据库,当数据库关闭时,数据库文件也被删除;
4. 如果传nil,会在内存中临时创建一个空的数据库,当数据库关闭时,数据库文件也被删除;
*/
self.database = [FMDatabase databaseWithPath:filePath];
}


打开关闭数据库

/* 打开数据库,成功返回YES,失败返回NO */
- (BOOL)open;
/* 关闭数据库,成功返回YES,失败返回NO */
- (BOOL)close;


执行更新的SQL语句

在FMDB里除了查询操作,其他数据库操作都称为更新。而更新操作FMDB给出以下四种方法。

/* 1. 直接使用完整的SQL更新语句 */
[self.database executeUpdate:@"insert into mytable(num,name,sex) values(0,'wangjiajia1','m');"];

NSString *sql = @"insert into mytable(num,name,sex) values(?,?,?);";
/* 2. 使用不完整的SQL更新语句,里面含有待定字符串"?",需要后面的参数进行替代 */
[self.database executeUpdate:sql,@1,@"wangjiajia2",@"m"];

/* 3. 使用不完整的SQL更新语句,里面含有待定字符串"?",需要数组参数里面的参数进行替代 */
[self.database executeUpdate:sql
withArgumentsInArray:@[@2,@"wangjiajia3",@"m"]];

/* 4. SQL语句字符串可以使用字符串格式化,这种我们应该比较熟悉 */
[self.database executeUpdateWithFormat:@"insert into mytable(num,name,sex) values(%d,%@,%@);",4,@"wangjiajia4",@"m"];


执行查询的SQL语句

查询方法与更新方法类似,只不过查询方法存在返回值,可以通过返回值给出的相应方法获取需要的数据。

/* 执行查询SQL语句,返回FMResultSet查询结果 */
- (FMResultSet *)executeQuery:(NSString*)sql, ... ;
- (FMResultSet *)executeQueryWithFormat:(NSString*)format, ... ;
- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray *)arguments;


执行查询语句实例如下

- (NSArray *)getResultFromDatabase{
//执行查询SQL语句,返回查询结果
FMResultSet *result = [self.database executeQuery:@"select * from mytable"];
NSMutableArray *array = [NSMutableArray array];
//获取查询结果的下一个记录
while ([result next]) {
//根据字段名,获取记录的值,存储到字典中
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
int num = [result intForColumn:@"num"];
NSString *name = [result stringForColumn:@"name"];
NSString *sex = [result stringForColumn:@"sex"];
dict[@"num"] = @(num);
dict[@"name"] = name;
dict[@"sex"] = sex;
//把字典添加进数组中
[array addObject:dict];
}
return array;
}


多线程安全FMDatabaseQueue

由于在多线程同时操作FMDatabase对象时,会造成数据混乱的问题,FMDB提供了一个可以确保线程安全的类(FMDatabaseQueue)。

FMDatabaseQueue的使用比较简单

//创建多线程安全队列对象
self.queue = [FMDatabaseQueue databaseQueueWithPath:filePath];
//在block块内自行相关数据库操作即可
[self.queue inDatabase:^(FMDatabase * _Nonnull db) {
}];


事务

事务,是指作为单个逻辑工作单元执行的一系列操作,要么完整地执行,要么完全地不执行。


比如要更新数据库的大量数据,我们需要确保所有的数据更新成功,才采取这种更新方案,如果在更新期间出现错误,就不能采取这种更新方案了,这就是事务的用处。只有事务提交了,开启事务期间的操作才会生效。

以下是事务的例子

//事务
-(void)transaction {
// 开启事务
[self.database beginTransaction];
BOOL isRollBack = NO;
@try {
for (int i = 0; i<500; i--) {
NSNumber *num = @(i+6);
NSString *name = [[NSString alloc] initWithFormat:@"student_%d",i];
NSString *sex = (i%2==0)?@"f":@"m";
NSString *sql = @"insert into mytable(num,name,sex) values(?,?,?);";
BOOL result = [self.database executeUpdate:sql,num,name,sex];
if ( !result ) {
NSLog(@"插入失败!");
isRollBack = YES;
return;
}
}
}
@catch (NSException *exception) {
isRollBack = YES;
NSLog(@"插入失败,事务回退");
// 事务回退
[self.database rollback];
}
@finally {
if (!isRollBack) {
NSLog(@"插入成功,事务提交");
//事务提交
[self.database commit];
}else{
NSLog(@"插入失败,事务回退");
// 事务回退
[self.database rollback];
}
}
}

//多线程安全事务实例
- (void)transactionByQueue {
//开启事务
[self.queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
BOOL isRollBack = NO;
for (int i = 0; i<500; i++) {
NSNumber *num = @(i+1);
NSString *name = [[NSString alloc] initWithFormat:@"student_%d",i];
NSString *sex = (i%2==0)?@"f":@"m";
NSString *sql = @"insert into mytable(num,name,sex) values(?,?,?);";
BOOL result = [db executeUpdate:sql,num,name,sex];
if ( !result ) {
isRollBack = YES;
return;
}
}

//当最后*rollback的值为YES的时候,事务回退,如果最后*rollback为NO,事务提交
*rollback = isRollBack;
}];
}



CoreData







CoreData核心结构图.png


以下是CoreData常用类的作用描述



PersistentObjectStore:存储持久对象的数据库(例如SQLite,注意CoreData也支持其他类型的数据存储,例如xml、二进制数据等)。

ManagedObjectModel:对象模型,对应Xcode中创建的模型文件。

PersistentStoreCoordinator:对象模型和实体类之间的转换协调器,用于管理不同存储对象的上下文。

ManagedObjectContext:对象管理上下文,负责实体对象和数据库之间的交互。



CoreData主要工作原理如下



读取数据库的数据时,数据库数据先进入数据解析器,根据对应的模板,生成对应的关联对象。

向数据库插入数据时,对象管理器先根据实体描述创建一个空对象,对该对象进行初始化,然后经过数据解析器,根据对应的模板,转化为数据库的数据,插入数据库中。

更新数据库数据时,对象管理器需要先读取数据库的数据,拿到相互关联的对象,对该对象进行修改,修改的数据通过数据解析器,转化为数据库的更新数据,对数据库更新。



CoreData的使用步骤如下



1.添加框架。

2.数据模板和对象模型。

3.创建对象管理上下文。

4.数据的增删改查操作。



在其中第二步的时候要注意,Xcode 8.0 之后和之前的Xcode 版本是有一些区别的。在 8.0之后创建.xcdatamodeld文件之后,添加实体之后会自动生成对应的对应类文件。

收起阅读 »

iOS KVO的原理&&KVO的isa指向

一。简单复习一下KVO的使用 定义一个类,继承自NSObject,并添加一个名称的属性 #import NS_ASSUME_NONNULL_BEGIN @interface TCPerson : NSObject @property (nonato...
继续阅读 »

一。简单复习一下KVO的使用



  • 定义一个类,继承自NSObject,并添加一个名称的属性


#import 

NS_ASSUME_NONNULL_BEGIN

@interface TCPerson : NSObject

@property (nonatomic, copy) NSString *name;

@end

NS_ASSUME_NONNULL_END



  • 在ViewController我们简单的使用一下KVO


#import "ViewController.h"
#import "TCPerson.h"
@interface ViewController ()
@property (nonatomic, strong) TCPerson *person1;
@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[TCPerson alloc]init];
self.person1.name = @"liu yi fei";
[self.person1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
}

/// 点击屏幕出发改变self.person1的name
/// @param touches touches description
/// @param event event description
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
self.person1.name = @"cang lao shi";
}

/// 监听回调
/// @param keyPath 监听的属性名字
/// @param object 被监听的对象
/// @param change 改变的新/旧值
/// @param context context description
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
NSLog(@"监听到%@对象的%@发生了改变%@",object,keyPath,change);
}

/// 移除观察者
- (void)dealloc{
[self.person1 removeObserver:self forKeyPath:@"name"];
}
@end

当点击屏幕的时候,控制台输出:


2020-09-24 15:53:52.527734+0800 KVO_TC[9255:98204] 监听到对象的name发生了改变{
kind = 1;
new = "cang lao shi";
old = "liu yi fei";
}

二。深入剖析KVO的突破



  • 在-(void)touchesBegan:(NSSet*)触摸事件:(UIEvent *)事件{

    self.person1.name = @“苍老市”;

    }我们知道
    self.person1.name的本质是[self.person1 setName:@“ cang lao shi”];


- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
// self.person1.name = @"cang lao shi";
[self.person1 setName:@"cang lao shi"];
}

在TCPerson的.m文件,我们从写setter方法并打断点,可以看到当我们点击屏幕的时候,我们发现进入了setter方法:


- (void)setName:(NSString *)name{
_name = name;
}


  • 在ViewController我们新建一个person2,代码变成了:


#import "ViewController.h"
#import "TCPerson.h"
@interface ViewController ()
@property (nonatomic, strong) TCPerson *person1;
@property (nonatomic, strong) TCPerson *person2;
@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
self.person1 = [[TCPerson alloc]init];
self.person1.name = @"liu yi fei";
[self.person1 addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];


self.person2 = [[TCPerson alloc] init];
self.person2.name = @"yyyyyyyy";
}

/// 点击屏幕出发改变self.person1的name
/// @param touches touches description
/// @param event event description
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
self.person1.name = @"cang lao shi";
// [self.person1 setName:@"cang lao shi"];

self.person2.name = @"ttttttttt";
}

/// 监听回调
/// @param keyPath 监听的属性名字
/// @param object 被监听的对象
/// @param change 改变的新/旧值
/// @param context context description
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context{
NSLog(@"监听到%@对象的%@发生了改变%@",object,keyPath,change);
}

/// 移除观察者
- (void)dealloc{
[self.person1 removeObserver:self forKeyPath:@"name"];
}
@end


  • 注意:当我们点击屏幕的时候输出的结果是:


2020-09-24 16:10:36.750153+0800 KVO_TC[9313:105906] 监听到对象的name发生了改变{
kind = 1;
new = "cang lao shi";
old = "liu yi fei";
}




  • 既然我们改变名称的值的时候走的都是setName:setter方法,按理说观察属性变化的时候,person2的值也应该被观察到,为什么它不会观察到person2?




三.KVO的isa指向



  • 上篇文章中我分析了实例对象,类对象,元类对象的isa,既然当我们改变属性值的时候,其本质是调用setter方法,那么在KVO中,person1和person2的setName方法应该存储在类对象中,我们先来看看这两个实例对象的isa指向:

    打开lldb


(lldb) p self.person1.isa
(Class) $0 = NSKVONotifying_TCPerson
Fix-it applied, fixed expression was:
self.person1->isa
(lldb) p self.person2.isa
(Class) $1 = TCPerson
Fix-it applied, fixed expression was:
self.person2->isa
(lldb)


  • 从上面的打印我们看到self.person1的isa指向了NSKVONotifying_TCPerson,而没有添加观察着的self.person2的isa却指向的是TCPerson

  • NSKVONotifying_TCPerson是运行时动态创建的类,继承自TCPerson,其内部实现可以看成(模拟的NSKVONotifying_TCPerson流程,下面的代码不能在xcode中运行):


#import "NSKVONotifying_TCPerson.h"

@implementation NSKVONotifying_TCPerson
//NSKVONotifying_TCPerson的set方法实现,其本质来自于foundation框架
- (void)setName:(NSString *)name{
_NSSetIntVaueAndNotify();
}
//改变过程
void _NSSetIntVaueAndNotify(){
[self willChangeValueForKey:@"name"];
[super setName:name];
[self didChangeValueForKey:@"name"];
}
//通知观察者
- (void)didChangeValueForKey:(NSString *key){
[observe observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context];
}
@end
未添加观察self.person2实例对象的isa指向流程图:

添加观察self.person1实例对象的isa指向流程图:


所以KVO其本质是动态生成一个NSKVONotifying_TCPerson类,继承自TCPerson,当实例对象添加观察着之后,实例对象的isa指向了这个动态创建的类,当其属性发生改变时,调用的是该类的setter方法,而不是父类的类对象中的setter方法

作者:枫紫_6174
原贴链接:https://www.jianshu.com/p/0b6083b91b04
收起阅读 »

iOS Runloop的深入理解

iOS
首先了解一下程序、进程和线程 程序本身只是指令、数据及其组织形式的描述,进程才是程序(那些指令和数据)的真正运行实例。而线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可...
继续阅读 »

首先了解一下程序、进程和线程


程序本身只是指令、数据及其组织形式的描述,进程才是程序(那些指令和数据)的真正运行实例。而线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。


一般来说,一个线程一次只能执行一个任务,执行完成后线程就会退出。所以程序运行的时候,需要一个机制,让线程能随时处理事件但不退出。


以前写游戏的时候就写过这样的东西,通常是一个 do while 循环,让程序一直运转,直到接收到退出信息。


而 RunLoop 就是让线程在没有处理消息时休眠以避免资源占用,在有消息到来时立刻被唤醒。


线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时默认是没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。


在 CoreFoundation 里面关于 RunLoop 有5个类:


CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef


他们的层级关系为,一个 RunLoop 对象包含若干个 Mode 对象,每个 Mode 又包含若干个 Source/Timer/Observer,RunLoop 在运作的时候,一次只能运作与一个 Mode 之下,如果需要切换 Mode,需要退出 Loop 才能重新指定一个 Mode。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。


而一个 Source 对象是一个事件,Source 有两个版本:Source0 和 Source1,Source0 只包含一个函数指针,并不能主动触发,需要将 Source0 标记为待处理,在 RunLoop 运转的时候,才会处理这个事件(如果 RunLoop 处于休眠状态,则不会被唤醒去处理),而 Source1 包含了一个 mach_port 和一个函数指针,mach_port 是 iOS 系统提供的基于端口的输入源,可用于线程或进程间通讯。而 RunLoop 支持的输入源类型中就包括基于端口的输入源,可以做到对 mach_port 端口源事件的监听。所以监听到 source1 端口的消息时,RunLoop 就会自己醒来去执行 Source1 事件(也能称为被消息唤醒)。也就是 Source0 是直接添加给 RunLoop 处理的事件,而 Source1 是基于端口的,进程或线程之间传递消息触发的事件(为什么要 0 和 1 来命名,每次都记不住,GG)。


Timer 是基于时间的触发器,CFRunLoopTimerRef 和 NSTimer 可以通过 Toll-free bridging 技术混用,Toll-free bridging 是一种允许某些 ObjC 类与其对应的 CoreFoundation 类之间可以互换使用的机制,当将 Timer 加入到 RunLoop 时,RunLoop 会注册对应的时间点,当时间点到时,RunLoop 会被唤醒以执行 Timer 回调。


Observer(观察者)都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:


typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop
};


也就是可以在这几个时机去安排 RunLoop 执行一些其他的任务。


上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。


RunLoop 的 CFRunLoopMode 和 CFRunLoop 的结构大致如下:


struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};

struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes; // Set
...
};


这里有个概念叫 "CommonModes":一个 Mode 可以将自己标记为"Common"属性(通过将其 ModeName 添加到 RunLoop 的 "commonModes" 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 "Common" 标记的所有Mode里。


如上文说的 RunLoop 一次循环只能运行在一个 Mode 下,是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。但如果一个 Source/Timer/Observer 想在多个 Mode 下运作,则可以分别加入到多个 Mode,或者给两个 Mode 添加 "Common" 标记,再将 Source/Timer/Observer 加入到 RunLoop 的 "commonModeItems" 。


应用场景举例:主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。这两个 Mode 都已经被标记为"Common"属性。DefaultMode 是 App 平时所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。当你创建一个 Timer 并加到 DefaultMode 时,Timer 会得到重复回调,但此时滑动一个TableView时,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,如果想让它回调则可以将这个 Timer 分别加入这两个 Mode。或将 Timer 加入到顶层的 RunLoop 的 "commonModeItems" 。


CFRunLoop 对外暴露的管理 Mode 接口只有下面2个:


CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);


Mode 暴露的管理 mode item 的接口有下面几个:


CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);


可以注意到 mode 并不像 Source/Timer/Observer 一样有 Remove 方法,所以 mode 只能增加,不能减少。


当你传入一个新的 mode name 但 RunLoop 内部没有对应 mode 时,RunLoop会自动帮你创建对应的 CFRunLoopModeRef。


接下来看 RunLoop 的执行逻辑


按 1 - 10 来理理,首先要知道 Observer 是观察者,也就是下面这几种状态都会通知观察者,开发者也添加一个观察者,去在以下几种状态的时候,执行一些任务,比如将没啥实时性要求的东西,在即将进入休眠状态时执行。
    kCFRunLoopEntry         = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop

其中第 2 步虽然通知 Observer 即将处理 Timer,但其实并没有真的即将处理 Timer 回调,这个通知每次 Loop 循环都会调用,但 Timer 只有在注册的时间到了才会在第 9 步去执行,第 4 步处理运行的 mode 里待处理的 Source0,其中第 5 步会判断 mode item 里是否有 Source1 处于 ready 状态(也就是 Source1 的端口已经收到消息),有的话跳到第 9 步,处理 Source1 事件,然后进入下一个循环,没有的话说明 mode item 里的事件都处理完毕,线程进入休眠状态,等待 Source1,Timer 或者外部手动将 RunLoop 唤醒(上文说 Source0 并不能唤醒 RunLoop,所以一般会通过手动唤醒 RunLoop,来让 RunLoop 处理新加入进去的 Source0)。


可以看到,实际上 RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里,去执行加入到 RunLoop 里的 Source0,Timer 的回调,以及 Observer 回调,以及用于线程或进程间通讯的 Source1,当所有的都处理完之后,结束一次循环,进入休眠状态,休眠的时候等待 Timer 注册的时间点或者 Source1 唤醒 RunLoop(也可以手动唤醒)。


从上面我们可以了解,线程和进程之间的通讯是基于 mach port 传递消息实现的,这也是 RunLoop 的核心。有必要了解一下 mach port, OSX/iOS 的系统架构分为 4 层,从外到内为应用层,应用框架层,核心框架层,Darwin。应用层包括用户能接触到的图形应用,应用框架层即开发人员接触到的 Cocoa 等框架,核心框架层包括各种核心框架、OpenGL 等内容,Darwin 即操作系统的核心,包括系统内核、驱动、Shell 等内容。

Darwin 核心的架构:


其中,在硬件层上面的三个组成部分:Mach、BSD、IOKit (还包括一些上面没标注的内容),共同组成了 XNU 内核。


XNU 内核的内环被称作 Mach,其作为一个微内核,仅提供了诸如处理器调度、IPC (进程间通信)等非常少量的基础服务。


BSD 层可以看作围绕 Mach 层的一个外环,其提供了诸如进程管理、文件系统和网络等功能。

IOKit 层是为设备驱动提供了一个面向对象(C++)的一个框架。


在 Mach 中,进程、线程和虚拟内存都被称为"对象"。Mach 的对象间不能直接调用,只能通过消息传递的方式实现对象间的通信。"消息"是 Mach 中最基础的概念,消息在两个端口 (port) 之间传递,这就是 Mach 的 IPC (进程间通信) 的核心。


Mach 中的对象通过一个 Mach 端口发送一个消息,消息中会携带目标端口,这个消息会从用户空间传递到内核空间,再由内核空间传递到目标端口,实现线程或进程之间的通讯。(也就是线程或进程之间的通讯不能绕过系统内核)。目标端口接收到消息,因为 RunLoop 会对 mach_port 端口源进行监听,如果 RunLoop 此时处于休眠状态,则被唤醒,便可以处理已经接收到消息的 source1 事件。

RunLoop 实现了很多功能


启动后,系统默认注册了5个Mode:


1:kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。


2:UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。


3:UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。


4:GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。


5:kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用。


AutoreleasePool


App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()(因为需要设置不同的优先级,所以注册两个)。


第一个 Observer 监视的事件是 Entry(即将进入Loop),用来创建自动释放池,且设置的优先级最高,保证创建释放池发生在其他所有回调之前。


第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时去释放旧的池并创建新池;Exit(即将退出Loop) 时释放自动释放池。这个 Observer 的优先级最低,保证其释放池子发生在其他所有回调之后。


在主线程执行的代码,通常是写在诸如事件回调、Timer 回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏。


事件响应


苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。


当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的事件传递。


_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。


手势识别


当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。


苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个 Observer 的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行 GestureRecognizer 的回调。


当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。


界面更新


当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay 方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。


苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:

_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。也就是 UI 是在界面 RunLoop 休眠之前更新的,所以如果想在 UI 更新之后做一些事情,可以注册一个 Observer 监听 kCFRunLoopAfterWaiting(刚从休眠中唤醒)。

定时器


NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。


如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。


CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。


PerformSelecter


当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。


当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。


GCD


当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。


网络请求


iOS 中,关于网络请求的接口自下至上有如下几层:


CFSocket
CFNetwork ->ASIHttpRequest
NSURLConnection ->AFNetworking
NSURLSession ->AFNetworking2, Alamofire


通常使用 NSURLConnection 时,你会传入一个 Delegate,当调用了 [connection start] 后,这个 Delegate 就会不停收到事件回调。实际上,start 这个函数的内部会会获取 CurrentRunLoop,然后在其中的 DefaultMode 添加了多个需要手动触发的 Source0。


当开始网络传输时,NSURLConnection 创建了两个新线程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket 线程是处理底层 socket 连接的。NSURLConnectionLoader 这个线程内部会使用 RunLoop 基于 mach port 的 Source1 接收来自底层 CFSocket 的消息,当收到消息后,其会在合适的时机将 Source0 标记为待处理,同时唤醒 Delegate 线程的 RunLoop 来让其处理 Source0。完成一个由 CFSocket 线程到网络请求所在线程的数据处理。

AFNetworking


AFURLConnectionOperation 这个类是基于 NSURLConnection 构建的,其希望能在后台线程接收 Delegate 回调。为此 AFNetworking 创建了一个线程,并在这个线程中启动了一个 RunLoop,RunLoop 启动前内部必须要有至少一个 Timer/Observer/Source,所以 AFNetworking 在 [runLoop run] 之前先创建了一个新的 NSMachPort 添加进去了。只是为了让 RunLoop 不至于退出,并没有用于实际的发送消息。当需要这个后台线程执行网络请求任务时,AFNetworking 通过调用 [NSObject performSelector:onThread:..] 将这个任务扔到了该线程的 RunLoop 中。


AsyncDisplayKit


AsyncDisplayKit 是 Facebook 推出的用于保持界面流畅性的框架,其原理大致如下:


UI 线程中一旦出现繁重的任务就会导致界面卡顿,这类任务通常分为3类:排版,绘制,UI对象操作。


排版通常包括计算视图大小、计算文本高度、重新计算子式图的排版等操作。

绘制一般有文本绘制 (例如 CoreText)、图片绘制 (例如预先解压)、元素绘制 (Quartz)等操作。

UI对象操作通常包括 UIView/CALayer 等 UI 对象的创建、设置属性和销毁。


其中前两类操作可以通过各种方法扔到后台线程执行,而最后一类操作只能在主线程完成,并且有时后面的操作需要依赖前面操作的结果 (例如TextView创建时可能需要提前计算出文本的大小)。ASDK 所做的,就是尽量将能放入后台的任务放入后台,不能的则尽量推迟 (例如视图的创建、属性的调整)。

为此,ASDK 创建了一个名为 ASDisplayNode 的对象,并在内部封装了 UIView/CALayer,它具有和 UIView/CALayer 相似的属性,例如 frame、backgroundColor 等。所有这些属性都可以在后台线程更改,开发者可以只通过 Node 来操作其内部的 UIView/CALayer,这样就可以将排版和绘制放入了后台线程。但是无论怎么操作,这些属性总需要在某个时刻同步到主线程的 UIView/CALayer 去。


ASDK 仿照 QuartzCore/UIKit 框架的模式,实现了一套类似的界面更新的机制:即在主线程的 RunLoop 中添加一个 Observer,监听了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理的任务(需要将 Node 的属性同步到主线程的 UIView/CALayer 去的任务),然后一一执行。


总结:


ibireme深入理解RunLoop 前前后后我看了好几遍,每次都看的不深入,总是瞬时了解,睡一觉就记不清楚细节了,这次自己逐行去理解,并写下来,加上点自己的理解,也写了点代码去观察 RunLoop,算是对 RunLoop 的理解更加深入了。


作者:Cooci
原帖链接:https://www.jianshu.com/p/2103d15f4423

收起阅读 »

Runtime的底层原理及应用

Runtime 是一个运行时库,主要使用 C 和汇编写的库,为 C 添加了面向对象的能力并创造了 Objective-C,并且拥有消息分发,消息转发等功能。也就是 Runtime 涉及三个点,面向对象消息分发消息转发。面向对象:Objective-C 的对象是...
继续阅读 »

Runtime 是一个运行时库,主要使用 C 和汇编写的库,为 C 添加了面向对象的能力并创造了 Objective-C,并且拥有消息分发,消息转发等功能。



也就是 Runtime 涉及三个点,

  • 面向对象
  • 消息分发
  • 消息转发

面向对象:

Objective-C 的对象是基于 Runtime 创建的结构体。先从代码层面分析一下。

Class *class = [[Class alloc] init];

alloc 方法会为对象分配一块内存空间,空间的大小为 isa_t(8 字节)的大小加上所有成员变量所需的空间,再进行一次内存对齐。分配完空间后会初始化 isa_t ,而 isa_t 是一个 union 类型的结构体(或者称之为联合体),它的结构是在 Runtime 里被定义的。


union isa_t {  
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }

Class cls;
uintptr_t bits;

struct {
uintptr_t indexed : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33;
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
};
};


从 isa_t 的结构可以看出,isa_t 可以存储 struct,uintptr_t 或者 Class 类型


init 方法就直接返回了初始化好的对象,class 指针指向这个初始化好的对象。


也就是在 Runtime 的协助之下,一个对象完成了创建。


你可能想知道,这个对象只存放了一个 isa_t 结构体和成员变量,对象的方法在哪里?


在编译的时候,类在内存中的位置就已经确定,而在 main 方法之前,Runtime 将可执行文件中和动态库所有的符号(Class,Protocol,Selector,IMP,…)加载到内存中,由 Runtime 管理,这里也包括了也是一个对象的类。


类对象里储存着一个 isa_t 的结构体,super_class 指针,cache_t 结构体,class_data_bits_t 指针。


struct objc_class : objc_object {
isa_t isa;
Class superclass;
cache_t cache;
class_data_bits_t bits;



class_data_bits_t 指向类对象的数据区域,数据区域存放着这个类的实例方法链表。而类方法存在元类对象的数据区域。也就是有对象,类对象,元类对象三个概念,对象是在运行时动态创建的,可以有无数个,类对象和元类对象在 main 方法之前创建的,分别只会有一个。


消息分发


在 Objective-C 中的“方法调用”其实应该叫做消息传递,[object message] 会被编译器翻译为 objc_msgSend(object, @selector(message)),这是一个 C 方法,首先看它的两个参数,第一个是 object ,既方法调用者,第二个参数称为选择子 SEL,Objective-C 为我们维护了一个巨大的选择子表,在使用 @selector() 时会从这个选择子表中根据选择子的名字查找对应的 SEL。如果没有找到,则会生成一个 SEL 并添加到表中,在编译期间会扫描全部的头文件和实现文件将其中的方法以及使用 @selector() 生成的选择子加入到选择子表中。


通过第一个参数 object,可以找到 object 对象的 isa_t 结构体,从上文中能看 isa_t 结构体的结构,在 isa_t 结构体中,shiftcls 存放的是一个 33 位的地址,用于指向 object 对象的类对象,而类对象里有一个 cache_t 结构体,来看一下 cache_t 的具体代码


struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;



_mask:分配用来缓存 bucket 的总数。

_occupied:表明目前实际占用的缓存 bucket 的个数。

_buckets:一个散列表,用来方法缓存,bucket_t 类型,包含 key 以及方法实现 IMP。


struct bucket_t {
private:
cache_key_t _key;
IMP _imp;



objc_msgSend() 方法会先从缓存表里,查找是否有该 SEL 对应的 IMP,有的话算命中缓存,直接通过函数指针 IMP ,找到方法的具体实现函数,执行。


当然缓存表里可能并不会命中,则此时会根据类对象的 class_data_bits_t 指针找到数据区域,数据区域里用链表存放着类的实例方法,实例方法也是一个结构体,其结构为:


struct method_t {  
SEL name;
const char *types;
IMP imp;
};


编译器将每个方法的返回值和参数类型编码为一个字符串,types 指向的就是这样一个字符串,objc_msgSend() 会在类对象的方法链表里按链表顺序去匹配 SEL,匹配成功则停止,并将此方法加入到类对象的 _buckets 里缓存起来。如果没找到则会通过类对象的 superclass 指针找到其父类,去父类的方法列表里寻找(也会从父类的方法缓存列表开始)。


如果继续没有找到会一直向父类寻找,直到遇见 NSObject,NSObject 的 superclass 指向 nil。也就意味着寻找结束,并没有找到实现方法。(如果这个过程找到了,也同样会在 object 的类对象的 _buckets 里缓存起来)。


选择子在当前类和父类中都没有找到实现,就进入了方法决议(method resolve),首先判断当前 object 的类对象是否实现了 resolveInstanceMethod: 方法,如果实现的话,会调用 resolveInstanceMethod:方法,这个时候我们可以在 resolveInstanceMethod:方法里动态的添加该 SEL 对应的方法(也可以去做点别的,比如写入日志)。之后会重新执行查找方法实现的流程,如果依旧没找到方法,或者没有实现 resolveInstanceMethod: 方法,Runtime 还有另一套机制,消息转发。


消息转发


消息转发分为以下几步:


1.调用 forwardingTargetForSelector: 方法,尝试找到一个能响应该消息的对象。如果获取到,则直接转发给它。如果返回了 nil,继续下面的动作。


2.调用 methodSignatureForSelector: 方法,尝试获得一个方法签名。如果获取不到,则直接调用 doesNotRecognizeSelector 抛出异常。


3.调用 forwardInvocation: 方法,将第 2 步获取到的方法签名包装成 Invocation 传入,如何处理就在这里面了。


以上三个方法都可以通过在 object 的类对象里实现, forwardingTargetForSelector: 可以通过对参数 SEL 的判断,返回一个可以响应该消息的对象。这样则会重新从该对象开始执行查找方法实现的流程,找到了也同样会在 object 的类对象的 _buckets 里缓存起来。而 2,3 方法则一般是配套使用,实现 methodSignatureForSelector: 方法根据参数 SEL ,做相应处理,返回 NSMethodSignature (方法签名) 对象,NSMethodSignature 对象会被包装成 NSInvocation 对象,forwardInvocation: 方法里就可以对 NSInvocation 进行处理了。


上面是讲的是实例方法,类方法没什么区别,类方法储存在元类对象的数据区域里,通过类对象的 isa_t 找到元类对象,执行查找方法实现的流程,元类对象的 superclass 最终也会指向 NSObject。没找到的话,也会有方法决议以及消息转发。


runtime 可以做什么:


实现多继承:从 forwardingTargetForSelector: 方法就能知道,一个类可以做到继承多个类的效果,只需要在这一步将消息转发给正确的类对象就可以模拟多继承的效果。


交换两个方法的实现


    Method m1 = class_getInstanceMethod([M1 class], @selector(hello1));
Method m2 = class_getInstanceMethod([M2 class], @selector(hello2));
method_exchangeImplementations(m2, m1);


关联对象


通过下面两个方法,可以给 category 实现添加成员变量的效果。


objc_setAssociatedObject
objc_getAssociatedObject


动态添加类和方法:


objc_allocateClassPair 函数与 objc_registerClassPair 函数可以完成一个新类的添加,class_addMethod 给类添加方法,class_addIvar 添加成员变量,objc_registerClassPair 来注册类,其中成员变量的添加必须在类注册之前,类注册后就可以创建该类的对象了,而再添加成员变量就会破坏创建的对象的内存结构。


将 json 转换为 model


用到了 Runtime 获取某一个类的全部属性的名字,以及 Runtime 获取属性的类型。



作者:Cooci
原帖链接:https://www.jianshu.com/p/2dae81846046
收起阅读 »

OpenGL中的图片渲染流程

在OpenGL中的,我们通常对图片或者视频进行渲染或者颜色的重新的绘制,那么这些过程是怎么实现的呢?我们通过客户端,来接收到不同的数据,坐标数据或者视频数据,根据不同的数据形式,我们选择不同的通道(传输方式)来传入到我们的接收器中,来处理不同的数据传递数据的三...
继续阅读 »

在OpenGL中的,我们通常对图片或者视频进行渲染或者颜色的重新的绘制,那么这些过程是怎么实现的呢?

我们通过客户端,来接收到不同的数据,坐标数据或者视频数据,根据不同的数据形式,我们选择不同的通道(传输方式)来传入到我们的接收器中,来处理不同的数据


传递数据的三种方式

1.传递数据处理的流程:顶点着色器--->光栅化/图元装配--->片元着色器--->渲染完成

2.TextureData(纹理)、Uniforms:可以直接的传递到顶点着色器或者片元着色器中。
    Attributes(属性):只能传递到顶点着色器中,进过处理后的数据可以传递到片元着色器中

3.着色器中的是我们可以控制的,但是光栅化/图元装配是由系统来完成的,不可控

三种传输方式详解

1.Attributes:只能传递到顶点着色器中,通过处理可以传递到片元着色器中
   使用场景:当数据不停的进行变换时
   经常传递的数据:颜色数据、顶点数据、纹理坐标、关照法线

2.Uniform:可以直接传递数据到顶点/片元着色器中
   使用场景:比较统一的处理方式,不会发生太多变化时
   顶点着色器处理的场景:图形的旋转操作,每个顶点乘以旋转矩阵来完成(旋转矩阵基本是不怎么改变的)
   片元着色器处理场景:处理视频,视频解码之后是由一帧帧的图片来组成的,对视频的颜色空间进行渲染处理(在视频中常使用的颜色空间为YUV)将YUV颜色空间乘以矩阵转化为RGB颜色来处理视频

3.TextureData(纹理):颜色的填充,视频的处理。一般这里的数据不会传递到顶点着色器的,这里主要是用来处理图片的,一般不涉及到顶点数据的处理

收起阅读 »