iOS随笔

图片的解压缩:

imageNamed: 方法的特点在于可以缓存已经加载的图片;使用时,先根据文件名在系统缓存中寻找图片,如果找到了就返回;如果没有,就在 Bundle 内查找到文件名,找到后把这个文件名放到 UIImage 里返回,并没有进行实际的文件读取和解码。当 UIImage 第一次显示到屏幕上时,其内部的解码方法才会被调用,同时解码结果会保存到一个全局缓存去。在图片解码后,App 第一次退到后台和收到内存警告时,该图片的缓存才会被清空,其他情况下缓存会一直存在。

imageWithContentsOfFile: 该方法直接返回图片,不会缓存。而且其解码依然要等到第一次显示该图片的时候。

/**将生成的image图片赋值给imageView的时候,一个隐式的 CATransaction 捕获到了 UIImageView 图层树的变化,在主线程的下一个 run loop 到来时,Core Animation 提交了这个隐式的 transaction,将压缩的图片数据解码成未压缩的位图形式,这是一个非常耗时的 CPU 操作,Core Animation 使用未压缩的位图数据渲染 UIImageView 的图层**/

位图就是一个像素数组,数组中的每个像素就代表着图片中的一个点。我们在应用中经常用到的 JPEG 和 PNG 图片就是位图。

像素格式则是用来描述每个像素的组成格式,它包括以下信息:

  • Bits per component :一个像素中每个独立的颜色分量使用的 bit 数;
  • Bits per pixel :一个像素使用的总 bit 数;rgba -> Oxffffffff -> 1 * 4bits
  • Bytes per row :位图中的每一行使用的字节数。width * Bits per pixel -> width * 4

不管是 JPEG 还是 PNG 图片,都是一种压缩的位图图形格式。只不过 PNG 图片是无损压缩,并且支持 alpha 通道,而 JPEG 图片则是有损压缩,可以指定 0-100% 的压缩比。

将磁盘中的图片渲染到屏幕之前,必须先要得到图片的原始像素数据,才能执行后续的绘制操作,这里就必须用到图片的解压缩。

解压缩后的图片大小 = 图片的像素宽 30 * 图片的像素高 30 * 每个像素所占的字节数 4

解压缩的原理就是对图片进行重新绘制

  

/**

  width:像素宽

  height:像素高

  bitsPerComponent: 像素的每个颜色分量使用的 bit 数,在 RGB 颜色空间下指定 8 即可

  bytesPerRow:位图的每一行使用的字节数,大小至少为 width * bytes per pixel 字节(width * 4), 我们指定 0 时,系统会为我们自动计算

  space :就是我们前面提到的颜色空间,一般使用 RGB 即可

  bitmapInfo: 位图的布局信息:

    typedef CF_OPTIONS(uint32_t, CGBitmapInfo) {

       kCGBitmapAlphaInfoMask = 0x1F,

       kCGBitmapFloatInfoMask = 0xF00,

       kCGBitmapFloatComponents = (1 << 8), /*颜色分量是否为浮点数*/

       kCGBitmapByteOrderMask = kCGImageByteOrderMask,

       kCGBitmapByteOrderDefault = (0 << 12),

       kCGBitmapByteOrder16Little = kCGImageByteOrder16Little,字节顺序使用 16 位的主机字节 --- kCGBitmapByteOrder16Host 兼容大小端

       kCGBitmapByteOrder32Little = kCGImageByteOrder32Little,字节顺序使用 32 位的主机字节 --- kCGBitmapByteOrder32Host 兼容大小端

       kCGBitmapByteOrder16Big = kCGImageByteOrder16Big,字节顺序使用 16 位的主机字节 --- kCGBitmapByteOrder16Host 兼容大小端

       kCGBitmapByteOrder32Big = kCGImageByteOrder32Big字节顺序使用 32 位的主机字节 --- kCGBitmapByteOrder32Host 兼容大小端

    };

    alpha 的信息由枚举值 CGImageAlphaInfo 来表示:

    typedef CF_ENUM(uint32_t, CGImageAlphaInfo) {

       kCGImageAlphaNone, /* For example, RGB. */

       kCGImageAlphaPremultipliedLast, /* For example, premultiplied RGBA */

       kCGImageAlphaPremultipliedFirst, /* For example, premultiplied ARGB */图片包含 alpha

       kCGImageAlphaLast, /* For example, non-premultiplied RGBA */

       kCGImageAlphaFirst, /* For example, non-premultiplied ARGB */

       kCGImageAlphaNoneSkipLast, /* For example, RBGX. */

       kCGImageAlphaNoneSkipFirst, /* For example, XRGB. */图片不包含 alpha

       kCGImageAlphaOnly /* No color data, alpha data only */

    };

  */

  CGBitmapContextCreate(void * __nullable data, size_t width, size_t height, size_t bitsPerComponent, size_t bytesPerRow, CGColorSpaceRef cg_nullable space, uint32_t bitmapInfo) CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);

 yyimage中图片解压缩方式为:

CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageRef) & kCGBitmapAlphaInfoMask;

        BOOL hasAlpha = NO;
        if (alphaInfo == kCGImageAlphaPremultipliedLast ||
            alphaInfo == kCGImageAlphaPremultipliedFirst ||
            alphaInfo == kCGImageAlphaLast ||
            alphaInfo == kCGImageAlphaFirst) {
            hasAlpha = YES;
        }

        // BGRA8888 (premultiplied) or BGRX8888
        // same as UIGraphicsBeginImageContext() and -[UIView drawRect:]
        CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
        bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;

        CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, YYCGColorSpaceGetDeviceRGB(), bitmapInfo);
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); //将原始位图绘制到上下文中
CGImageRef newImage = CGBitmapContextCreateImage(context);// 创建一张新的解压缩后的位图
CFRelease(context);

performSelector:

performSelector响应Objective-C动态性,将方法的绑定延迟到运行时,因此编译阶段不会检测方法有效性,即方法不存在也不会提示报错。反之因为此特性,performSelector也广泛用于动态化和组件化的模块中。


在ARC环境下,编译器并不知道将要调用的选择子是什么,有没有返回值,返回值是什么,所以ARC不能判断返回值是否能释放,因此ARC做了一个比较谨慎的做法:只添加retain,不加release。因此在有返回值或参数的时候可能导致内存泄漏。

避免内存泄漏的处理方式为及时调用:

[NSObject cancelPreviousPerformRequestsWithTarget:self]

Tagpoint:

Tagged Pointer专门用来存储小的对象,例如NSNumber和NSDate.

Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。

所以,它的内存并不存储在堆中,也不需要malloc和free。

对于64位程序,我们的数据类型的长度是跟CPU的长度有关的。导致了 一些对象占用的内存会翻倍。

维护程序中的对象需要 分配内存,维护引用计数,管理生命周期,使用对象给程序的运行增加了负担。

将一个对象的指针拆成两部分,一部分直接保存数据,另一部分作为特殊标记,表示这是一个特别的指针,不指向任何一个地址。
以 0x127 为例 去掉 tag27(假设27为标记) 0x1 就是number = 1 的值。

APNS流程图:

1、应用程序注册APNS消息推送。

2、iOS从APNS Server获取device token,应用程序接收device token。

3、应用程序将device token发送给程序的PUSH服务端程序。

4、服务端程序向APNS服务发送消息。

5、APNS服务将消息发送给iPhone应用程序。

青花瓷抓包原理:

Charles 作为一个“中间人代理”,当浏览器和服务器通信时,Charles接收服务器的证书,但动态生成一张证书发送给浏览器,

也就是说Charles作为中间代理在浏览器和服务器之间通信,所以通信的数据可以被Charles拦截并解密。

由于Charles更改了证书,浏览器校验不通过会给出安全警告,必须安装Charles的证书后才能进行正常访问。

客户端信任了 Charles 自己制作的证书,然后导致 Charles 拿到 CA 证书和对称加密的公开密钥。

静态库:

1 注意理解:无论是.a 静态库还.framework 静态库,我们需要的都是二进制文件+.h+其它资源文件的形式,不同的是,.a 本身就是二进制文件,需要我们自己配上.h 和其它文件才能使用,而.framework 本身已经包含了.h和其它文件,可以直接使用。

2 图片资源的处理:两种静态库,一般都是把图片文件单独的放在一个.bundle 文件中,一般.bundle 的名字和.a 或.framework 的名字相同。.bundle 文件很好弄,新建一个文件夹,把它改名为.bundle 就可以了,右键,显示包内容可以向其中添加图片资源。

category 是我们实际开发项目中经常用到的,把 category 打成静态库是没有问题的,但是在用这个静态库的工程中,调用 category 中的方法时会有找不到该方法的运行时错误(selector not recognized),解决办法是:在使用静态库的工程中配置 other linker flags 的值为-ObjC

4 如果一个静态库很复杂,需要暴露的.h 比较多的话,就可以在静态库的内部创建一个.h 文件(一般这个.h 文件的名字和静态库的名字相同),然后把所有需要暴露出来的.h 文件都集中放在这个.h 文件中,而那些原本需要暴露的.h 都不需要再暴露了,只需要把.h 暴露出来就可以了。

setObject:forKey:  &&  setValue:forKey:

-setValue:forKey:是KVC提供的方法,可对绝大部分对象进行操作(包括自定义对象),-setObject: forKey是NSMutableDictionary提供的方法,只能操作可变字典;
-setValue:forKey:当value为nil时,对于字典相当于删除key-value对,-setObject: forKey当object为nil时将直接导致程序crash,因此调用之前需要判空;

NSCache:  NSDictionary:

1.当系统资源将要耗尽的时候,NSCache可以自动删减缓存,而且线性删减"最久未使用的"对象,NSCache是不是很强大。但是NSDictionary就需要在系统发出"低内存"通知时手工删减缓存,还需要自己编写相应优先删减内存等一系列逻辑。

2.NSCache是线程安全的,可以在供多个线程同时访问NSCache但是NSDictionary就不具备此优势

3.NSCache对象不拷贝键,因为很多时候键都是由不支持拷贝操作的对象来充当的,因此NSCache不会自动拷贝

- (void)setObject:(ObjectType)obj forKey:(KeyType)key cost:(NSUInteger)g;

NSCache: 如果设置了totalCostLimit必然存储缓存的方法调用必然带上了cost,否则totalCostLimit是无用的。

- (void)cache:(NSCache *)cache willEvictObject:(id)obj;

cache对象的代理 , 用来即将清理cache的时候得到通知.

这里面有几种情况会导致该方法执行:

  • 手动移除(removeObjectForKey)
  • 缓存超过设定的上线
  • App不活跃
  • 系统内存爆炸

dyld:

由于 iOS 系统中 UIKit / Foundation 等库每个应用都会通过 dyld 加载到内存中 , 因此 , 为了节约空间 , 苹果将这些系统库放在了一个地方 : 动态库共享缓存区 (dyld shared cache) .

类似 NSLog 的函数实现地址 , 并不会也不可能会在我们自己的工程的 Mach-O 中 

在工程编译时 , 所产生的 Mach-O 可执行文件中会预留出一段空间 , 这个空间其实就是符号表 , 存放在 _DATA 数据段中

编译时 : 工程中所有引用了共享缓存区中的系统库方法 , 其指向的地址设置成符号地址

运行时 : 当 dyld将应用进程加载到内存中时 , 根据 load commands 中列出的需要加载哪些库文件 , 去做绑定的操作, dyld 就会去找到 Foundation 中 NSLog 的真实地址写到 _DATA 段的符号表中 NSLog 的符号上面

+load():

load 方法在 runtime 库开始运行时调用.

一个类的 load 方法在所有的父类 load 方法调用之后.

分类的 load 方法在类的 load 方法之后.

不同分类之间的 load 方法调用顺序和编译顺序有关.

Category & Extension:

category:

typedef 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; // 属性列表
} category_t;

 category可以添加实例方法,类方法,甚至可以实现协议,添加属性,但无法添加实例变量。

 category结构体本身命名,而且有static来修饰,在同一个编译单元里我们的category名不能重复

 所有的category文件,都会被编译成名为OBJC$_CATEGORY_xxClass$_categoryname的struct对象,对结构体成员变量进行初始化和赋值操作。

static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= { &_OBJC_$_CATEGORY_xxClass$_categoryname };

 编辑器会在objc_catlist section中的category_t 数组保存该category对应的struct对象

 在app的启动阶段,调用_read_images,拿到 category_t 结构体数组(category_t的顺序由compile sources编译顺序决定),通过attachCategoryMethods反向拼接数组中的实例方法,通过attachMethodLists将拼接好的方法放到类的方法列表前面。造成方法覆盖的假象。

在追加方法和属性后,才会走+load().+load()的执行顺序是先类本身,再category,而category的+load执行顺序是根据编译顺序决定的。而追加的方法执行的顺序按照编译顺序决定

关联对象都由AssociationsManager管理,而AssociationsManager定义如下:

class AssociationsManager {
    static OSSpinLock _lock;
    static AssociationsHashMap *_map;               // associative references:  object pointer -> PtrPtrHashMap.
public:
    AssociationsManager()   { OSSpinLockLock(&_lock); }
    ~AssociationsManager()  { OSSpinLockUnlock(&_lock); }
    
    AssociationsHashMap &associations() {
        if (_map == NULL)
            _map = new AssociationsHashMap();
        return *_map;
    }
};

 AssociationsHashMap的key是这个对象的指针地址(任意两个不同对象的指针地址一定是不同的),而这个map的value又是另外一个AssociationsHashMap,里面保存了关联对象的kv对。

调用_object_remove_assocations做关联对象的清理工作。

extension:

extension可以声明方法,还可以声明属性、成员变量。extension一般用于声明私有方法,私有属性,私有成员变量。

extension在编译期决议,它就是类的一部分。头文件里的@interface以及具体实现文件里的@implement一起形成一个完整的类。

你必须有一个类的源码才能为一个类添加extension,所以你无法为系统的类比如NSString添加extension,除非创建子类再添加extension。

viewController.m文件中通常寄生这么个东西,其实这就是一个extension:

@interface ViewController ()

@end

 通过addFile->select Objective-C File 来创建extension:如ClassA_extension.h,在ClassA或者其子类中的.m文件 #import "ClassA_extension.h" 就可以使用extension的方法和属性

Static & const & extern & #define

static

  修饰局部变量

  1.让局部变量只初始化一次

  2.局部变量在程序中只有一份内存

  3.并不会改变局部变量的作用域,仅仅是改变了局部变量的生命周期(只到程序结束,这个局部变量才会销毁)

  修饰全局变量

  1.全局变量的作用域仅限于当前文件

  2.避免重复定义全局变量

const

    1.没有const修饰的指针

        指针p和*p都能被修改

    2.const修饰的*p

        被const修饰的*p只能被赋值一次,以后不能赋值,否则编译器报错

    3.const修饰的p

        被const修饰的p只能存一次地址,以后再也不能其它存地址了,否则编译器报错

    4.const在声明字符串的用法(一般我们定义一个常量又不想被修改应该这样:)

        NSString *const FYNName =@"jack";

extern

    只是用来获取全局变量(包括全局静态变量)的值,不能用于定义变量

static和const联合使用

    1.用static声明局部变量,使其变为静态存储方式(静态数据区),作用域不变;用static声明                            外部变量,其本身就是静态变量,这只会改变其连接方式,使其只在本文件内部有效,而其他文件不可连接或引用该变量。

    2.const将一个变量变成常量

    3.初始化一个全局变量或static变量时,只能用常量赋值,不能用变量赋值!

    

extern与const组合:
只需要定义一份全局变量,多个文件共享。
UIKIT_EXTERN CGFloat const cellH;
CGFloat const cellH = 50;
 

static和const. & #define

  宏定义的是常量,常量都放在常量区,只会生成一份内存。

  1. static const修饰变量只有一份内存

      2.宏定义,只是简单的替换,宏定义的是变量的时候每次使用都需要创建一份内存

      3. 编译时刻:宏是预编译(编译之前处理),const是编译阶段。

      4.编译检查:宏不做检查,不会报编译错误,只是替换,const会编译检查,会报编译错误。

      5.宏的好处:宏能定义一些函数,方法。 const不能。

      6.宏的坏处:使用大量宏,容易造成编译时间久,每次都需要重新替换。

load & initialize

load:

当类被引用进项目的时候(类文件加载到系统中)就会执行load函数(在main函数开始执行之前),与这个类是否被用到无关,每个类的load函数只会自动调用一次.由于load函数是系统自动加载的,因此不需要调用父类的load函数,否则父类的load函数会多次执行。

  • 1.当父类和子类都实现load函数时,父类的load方法执行顺序要优先于子类
  • 2.当子类未实现load方法时,不会调用父类load方法
  • 3.类中的load方法执行顺序要优先于类别(Category)
  • 4.当有多个类别(Category)都实现了load方法,这几个load方法都会执行,但执行顺序不确定(其执行顺序与类别在Compile Sources中出现的顺序一致)
  • 5.当然当有多个不同的类的时候,每个类load 执行顺序与其在Compile Sources出现的顺序一致
initialize:
initialize在类或者其子类的第一个方法被调用前调用。即使类文件被引用进项目,但是没有使用,initialize不会被调用。由于是系统自动调用,也不需要再调用 [super initialize] ,否则父类的initialize会被多次执行。假如这个类放到代码中,而这段代码并没有被执行,这个函数是不会被执行的。
  • 1.父类的initialize方法会比子类先执行
  • 2.当子类未实现initialize方法时,会调用父类initialize方法,子类实现initialize方法时,会覆盖父类initialize方法.
  • 3.当有多个Category都实现了initialize方法,会覆盖类中的方法,只执行一个(会执行Compile Sources 列表中最后一个Category 的initialize方法)

Method & SEL & IMP

Selector

typedef struct objc_selector *SEL

叫做选择子或者选择器,选择子代表方法在 Runtime 期间的标识符。为 SEL 类型,虽然 SEL 是 objc_selector 结构体指针,但实际上它只是一个 C 字符串

不同类中相同名字的方法所对应的方法选择子是相同的.

const char *sel_getName(SEL sel) {

  return sel ? (const char *)sel : "<null selector>";

}

Implementation(IMP):

typedef id (*IMP)(id, SEL, ...)

代表函数指针,即函数执行的入口。该函数使用标准的 C 调用。第一个参数指向 self(它代表当前类实例的地址,如果是类则指向的是它的元类),作为消息的接受者;第二个参数代表方法的选择子;... 代表可选参数,前面的 id 代表返回值。

Method

typedef struct objc_method *Method

Method 对开发者来说是一种不透明的类型,被隐藏在我们平时书写的类或对象的方法背后。它是一个 objc_method 结构体指针,objc_method 的定义为:

struct objc_method {

  SEL method_name;// 方法的名字

  char *method_types;// 方法的签名

  IMP method_imp;// 方法的实现

};

Method method = class_getInstanceMethod(currentClass@selector(resume));// 从SEL获取method

method_getImplementation(method);// 从method获取imp

 

对称加密和非对称加密:

对称加密算法有DES、3DES、AES

非对称 RSA

 内存的几大区域:

App程序启动,系统会把开启的那个App程序从Flash或ROM里面拷贝到内存(RAM),然后从内存里面执行代码。
CPU不能直接从内存卡里面读取指令
 
栈区:一般存放函数参数,局部变量等值,由系统自动分配和管理,程序员不必关心。存放里面的数据,遵从先进后出的原则。
堆区:由程序员申请,管理和内存回收。数据储存的结构是链表。
全局区/静态区:储存全局初始化和未初始化变量和静态变量。
文字常量区:主要储存字符串常量。
程序代码区:存放程序的二进制代码。
 
 
锁:
atomic: 内部采用了优化过性能的spinlock_t 自旋锁,内部实现为互斥锁。单独的 原子操作绝对是线程安全的,但是组合一起的操作就不能保证。
    self.intSource = 0;

    dispatch_async(queue1, ^{
      for (int i = 0; i < 10000; i++) {
          self.intSource = self.intSource + 1;
      }
    });

    dispatch_async(queue2, ^{
      for (int i = 0; i < 10000; i++) {
          self.intSource = self.intSource + 1;
      }
    });
自旋锁:自旋锁在无法进行加锁时,会不断的进行尝试,一般用于临界区的执行时间较短的场景,不过iOS的自旋锁OSSpinLock不再安全,主要原因发生在低优先级线程拿到锁时,高优先级线程进入忙等(busy-wait)状态,消耗大量 CPU 时间,从而导致低优先级线程拿不到 CPU 时间,也就无法完成任务并释放锁。这种问题被称为优先级反转。
互斥锁:对于某一资源同时只允许有一个访问,无论读写,平常使用的NSLock就属于互斥锁
读写锁:对于某一资源同时只允许有一个写访问或者多个读访问,iOS中pthread_rwlock就是读写锁
条件锁:在满足某个条件的时候进行加锁或者解锁,iOS中可使用NSConditionLock来实现
递归锁:可以被一个线程多次获得,而不会引起死锁。它记录了成功获得锁的次数,每一次成功的获得锁,必须有一个配套的释放锁和其对应,这样才不会引起死锁。只有当所有的锁被释放之后,其他线程才可以获得锁,iOS可使用NSRecursiveLock来实现
@synchronized(obj): 内部采用hash算法找到SyncData,并采用的是SyncData.mutext 递归锁
if (obj) {
    SyncData* data = id2data(obj, ACQUIRE);// 一个简单的Hash算法,然后将传入的对象地址,通过地址找到下标映射到不同的SyncList上。而SyncList是一个维护SyncData的链表,每个SyncList都单独维护操作自己的散列锁lock。
    assert(data);
    data->mutex.lock();
}

typedef struct SyncData {
    struct SyncData* nextData;
    DisguisedPtr<objc_object> object;
    int32_t threadCount;  // number of THREADS using this block
    recursive_mutex_t mutex;// 一把递归锁
} SyncData;
 
编译过程: (我不懂这一块的东西)
  • 预编译:主要处理以“#”开始的预编译指令。
  • 编译:
    1. 词法分析:将字符序列分割成一系列的记号。
    2. 语法分析:根据产生的记号进行语法分析生成语法树。
    3. 语义分析:分析语法树的语义,进行类型的匹配、转换、标识等。
    4. 中间代码生成:源码级优化器将语法树转换成中间代码,然后进行源码级优化,比如把 1+2 优化为 3。中间代码使得编译器被分为前端和后端,不同的平台可以利用不同的编译器后端将中间代码转换为机器代码,实现跨平台。
    5. 目标代码生成:此后的过程属于编译器后端,代码生成器将中间代码转换成目标代码(汇编代码),其后目标代码优化器对目标代码进行优化,比如调整寻址方式、使用位移代替乘法、删除多余指令、调整指令顺序等。
  • 汇编:汇编器将汇编代码转变成机器指令。
  • 静态链接:链接器将各个已经编译成机器指令的目标文件链接起来,经过重定位过后输出一个可执行文件。
  • 装载:装载可执行文件、装载其依赖的共享对象。
  • 动态链接:动态链接器将可执行文件和共享对象中需要重定位的位置进行修正。
  • 最后,进程的控制权转交给程序入口,程序终于运行起来了。
 
 

静态库和动态库:

静态链接是指将多个目标文件合并为一个可执行文件,直观感觉就是将所有目标文件的段合并。需要注意的是可执行文件与目标文件的结构基本一致,不同的是是否“可执行”。 
 
静态库:链接时完整地拷贝至可执行文件中,被多次使用就有多份冗余拷贝。 
动态库:链接时不复制,程序运行时由系统动态加载到内存,供程序调用,系统只加载一次,多个程序共用,节省内存。
 
内联函数:

内联函数是为了减少函数调用的开销,编译器在编译阶段把函数体内的代码复制到函数调用处。类似于宏定义,但可以进行语法检查。

网络层的优化:

减少DNS请求,使用HttpDNS获取原始ip地址进行请求,在header请求头中增加host字段,指定为原始请求地址

数据传输使用gzip

使用断点续传,否则网络不稳定时可能多次传输相同的内容

结合ETag 和 Last-Modified 减少数据重复传输

网络请求本地缓存

避免频繁发起请求

请求失败缓存后重发请求

2G、3G、4G、wifi下设置不同的超时时间

取消系统默认kvo并手动触发:

- (void)setAge:(NSString *)age  {  

      if (age > 18 ) {

    [self willChangeValueForKey:@"age"];

    _age = age;

     [self didChangeValueForKey:@"age"];

  }else {

    _age = age;

  }

}  

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{

  // 如果监测到键值为age,则指定为非自动监听对象

  if ([key isEqualToString:@"age"])  {

    return NO;  

  }

  return [super automaticallyNotifiesObserversForKey:key]; 

}

SideTables:

为了管理所有对象的引用计数和weak指针,苹果创建了一个全局的SideTables,虽然名字后面有个"s"不过他其实是一个全局的 Hash表,里面的内容装的都是 SideTable结构体而已。它使用对象的 内存地址当它的key。管理引用计数和weak指针就靠它了。
因为内存中对象的数量是 非常非常庞大的需要非常频繁的操作SideTables,所以 能对整个Hash表加锁。苹果采用了 分离锁技术。
使用对象的内存地址当key所以Hash的分部也很平均。假设Hash表有n个元素,则可以将Hash的冲突减少到n分之一,支持n路的并发写操作。
 
struct SideTable { 
    spinlock_t slock;//操作SideTable时用到的锁 
    RefcountMap refcnts;//引用计数器的值,这是一个map
    weak_table_t weak_table;//存放weak指针的哈希表 
};

struct weak_table_t { weak_entry_t *weak_entries; //保存了所有指向指定对象的weak指针 weak_entries的对象 size_t num_entries; // weak对象的存储空间 uintptr_t mask; //参与判断引用计数辅助量 uintptr_t max_hash_displacement; //hash key 最大偏移值 };

struct weak_entry_t {
  
DisguisedPtr<objc_object> referent; //指向当前weak修饰的对象
    union { struct { weak_referrer_t *referrers;// weak指针对象 uintptr_t out_of_line : 1; uintptr_t num_refs : PTR_MINUS_1; uintptr_t mask; uintptr_t max_hash_displacement; }; struct { // out_of_line=0 is LSB of one of these (don't care which) weak_referrer_t inline_referrers[WEAK_INLINE_COUNT]; }; }
}

RefcountMap其实是个C++的Map。为什么Hash以后还需要个Map?其实苹果采用的是分块化的方法。 

内存中对象的数量实在是太庞大了我们通过第一个Hash表只是过滤了第一次,然后我们还需要再通过这个Map才能精确的定位到我们要找的对象的引用计数器。

 

weak和strong都是Object-C的修饰词,

strong是通过runtime维护的一个自动计数表结构。

weak是有Runtime维护的weak哈希表,用于存储指向某个对象的所有weak指针,Key是所指对象的地址,Value是weak指针的地址数组。

 
AssociationsManager:
void objc_setAssociatedObject(id object, const void * key,id value, objc_AssociationPolicy policy)
AssociationsManager =>
     AssociationsHashMap {
        object的内存地址:  ObjectAssociationMap {
                                        key: ObjcAssociation(policy, new_value)// 如果value不为nil,根据policy决定是否进行retain操作,如果value为nil,根据policy决定是否进行release操作
                                   }
     }

Autoreleasepool:

Autoreleasepool是由多个AutoreleasePoolPage以双向链表的形式连接起来的, Autoreleasepool的基本原理:在每个自动释放池创建的时候,会在当前的AutoreleasePoolPage中设置一个标记位,在此期间,当有对象调用autorelsease时,会把对象添加到AutoreleasePoolPage中,若当前页添加满了,会初始化一个新页,然后用双向量表链接起来,并把新初始化的这一页设置为hotPage,当自动释放池pop时,从最下面依次往上pop,调用每个对象的release方法,直到遇到标志位。

ARC下,我们使用@autoreleasepool{}来使用一个AutoreleasePool,随后编译器将其改写成下面的样子:

void *context = objc_autoreleasePoolPush();
// {}中的代码objc_autoreleasePoolPop(context);

而这两个函数都是对AutoreleasePoolPage的简单封装,所以自动释放机制的核心就在于这个类。

每当进行一次objc_autoreleasePoolPush调用时,runtime向当前的AutoreleasePoolPage中add进一个哨兵对象,值为0(也就是个nil),

objc_autoreleasePoolPush的返回值正是这个哨兵对象的地址,被objc_autoreleasePoolPop(哨兵对象)作为入参

根据传入的哨兵对象地址找到哨兵对象所处的page

从最新加入的对象一直向前清理,可以向前跨越若干个page,直到哨兵所在的page

嵌套的AutoreleasePool就非常简单了,pop的时候总会释放到上次push的位置为止,多层的pool就是多个哨兵对象而已

 
class AutoreleasePoolPage {
    magic_t const magic;
    id *next;//下一个存放autorelease对象的地址
    pthread_t const thread; //AutoreleasePoolPage 所在的线程
    AutoreleasePoolPage * const parent;//父节点
    AutoreleasePoolPage *child;//子节点
    uint32_t const depth;//深度,也可以理解为当前page在链表中的位置
    uint32_t hiwat;
}

内省: 

  • isMemberOfClass //对象是否是某个类型的对象
  • isKindOfClass //对象是否是某个类型或某个类型子类的对象
  • isSubclassOfClass //某个类对象是否是另一个类型的子类
  • isAncestorOfObject //某个类对象是否是另一个类型的父类
  • respondsToSelector //是否能响应某个方法
  • conformsToProtocol //是否遵循某个协议

反射:

  • JSON与模型之间的相互转换
  • Method Swizzling
  • KVO的实现原理
  • 实现NSCoding的自动归档和自动解档

启动和优化:

iOS的启动流程

1、根据 info.plist 里的设置加载闪屏,建立沙箱,对权限进行检查等

2、加载可执行文件

3、加载动态链接库,进行 rebase 指针调整和 bind 符号绑定

4、Objc 运行时的初始处理,包括 Objc 相关类的注册、category 注册、selector 唯一性检查等;

5、初始化,包括了执行 +load() 方法、attribute((constructor)) 修饰的函数的调用、创建 C++ 静态全局变量。

6、执行 main 函数

7、Application 初始化,到 applicationDidFinishLaunchingWithOptions 执行完

8、初始化帧渲染,到 viewDidAppear 执行完,用户可见可操作。

启动优化

1、减少动态库的加载

2、去除掉无用的类和C++全局变量的数量

3、尽量让load方法中的内容放到首屏渲染之后再去执行,或者使用initialize替换

4、去除在首屏展现之前非必要的功能

5、检查首屏展现之前主线程的耗时方法,将没必要的耗时方法滞后或者延迟执行

crash和解决方案:

  • unrecognized selector sent to instance 方法找不到
  • 数组越界,插入空值
  • [NSDictionary initWithObjects:forKeys:]使用此方法初始化字典时,objects和keys的数量不一致时
  • NSMutableDictionary,setObject:forKey:或者removeObjectForKey:时,key为nil
  • setValue:forUndefinedKey:,使用KVC对对象进行存取值时传入错误的key或者对不可变字典进行赋值
  • NSUserDefaults 存储时key为nil
  • 对字符串操作时,传递的下标超出范围,判断是否存在前缀,后缀子串时,子串为空
  • 使用C字符串初始化字符串时,传入null
  • 对可变集合或字符串使用copy修饰并进行修改操作
  • 在空间未添加到父元素上之前,就使用autoLayout进行布局
  • KVO在对象销毁时,没有移除KVO或者多次移除KVO
  • 野指针访问
  • 死锁

1-9都可以利用Runtime进行拦截,然后进行一些逻辑处理,防止crash

NSInvalidArgumentException
向容器加入nil,引起的崩溃。

SIGSEGV

访问没有被开辟的内存或者已经被释放的内存。

NSRangeException

数组越界,字符串截取越界

 SIGABRT

异常 这是一个让程序终止的标识,会在断言、app内部、操作系统用终止方法抛出。

卡顿和解决方案:

主线程中进行IO或其他耗时操作,解决:把耗时操作放到子线程中操作

GCD并发队列短时间内创建大量任务,解决:使用线程池

文本计算,解决:把计算放在子线程中避免阻塞主线程

大量图像的绘制,解决:在子线程中对图片进行解码之后再展示

 
 
dispatch_once:

dispatch_once用原子性操作block执行完成标记位static dispatch_once_t onceToken,同时用信号量确保只有一个线程执行block,等block执行完再唤醒所有等待中的线程。

dispatch_once常被用于创建单例、swizzeld method等功能。

AutoLayout:

我们知道view的frame属性是CGRect类型,只包含origin(x,y)和size(width,height),万变不离其宗,Auto Layout的线性表达式,最终也是求解出这四个值,然后布局视图的,我们看个简单的例子:
 

通过Auto Layout来布局的过程就是将上述等式转换为frame的过程,这时我们可以注意到,如果求解ViewA的frame需要解4元1次方程组,而求解ViewB的frame需要解6元1次方程组,这还是在width和height都是直接设置的原因,如果这两个约束也依赖ViewA的话,就要求解8元1次方程组了.
 
如果页面子视图超过30个,autolayout会造成卡顿的情况。在ios12之后,页面子视图在100个之类,autolayout的性能都是ok的。

instrument:

Time Profiler:CPU分析工具分析代码的执行时间。

Core Animation:离屏渲染,图层混合等GPU耗时。

1、"Color Blended Layers":图层混合

2、"Color Hits Green and Misses Red":图层缓存

3、"Color Offscreen-Rendered Yellow":离屏渲染

Leaks:内存检测,内存泄漏检测工具。

Allocation: 内存分配情况

Zombie: 野指针检测

MVC & MVP & MVVM:

MVC:

view负责接收用户事件,并告诉控制器事件来源,控制器负责通知model进行请求数据,然后model进行数据更新,view内部监听model更新而刷新view本身

MVP

1. 各部分之间的通信,都是双向的。

2. View 与 Model 不发生联系,都通过 Presenter 传递。

3. View 非常薄,不部署任何业务逻辑,称为"被动视图"(Passive View),即没有任何主动性,而 Presenter非常厚,所有逻辑都部署在那里。

MVVM

将 Presenter 改名为 ViewModel,基本上与 MVP 模式完全一致。

唯一的区别是,它采用双向绑定(data-binding):View的变动,自动反映在 ViewModel,反之亦然。

6大设计原则,23种设计模式:

1、单一职责原则:一个类只承担一个职责

2、开放封闭原则:如果要修改代码,尽量用继承或组合的方式来扩展类的功能,而不是直接修改类的代码。这是解决if-else多的好方式

3、里式替换原则:子类可以扩展父类的功能,但不能改变父类原有的功能

4、最少知道原则:一定要做到:低耦合、高内聚

5、接口隔离原则:不要对外暴露没有实际意义的接口

6、依赖倒置原则:高层模块不应该依赖于底层模块,而应该依赖于抽象。抽象不应依赖于细节,细节应依赖于抽象。应该面向接口编程,不该面向实现类编程。

1、工厂方法模式:

凡是出现了大量的产品需要创建,并且具有共同的接口时,可以通过工厂方法模式进行创建。

2、单例模式:

3、建造者模式:

工厂类模式提供的是创建单个类的模式,而建造者模式则是将各种产品集中起来进行管理,用来创建复合对象,所谓复合对象就是指某个类具有不同的属性。

4、适配器模式:

使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。 

drawRect && layoutSubviews:

drawRect在以下情况下会被调用:
1、如果在UIView初始化时没有设置rect大小,将直接导致drawRect不被自动调用。drawRect 掉用是在Controller->loadView, Controller->viewDidLoad 两方法之后掉用的.所以不用担心在 控制器中,这些View的drawRect就开始画了.这样可以在控制器中设置一些值给View(如果这些View draw的时候需要用到某些变量 值).
2、该方法在调用sizeToFit后被调用,所以可以先调用sizeToFit计算出size。然后系统自动调用drawRect:方法。
3、通过设置contentMode属性值为UIViewContentModeRedraw。那么将在每次设置或更改frame的时候自动调用drawRect:。
4、直接调用setNeedsDisplay

drawRect方法使用注意点:

1、 drawRect:方法不能手动显示调用,必须通过调用setNeedsDisplay 或 者 setNeedsDisplayInRect,让系统自动调该方法。
2、若使用calayer绘图,只能在drawInContext: 中(类似于drawRect)绘制,或者在delegate中的相应方法绘制。同样也是调用setNeedDisplay等间接调用以上方法
3、若要实时画图,不能使用gestureRecognizer,只能使用touchbegan等方法来调用setNeedsDisplay实时刷新屏幕
 
layoutSubviews在以下情况下会被调用:
1、init初始化不会触发layoutSubviews。
2、addSubview会触发layoutSubviews。
3、设置view的Frame会触发layoutSubviews,当然前提是frame的值设置前后发生了变化。
4、滚动一个UIScrollView会触发layoutSubviews。
5、旋转Screen会触发父UIView上的layoutSubviews事件。
6、改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件。
7、直接调用setNeedLayout\layoutIfNeed。

猜你喜欢

转载自www.cnblogs.com/diyigechengxu/p/12446215.html
ios