一.NSObjcet对象在内存中占多少个字节?
- 点进去NSObject,发现NSObject内部只有一个Class.
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
- Class是一个
objc_class结构体
指针,指向objc_class
这个结构体
typedef struct objc_class *Class;
-
objc_class
内部实现
!__OBJC2__
代表:不是objc2这个版本才可以使用下面的定义;
Use Class instead of struct objc_class
:让我们使用Class这个结构体.
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */
4.从源码中找到Class结构体(从下往上看)
#:类最初始的信息,不包含catagory等的信息---ro的意思:只读
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
const uint8_t * ivarLayout;
const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
}
#:类的具体信息----rw的意思:可读可写
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint32_t version;
const class_ro_t *ro;//包含了上面的结构体
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
}
//因为是继承关系,所以objc_class里面也有一个isa指针
struct objc_class : objc_object {
// Class ISA;
Class superclass;
#方法缓存
cache_t cache; // formerly cache pointer and vtable
#:bits用来获取具体的类信息,bits&FAST_DATA_MASK就可以拿到struct class_rw_t的信息.
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
}
答案:
arm64架构的数据总线为64bits,也就是8bytes,所以在arm64架构里面一个指针占8个字节.
如果是arm32,那么就占4个字节.
拓展
demo1:
使用runtime的class_getInstanceSize()方法得到一个 类 的大小
内存对齐过的
使用malloc_size()得到的是实际分配的内存大小-----#import<malloc/malloc.h> -----5s,6等arm64架构下输出 ----- ----- NSLog(@"%zd", -----class_getInstanceSize([NSObject class]));//8 ----- ----- NSObject *objc = [[NSObject alloc]init]; ----- NSLog(@"%zd",malloc_size((__bridge const void *)(objc)));//16
- 可以看出来,虽然Class大小应该是8个字节,但是系统实际上给分配了16个字节.
所以NSObjcet对象在内存中占16个字节(arm64架构).demo2:
@interface Father: NSObject { int age; int weight; int height; } @end @implementation Father @end 打印:NSLog(@"%zd", class_getInstanceSize([Father class])); 输出:2019-01-10 10:14:24.498 iOSWorld[2913:385535] 24 Father *f = [[Father alloc]init]; NSLog(@"%zd",malloc_size((__bridge const void *)(f))); 输出32.
注意:因为Father继承自NSObject,所以它内部有NSObject的实现,那么它占的内存为8(NSObject的isa指针)+4x(Father的3个int类型) = 8 + 12 =20个字节
但是:因为结构体计算内存的时候,要考虑内存对齐(和数据总线有关
)的问题.
不满8个字节也要算8个字节,也就是必须是8的倍数,所以实际占用24个字节.
但但但又因为:苹果规定了每个对象在内存中至少16个字节,也就是16的倍数.所以f这个实例对象实际上占用了32个字节.(objc4源码的alloc方法里面有苹果的规定
)
结论:Father对象其实只需要24个字节的空间,但实际上给分配了32个字节.demo3:
@interface Father : NSObject { int age; } @end @implementation Father @end @interface Son : Father { int weight; } @end @implementation Son @end 打印:NSLog(@"%zd", class_getInstanceSize([Son class])); 输出:2019-01-10 10:48:00.284 iOSWorld[2940:390486] 16
father占了8(NSObject 指针) + 4(int类型) = 12
son 继承自 father,所以son的c++结构体应该张这样struct Son_IMPL{ struct Father_IMPL s; //16个字节 int weight;//4个字节 }
乍一看16+4= 20, 但是答案是16.
原因还在于内存对齐,毕竟Father_IMPL还有4个字节是空的,刚好容得下weight这个int
注意:
1.结构体内存对齐:结构体的大小必须为最大成员大小的倍数
用空间换时间,合理利用内存的地址.
2.OC中,对象的变量和方法不在同一区域,所以对象在内存中占的大小不包括方法.
解释:NSObject实际上使用了8个字节,但是apple规定分配了16个字节,所以NSObject实际上占用了16个字节(使用和占用不太一样哦
)
apple规定:如果一个对象分配的内存<16,那么就等于16.而且对象分配的内存为16的倍数
猜想:给8个字节太小了,放个指针就没有了,都没地方放成员变量,所以就以16为单位了吧😅
从源码看规定,下面是方法的依次调用1. + (id)allocWithZone:(struct _NSZone *)zone { return _objc_rootAllocWithZone(self, (malloc_zone_t *)zone); } 2. obj = class_createInstance(cls, 0); 3. class_createInstance(Class cls, size_t extraBytes) 4. _class_createInstanceFromZone(cls, extraBytes, nil); 5. 拿到size,分配内存 size_t size = cls->instanceSize(extraBytes); obj = (id)calloc(1, size); 6. size的实现 ,如果<16,则=16 size_t size = alignedInstanceSize() + extraBytes;//extraBytes为0 // CF requires all objects be at least 16 bytes. if (size < 16) size = 16; 7.alignedInstanceSize()的实现,也就是内存对齐的实现. unalignedInstanceSize是对象的实际大小,不经过内存对齐的.经过word_align后就是内存对齐过的. word_align(unalignedInstanceSize())
二.类的信息放在哪里?
*OC对象共3种:
1.实例对象:存放isa指针(指向类)和变量
2.类对象:存放isa指针(指向元类)、superClass、属性、对象方法、协议、成员变量
3.元类对象:存放isa指针(指向元类本身)、superClass、类方法
所有元类的isa指针都指向根元类(rootMetaClass)
根元类的superClasss指向RootClass
答案:类的信息放在元类里面
拓展:
@interface Father: NSObject { int age; } @end @implementation Father @end Father *father = [[Father alloc]init];
father这个实例对象里面的isa地址:
p/x (long)father->isa
0x1a1003a7cf9
Father这个类对象的地址:NSLog(@"%p",object_getClass(father));
0x1003a7cf8
p/x 0x1a1003a7cf9 & 0xffffffff8
= 0x1003a7cf8
object_getClass获取的是isa指针指向的类,而objc_getClass是通过查找mapPair(字典)的形式获取类
apple官方源码:实例对象的isa指针需要进行&计算才能得到类对象的真正地址
为啥需要&运算呢?(下面为objc源码)
#union:共用体 union isa_t { isa_t() { } isa_t(uintptr_t value) : bits(value) { } Class cls; uintptr_t bits; #if defined(ISA_BITFIELD) struct { ISA_BITFIELD; // defined in isa.h }; #endif }; #ISA_BITFIELD的定义 # if __arm64__ # define ISA_MASK 0x0000000ffffffff8ULL # define ISA_MAGIC_MASK 0x000003f000000001ULL # define ISA_MAGIC_VALUE 0x000001a000000001ULL # define ISA_BITFIELD \ uintptr_t nonpointer : 1; \ uintptr_t has_assoc : 1; \ uintptr_t has_cxx_dtor : 1; \ uintptr_t shiftcls : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ >\ uintptr_t magic : 6; \ uintptr_t weakly_referenced : 1; \ uintptr_t deallocating : 1; \ uintptr_t has_sidetable_rc : 1; \ uintptr_t extra_rc : 19
因为apple在arm64后,对isa做了优化.
原来的isa直接指向类对象或元类对象地址
,现在优化过后将位域的33bits保存类对象或元类对象地址
,其他的64-33=31位保存其他信息.
三.extention和category的区别?
- extension看起来很像一个匿名的category,extension在编译期决议,它就是类的一部分,一般用来隐藏类的私有信息.
- category则完全不一样,它是在运行期决议的.在运行程序的时候,内核会加载dyld,dyld会加载各种动态库镜像,会加载catagory并将catagroy的类方法添加到已经编译好的类的方法列表里面,然后才会调用
main()
函数.
所以说:使用catagory会降低程序的运行速度. - 所以extension可以添加实例变量,而category是无法添加实例变量的(因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的.相对的,对象的方法定义都保存在类的可变区域中。
四. + (void)load方法调用时机?
//1.从objc-os.mm文件的void _objc_init(void)方法进来
_dyld_objc_notify_register(&map_images, load_images, unmap_image);
//2.进入load_images
//判断是否有load方法,没有直接返回
if (!hasLoadMethods((const headerType *)mh)) return;
//3.发现load方法
prepare_load_methods((const headerType *)mh);
//4.添加到loadable_classes表中去,这个表中维护了所有类,及其对应的load方法的IMP
schedule_class_load(remapClass(classlist[i]));
//内部实现:先递归添加父类的load方法
schedule_class_load(cls->superclass);
//5.获取非懒加载Category的列表,添加到loadable_categories表中去
add_category_to_loadable_list(cat);
//6.调用load方法
call_load_methods();
//内部实现
void *pool = objc_autoreleasePoolPush();
do {
// 1. Repeatedly call class +loads until there aren't any more
//调用class的load方法
while (loadable_classes_used > 0) {
call_class_loads();
}
// 2. Call category +loads ONCE
//调用Category的load方法
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);
-
+ (load)(在main函数开始执行之前,调用顺序和编译顺序无关,并且只调用一次)
的顺序是先执行父类中的+load,然后是子类,再然后执行category中的+load如果有多个category,那么就按照编译的顺序来调用+load
. - 而且分类的load()方法不会覆盖原来类的load()方法,也就是说所有的load方法都会执行.
五.+ (void)initialize方法调用时机?
- 在类第一次接收到消息的时候调用,比如第一次alloc的时候.
- 调用子类的
initialize()
时候,会先调用父类的initialize()
.毕竟你爸爸都不存在,你还怎么玩继承呢?
- catagory的initialize()方法会覆盖原来类的initialize()方法(这是和load()方法的区别)
解释:initialize()方法走的是msg_send(),所以catagory的initialize()方法调用顺序优先级要高于原来类的优先级源码: void callInitialize(Class cls) { ((void(*)(Class, SEL))objc_msgSend)(cls, SEL_initialize); asm(""); }
六.kvc设置值会触发kvo吗
答案:会的.
- 如果有
set()
,那么kvc会触发set()
,set()
又会触发kvo - 如果没有
set()
,kvc内部会调用willChangeValueForKey()
和didChangeValueForKey()
,同样也会触发kvo
拓展 : kvc的实现
kvc会依次寻找下面的方法
- setName()
- _setName()
- 判断accessInstanceVariablesDirectly.如果返回YES,那就开始查找成员变量
- 给_name赋值
- 给_isName赋值
- 给name赋值
- 给isName赋值
- 如果都没有,报错valueForUndefinedKey.
七. 打印结果是什么?
创建了一个son类和father类,他们都有eat()
@interface Father : NSObject
- (void)eat;
@end
@implementation Father
- (void)eat
{
}
@end
@interface Son : Father
- (void)eat;
@end
@implementation Son
- (void)eat
{
NSLog(@"self的类为%@",[self class]);
NSLog(@"super的类为%@",[super class]);
}
@end
打印结果:
2019-01-17 16:39:36.548350+0800 demo[23333:6167589] self的类为Son
2019-01-17 16:39:36.548401+0800 demo[23333:6167589] super的类为Son
- 为什么
[super class]
的值也是Son呢?
- 使用
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
命令得到c++代码,搜索_Son_eat
做删减后得到源码
static void _I_Son_eat(Son * self, SEL _cmd) {
NSLog( (objc_msgSend) (self, sel_registerName("class")) );
NSLog((objc_msgSendSuper)({self, Father.class}, sel_registerName("class")));
}
- 第一个NSLog是给self发送class消息.
因为son没有class()
方法,所以会找Father类.Father类也没有所以会找到NSObject的class()
方法,所以会返回Son - 第二个NSLog里面调用了
objc_msgSendSuper()
方法,查看objc_msgSendSuper()
的源码
里面需要传入一个结构体和SEL,和我们简化过的c++代码吻合
#objc_msgSendSuper
#方法说明:给一个实例对象的父类发送一条消息
#参数1:是一个objc_super结构体,包含了接受消息的实例对象和开始搜索方法实现的超类
#参数2:@SEL 发送的消息
OBJC_EXPORT id _Nullable
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
-
struct objc_super
结构体的源码为
这里的receiver为刚才我们打印出来的self, super_class为Father.class.
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained _Nonnull id receiver;
__unsafe_unretained _Nonnull Class super_class;
};
- 从
objc_msgSendSuper()
方法我们可以得出:
第二个NSLog的内容其实是给self发送class消息,而且查找class()
这个方法的起始地为Father这个类的cache,然后是Father这个类的msthodList,然后是Father的父类NSObject.
所以:既然还是给self发送消息,那么打印的值仍然是self的类Son
拓展
我们平时写得
self = [super init]
,其实是告诉系统去父类的methodList中查找方法而已,并不是初始化父类....
八.objc_getClass()
和object_getClass()
区别
1.objc_getClass()
- 根据map表找到Class,并且只能找到类,不能找到元类.
- 传入字符串
Class objc_getClass(const char *aClassName)
{
if (!aClassName) return Nil;
// NO unconnected, YES class handler
//获取类
return look_up_class(aClassName, NO, YES);
}
2.object_getClass()
- 直接去找isa指针.
- 传入实例对象,返回类.传入类对象,返回元类.
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
九.为什么UIKit不是线程安全的
- 成本较大,性价比低.
十.property里面的atomic和nonatomic是怎样实现的?
- 其实是使用了自旋锁(自旋锁适合消耗时间短的操作,而set方法刚好满足,所以使用自旋锁比使用synchronized更合适),看下面的代码
- atomic保证了同一时刻只有一个set方法能执行,但不保证get方法.(多读单写)
- atomic并不安全,直接调用 _age=10就跳过了set方法...
if (!atomic)
{
oldValue = *slot;
*slot = newValue;
}
else
{
spin_lock_t *slotlock = &PropertyLocks[GOODHASH(slot)];
_spin_lock(slotlock);
oldValue = *slot;
*slot = newValue;
_spin_unlock(slotlock);
}
十一.优化
1.CALayer替代UIView2
2.提前计算好布局
3.不适用autolayout
4.把耗时操作放到子线程
5.减少图层
6.减少透明视图的存在.
7.避免离屏渲染
8.减少、合并动态库
9.减少类、分类
10.不要把所有的东西方法didfinishLaunch
十二.信号量和互斥锁的区别
- 信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作(大家都在semtake的时候,就阻塞在 哪里)
- 而互斥锁是用在多线程多任务互斥的,一个线程占用了某一个资源,那么别的线程就无法访问,直到这个线程unlock,其他的线程才开始可以利用这个资源。比如对全局变量的访问,有时要加锁,操作完了,在解锁。有的时候锁和信号量会同时使用的
- 也就是说,信号量不一定是锁定某一个资源,而是流程上的概念,比如:有A,B两个线程,B线程要等A线程完成某一任务以后再进行自己下面的步骤,这个任务 并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类。而线程互斥量则是“锁住某一资源”的概念,在锁定期间内,其他线程无法对被保护的数据进行操作。在有些情况下两者可以互换。
互斥量和信号量的区别
- 互斥量用于线程的互斥,信号量用于线程的同步。
这是互斥量和信号量的根本区别,也就是互斥和同步之间的区别。
互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源 - 互斥量值只能为0/1,信号量值可以为非负整数。
也就是说,一个互斥量只能用于一个资源的互斥访问,它不能实现多个资源的多线程互斥问题。信号量可以实现多个同类资源的多线程互斥和同步。当信号量为单值信号量是,也可以完成一个资源的互斥访问。 - 互斥量的加锁和解锁必须由同一线程分别对应使用,信号量可以由一个线程释放,另一个线程得到。
总结:
1.信号量用于同步,互斥锁用于同步.信号量不会锁定资源,而互斥锁会锁定资源.
2.信号量的值为非负整数,互斥锁只有0和1.
3.信号量可以在一个线程释放,在另一线程得到.而互斥锁只能在同一线程使用.
十三.iOS开发中静态库和动态库区别
静态库和动态库是相对编译期和运行期的:静态库在程序编译时会被链接到目标代码中,程序运行时将不再需要改静态库;而动态库在程序编译时并不会被链接到目标代码中,只是在程序运行时才被载入,因为在程序运行期间还需要动态库的存在。
动态库形式:.dylib和.framework
静态库形式:.a和.framework
使用静态库的好处
1,模块化,分工合作
2,避免少量改动经常导致大量的重复编译连接
3,也可以重用,注意不是共享使用
使用动态库的好处
1,使用动态库,可以将最终可执行文件体积缩小
2,使用动态库,多个应用程序共享内存中得同一份库文件,节省资源
3,使用动态库,可以不重新编译连接可执行程序的前提下,更新动态库文件达到更新应用程序的目的。
- 库里面都是函数,是一种可执行代码的二进制格式,可以被载入内存中执行
- 静态库:
- 在程序编译时已经加入到可执行文件中
- 不能再多个程序间共享
- .a或.framework结尾
- 动态库:
- 在程序运行时才去载入该库
- 可以多个程序间共享
- .tbd或.framework结尾
- tips:苹果系统提供的framework(UIKit等)全部属于动态库,可在多个程序间共享.
动态库和静态库的使用时机
- iOS8以后苹果允许上架自写动态库,但是会对动态库签名,所以不可以在线更新动态库
- 一般都选择制作静态库
- 动态库可以使用的两个地方 1.App Extention 2. 企业级应用
网友评论