普通视图

发现新文章,点击刷新页面。
昨天以前掘金专栏-会飞的金鱼

Objective-C之Class底层结构探索

2024年3月24日 00:34

isa 走位图

在讲 OC->Class 底层类结构之前,先看下下面这张图:

isa走位

通过isa走位图 得出的结论是: 1,类,父类,元类都包含了 isa, superclass
2,对象isa指向类对象,类对象的isa指向了元类,元类的 isa 指向了根元类,根元类 isa 指向自己
3,类的 superclass 指向父类,父类的 superclass 指向的根类,根类的superclass 指向的nil
4,元类的 superclass 指向父元类,父元类 superclass 指向的根元类,根元类 superclass 指向根类,根类 superclass 指向nil

这下又复习了 isasuperclass 走位;那么问题这些类,类对象,元类对象当中的在底层展现的数据结构是怎样呢,这是我需要探索的,于是把源码贴出来展开分析下:

struct objc_class

struct objc_class : objc_object {
    // Class ISA;
    Class superclass; 
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;  
    class_rw_t *data() const {
        return bits.data();
    }
    const class_ro_t *safe_ro() const {
        return bits.safe_ro();
    }
}

从源码没见 isa 属性,其实它继承了objc_object ,而 objc_object 中有个isa ,在运行时类图生成中会产生一个isa 指向objc_object 这个类图,而 superclass 指向它的父类;根据上面 isa , superclass 走位图就知道它的指向关系。

cache_t & class_data_bits_t

cache 方法缓存,这个作用将常调用的方法缓存下来;便于下次直接查找调用,提高查找效率。 它的结构:

struct cache_t {
struct bucket_t *buckets() const;//存储方法的散列表
mask_t mask() const;//散列表缓存长度
mask_t occupied() const;//已缓存方法个数
}
struct class_data_bits_t {
    class_rw_t* data() const;//类信息
}

bits 存储具体类信息,它需要&FAST_DATA_MASK来计算得到类心所有信息,源码如下:

FAST_DATA_MASK 掩码值

imageng

bool has_rw_pointer() const {
#if FAST_IS_RW_POINTER
        return (bool)(bits & FAST_IS_RW_POINTER);
#else
        class_rw_t *maybe_rw = (class_rw_t *)(bits & FAST_DATA_MASK);
        return maybe_rw && (bool)(maybe_rw->flags & RW_REALIZED);
#endif
}

通过源码确实需要这种方式计算能得到类的存储信息;那为什么要用这种方式去处理呢。 比如说我要得到存储在 class_rw_t 类信息信息我只要通过 FAST_DATA_MASK 掩码值就能得到它的地址信息,通过地址信息就能从内存中拿到所有类的存储信息。

那这样我的FAST_DATA_MASK掩码值不一样,我通过&计算,得到的数据信息也就不一样,不得不说苹果工程师想的周到,而且这种方式不仅isa也是这样,很多地方都用这种方式取值,大大提高访问速度,数据提取效率。

class_rw_t ,class_ro_t,class_rw_ext_t

struct class_rw_t {
     const class_ro_t *ro() const ;
     const method_array_t methods() const ;//如果是类对象:放对象方法,元类:元类对象方法
     
     const property_array_t properties() const;
     const protocol_array_t protocols() const;
     class_rw_ext_t *ext() const;
}
struct class_rw_ext_t {
    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;
    uint32_t version;
}

可以看出类的信息具体就存储在class_rw_tclass_ro_tclass_rw_ext_t 中,

剖析下class_rw_t 先看看method_array_tproperty_array_tprotocol_array_t源码结构

class property_array_t : 
    public list_array_tt<property_t, property_list_t, RawPtr>
{
    typedef list_array_tt<property_t, property_list_t, RawPtr> Super;

 public:
    property_array_t() : Super() { }
    property_array_t(property_list_t *l) : Super(l) { }
};


class protocol_array_t : 
    public list_array_tt<protocol_ref_t, protocol_list_t, RawPtr>
{
    typedef list_array_tt<protocol_ref_t, protocol_list_t, RawPtr> Super;

 public:
    protocol_array_t() : Super() { }
    protocol_array_t(protocol_list_t *l) : Super(l) { }
};

看完之后,他们都继承list_array_tt,那么 list_array_tt 是什么鬼,它数据结构是怎样的,这下在取找下它。源码如下:

template <typename Element, typename List, template<typename> class Ptr>
class list_array_tt {
 protected:
    template <bool authenticated>
    class iteratorImpl {
        const Ptr<List> *lists;
        const Ptr<List> *listsEnd;
    }
        
    using iterator = iteratorImpl<false>;
    using signedIterator = iteratorImpl<true>;

 public:
    list_array_tt() : list(nullptr) { }
    list_array_tt(List *l) : list(l) { }
    list_array_tt(const list_array_tt &other) {
        *this = other;
    }

    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;
            array_t *newArray =(array_t*)malloc(array_t::byteSize(newCount));
            newArray->count = newCount;
            array()->count = newCount;

            for (int i = oldCount - 1; i >= 0; i--)
                newArray->lists[i + addedCount] = array()->lists[i];
            for (unsigned i = 0; i < addedCount; i++)
                newArray->lists[i] = addedLists[i];
            free(array());
            setArray(newArray);
            validate();
        }
        else if (!list  &&  addedCount == 1) {
            // 0 lists -> 1 list
            list = addedLists[0];
            validate();
        } 
        else {
            // 1 list -> many lists
            Ptr<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;
            for (unsigned i = 0; i < addedCount; i++)
                array()->lists[i] = addedLists[i];
            validate();
        }
    }
    
}

我把主要地方拿去出来,可以看到 attachLists 它的目的是将一个或多个列表(List 类型)附加到某个 list_array_tt对象中。这个对象可以包含零个、一个或多个列表,这些列表可以是单个指针,也可以是指针数组。函数的输入参数是一个指向 List 指针数组的指针 addedLists 和一个无符号整数 addedCount,表示要添加的列表数量。

由此我推断它是一个数组,而且是一个二维数组存储的,所有由此得出 class_rw_tmethodspropertiesprotocols这几个属性利用二维数组取存储类的方法,协议等信息,而且是可读可写的属性。

那它设计这种二维数组有什么好处呢?当然有好处,它可以动态的给数组里面增加删除方法,很方便我们分类方法的编写完进行存储。

那搞清楚了 class_rw_t 几个重要数据存储信息,那 class_rw_t 它的作用是干什么的呢;

class_rw_t 结构体定义来看;它是在应用运行时,将OC类,分类的信息直接写入到class_rw_t结构的数据结构中,在类的方法,协议进行调用时,从里面去读取,然后常调用的方法,又存储在cache_t这个结构体中,可想而知,苹果对OC类的处理,煞费苦心。

struct class_ro_t

class_rw_t结构体中有个 class_ro_t 结构体,在探索下这个东西做什么的,它的源码如下:

struct class_ro_t {
    WrappedPtr<method_list_t, method_list_t::Ptrauth> baseMethods;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;
    property_list_t *baseProperties;
}

先说说 ivars 这个属性修饰的结构体源码如下:

struct ivar_list_t : entsize_list_tt<ivar_t, ivar_list_t, 0> {
    bool containsIvar(Ivar ivar) const {
        return (ivar >= (Ivar)&*begin()  &&  ivar < (Ivar)&*end());
    }
};

这个貌似只有一个继承 entsize_list_tt,那在探索下源码:

struct entsize_list_tt {
    uint32_t entsizeAndFlags;
    uint32_t count;
     struct iteratorImpl {
     uint32_t entsize;
        uint32_t index;  // keeping track of this saves a divide in operator-

        using ElementPtr = std::conditional_t<authenticated, Element * __ptrauth(ptrauth_key_process_dependent_data, 1, 0xdead), Element *>;

        ElementPtr element;

        typedef std::random_access_iterator_tag iterator_category;
        typedef Element value_type;
        typedef ptrdiff_t difference_type;
        typedef Element* pointer;
        typedef Element& reference;

        iteratorImpl() { }

        iteratorImpl(const List& list, uint32_t start = 0)
            : entsize(list.entsize())
            , index(start)
            , element(&list.getOrEnd(start))
        { }
     }
}

可以看出这段代码定义了一个结构体 entsize_list_tt,它内部包含一个嵌套的结构体 iteratorImpl,用于实现一个迭代器。遍历容器(如列表、数组等)的对象。

到此可以得出ivars 是一个 ivar_list_t 数组,它存储了类的属性变量信息,那protocol_list_t结构体内部也是数组形式构建的。

baseProtocolsbaseProperties 这两个属性对类的存储信息只能读取,不能写入。

所以总结的是:从 class_ro_t 结构体定义来看,它存储类的变量,方法,协议信息,而且这个结构体属于类的只读信息,它包含了类的初始信息。

class_rw_ext_t

这个结构体不在过多叙述,简单来说它是基于 class_rw_t 之后为了更好管理oc类的高级特性,比如关联属性等,衍生出来的一个结构体,包括:method_array_t ,property_arrat_t ,protocol_array_t 等定义属性类型

到这里类结构及存储所关联的信息都在这里了;来一张他们关联的结构思维图:

imageng

总结:一开始编译时,程序将类的初始信息放在 class_ro_t中,当程序运行时,将类的信息合并在一起的时候,它会将 class_ro_t 类的信息合并到 class_rw_t 结构体中去。

struct method_t

为什么要说method_t,因为它不仅在 class_ro_t 有使用,在OC底层其他地方也有使用;比如如下源码:

void method_exchangeImplementations(Method m1Signed, Method m2Signed)
{
    if (!m1Signed  ||  !m2Signed) return;

    method_t *m1 = _method_auth(m1Signed);
    method_t *m2 = _method_auth(m2Signed);

    mutex_locker_t lock(runtimeLock);

    IMP imp1 = m1->imp(false);
    IMP imp2 = m2->imp(false);
    SEL sel1 = m1->name();
    SEL sel2 = m2->name();

    m1->setImp(imp2);
    m2->setImp(imp1);


    // RR/AWZ updates are slow because class is unknown
    // Cache updates are slow because class is unknown
    // fixme build list of classes whose Methods are known externally?

    flushCaches(nil, __func__, [sel1, sel2, imp1, imp2](Class c){
        return c->cache.shouldFlush(sel1, imp1) || c->cache.shouldFlush(sel2, imp2);
    });

    adjustCustomFlagsForMethodChange(nil, m1);
    adjustCustomFlagsForMethodChange(nil, m2);
}

static IMP
_method_setImplementation(Class cls, method_t *m, IMP imp)
{
    lockdebug::assert_locked(&runtimeLock);

    if (!m) return nil;
    if (!imp) return nil;

    IMP old = m->imp(false);
    SEL sel = m->name();

    m->setImp(imp);

    // Cache updates are slow if cls is nil (i.e. unknown)
    // RR/AWZ updates are slow if cls is nil (i.e. unknown)
    // fixme build list of classes whose Methods are known externally?

    flushCaches(cls, __func__, [sel, old](Class c){
        return c->cache.shouldFlush(sel, old);
    });

    adjustCustomFlagsForMethodChange(cls, m);

    return old;
}


方法交换,实现中底层都有用到,我们探索下,先看看 method_t 源码:

struct method_t {

    // The representation of a "big" method. This is the traditional
    // representation of three pointers storing the selector, types
    // and implementation.
    struct big {
        SEL name;
        const char *types;
        MethodListIMP imp;
    };

    // A "big" method, but name is signed. Used for method lists created at runtime.
    struct bigSigned {
        SEL __ptrauth_objc_sel name;
        const char * ptrauth_method_list_types types;
        MethodListIMP imp;
    };

    // ***HACK: This is a TEMPORARY HACK FOR EXCLAVEKIT. It MUST go away.
    // rdar://96885136 (Disallow insecure un-signed big method lists for ExclaveKit)
#if TARGET_OS_EXCLAVEKIT
    struct bigStripped {
        SEL name;
        const char *types;
        MethodListIMP imp;
    };
#endif

}

可以看到这结构体中掐套了多个结构体;在把它简化下:

struct method_t {
    SEL name;//方法名
    const char *types;//包含函数具有参数编码的字符串类型的返回值
    MethodListIMP imp;//函数指针(指向函数地址的指针)
}

SEL :函数名,没特别的意义;

特点: 1,使用@selector()sel_registerName()获得 2,使用sel_getName()NSStringFromSelector()转成字符串 3,不同类中相同名字方法,对应的方法选择器是相同或相等的

底层代码结构:

/// An opaque type that represents a method selector.
typedef struct objc_selector *SEL;

types:包含了函数返回值、参数编码的字符串

imagengimageng

可以看到types在值:v16@0:8 ,可以看出name,types,IMP其实都在class_ro_t结构体中,这样确实证明了之前说的;class_ro_t结构体在运行时存储着类的初始状态数据。

v16@0:8说明下:
v:方法返回类型,这里说void,
16:第一个参数,
@:id类型第二个参数,
0:第三个参数
: :selector类型
8:第四个参数

那这种types参数又是什么鬼东西,查下了资料这叫:Type Encoding(类型编码) 怎么证明了,使用如下代码: imagepng

苹果官网types encoding表格: imageng

IMP 其实就是指向函数的指针,感觉这个就没有必要讲了。

struct cache_t

cache_t 用于 class的方法缓存,对class常调用的方法缓存下来,提高查询效率,这个上之前都已经说过;接下来看看 bucket_t

struct bucket_t

struct bucket_t {
cache_key_t _key;//函数名
IMP _imp;//函数内存地址
}

这种散列表的模型,其实在底层用一个数组展现:

imagng

其实它的内部就是一个一维数组,那可能问了,数组难道它是循环查找吗,其实不然;在它元素超找时,它是拿到你的 函数名 & mask,而这个 mask 就是 cache_t 结构体中的 mask值;计算得到函数在 散列表 存储的索引值,在通过索引拿到函数地址,进行执行。

接下来看个事例:

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

    @autoreleasepool {

        Student *stu=[Student new];

        [stu test];

        [stu test];

        [stu test];

        [stu test];

    }

    return 0;

}

如上方法:当首次调用它会去类对象中查找,在方法执行时,他会放入cache_t 缓存中,当第二次,第三次,第四次时,它就去缓存中查找。

imagpng

当方法执行后;我们看到 _mask 是:3,这个3代表了我类中定义了三个函数;而——_occupied 是一个随意的值;它其实代表了换存方法的个数。

那如何知道方法有缓存了,再继续往下执行:

imageng

这时候执行完 test02, _mask的值从 3 变成了 7 ,说明散列表 bucket_t 做了扩容操作。在这里bucket_t 元素需要 _mask 个元素,所以最终 bucket_t 从原有的3个元素进行了 2倍 扩容。

在看下方法是否进行缓存:

imageng

可以看见当执行完 [stu test02] 时,数据做了扩容,并且扩容的数据使用(null) 进行填充。

在看个事例:

imageng

在执行 [stu test] 之前;其实bucket_t 就3个元素,并且存入了 init 方法;

imageng

当执行完 [stu test] 之后;就存入 test 方法。

但是注意的地方:它在扩容时对之前的缓存进行清除。

image.png

通过查看源码,我们知道了它如何进行清除操作,

imageng

当执行完 [stu test02];[stu test03]; 之后,它先将缓存清空;这时候 init , test 方法被清空,bucket_t扩容完在存储:test02test03 方法。

那问题又来了,它是如何快速定位到方法的,然后执行的?接下来看看代码:

imagepng

可以清楚看见,当我使用 @selector(test03)&stu_cache._mask 就可以得到下标,然后再从 bucket_t 拿到方法。

到这里 class结构,类的方法缓存到此结束了,从上面也可以思考下:如果自己去实现散列表数组,是不是思路就跟清晰了。

谢谢大家!青山不改,绿水长流。后会有期!

iOSload和initialize详解

2023年8月5日 14:19

load和initialize详解

在介绍之前,我们首先来了解一下类的使用,我们要使用一个类,大概要经过以下步骤
1启动App,程序开始加载类到内存中(代码区)+(void)load
2首次使用该类时,创建类对象(我们可以把它看作是一个单例,它在整个程序中只有一份)+(void)initialize
3通过类对象创建实例对象+(instancetype)alloc、-(instancetype)init
4通过实例对象,我们就可使用实例方法、类属性了
从上面的步骤我们也大概了解到load和initialize的调用时机了,下面在来详细说一下

一、+load类方法

当类被引用进项目的时候就会执行load函数(在main函数开始执行之前),与这个类是否被用到无关,每个类的load函数只会自动调用一次.由于load函数是系统自动加载的,因此不需要再调用[super load],否则父类的load函数会多次执行。
●1.当父类和子类都实现load函数时,父类的load方法执行顺序要优先于子类
●2.当一个类未实现load方法时,不会调用父类load方法
●3.类中的load方法执行顺序要优先于类别(Category)
●4.当有多个类别(Category)都实现了load方法,这几个load方法都会执行,但执行顺序不确定(其执行顺序与类别在Compile Sources中出现的顺序一致)
●5.当然当有多个不同的类的时候,每个类load 执行顺序与其在Compile Sources出现的顺序一致

二、+initialize类方法

●即使类文件被引用进项目,但是没有使用,initialize不会被调用
●假如这个类放到代码中,而这段代码并没有被执行,这个函数是不会被执行的。
●类或者其子类的第一个方法被调用前调用
●由于是系统自动调用,也不需要再调用 [super initialize],否则父类的initialize会被多次执行
●父类的initialize方法会比子类先执行
●子类未实现initialize方法时,会调用父类initialize方法,子类实现initialize方法时,会覆盖父类initialize方法.
●当有多个Category都实现了initialize方法,会覆盖类中的方法,只执行一个(会执行Compile Sources 列表中最后一个Category 的initialize方法)

三、两者的异同
1、相同点
1load和initialize会被自动调用,不能手动调用它们。
2如果父类和子类都被调用,父类的调用一定在子类之前
3子类实现了load和initialize的话,会隐式调用父类的load和initialize方法。
4load和initialize方法内部使用了锁,因此它们是线程安全的。
2、不同点
1调用顺序不同,以main函数为分界,+load方法在main函数之前执行,+initialize在main函数之后执行。
2子类中没有实现+load方法的话,不会调用父类的+load方法;而子类如果没有实现+initialize方法的话,也会自动调用父类的+initialize方法。
3+load方法是在类被装在进来的时候就会调用,+initialize在第一次给某个类发送消息时调用(比如实例化一个对象),并且只会调用一次,是懒加载模式,如果这个类一直没有使用,就不会调用到+initialize方法。
四、使用场景
1.+load一般是用来交换方法Method Swizzle,由于它是线程安全的,而且一定会调用且只会调用一次,通常在使用UrlRouter的时候注册类的时候也在+load方法中注册。

+(void)load {

static dispatch_once_t oneToken;

dispatch_once(&oneToken, ^{

    Method imageNamed = class_getClassMethod(self,@selector(imageNamed:));

    Method mkeImageNamed =class_getClassMethod(self,@selector(swizze_imageNamed:));

    method_exchangeImplementations(imageNamed, mkeImageNamed);

    });

}

+ (instancetype)swizze_imageNamed:(NSString*)name {

//这个时候swizze_imageNamed已经和imageNamed交换imp,所以当你在调用swizze_imageNamed时候其实就是调用imageNamed

UIImage * image;

if( IS_IPHONE ){

// iphone处理

UIImage * image = [self swizze_imageNamed:name];

    if (image != nil) {

        return image;

    } else {

        return nil;

    }

} else {

    // ipad处理,_ipad是自己定义,~ipad是系统自己自定义。

    UIImage *image = [self swizze_imageNamed:[NSString stringWithFormat:@"%@_ipad",name]];

    if (image != nil) {

            return image;

        }else {

            image = [self swizze_imageNamed:name];

            return image;

    }

}

2.+initialize方法主要用来对一些不方便在编译期初始化的对象进行赋值,或者说对一些静态常量进行初始化操作。


/ In Person.m

// int类型可以在编译期赋值

static int someNumber = 0;

static NSMutableArray *someArray;

+ (void)initialize {

if (self == [Person class]) {

    // 不方便编译期复制的对象在这里赋值

    someArray = [[NSMutableArray alloc] init];

    }

}

五、总结
1load和initialize方法都会在实例化对象之前调用
2load执行在main函数以前,initialize执行main函数之后
3这两个方法会被自动调用,不能手动调用它们。
4load和initialize方法都不用显示的调用父类的方法而是自动调用
5子类没有initialize方法也会调用父类的方法,而load方法则不会调用父类
6initialize方法对一个类而言只会调用一次(Person、或者是Person+Category)都是一个Perosn类。load方法则是每个都会调用,只要你写了load方法,添加到工程都会实现。
7load方法通常用来进行Method Swizzle,initialize方法一般用于初始化全局变量或静态变量。
8load和initialize方法内部使用了锁,因此它们是线程安全的。实现时要尽可能保持简单,避免阻塞线程,不要再使用锁。

聊聊iOS自动释放池AutoreleasePool

2023年8月5日 13:56

自动释放池

●自动释放池的主要底层数据结构是:__AtAutoreleasePool、AutoreleasePoolPage
●调用了autorelease的对象最终都是通过AutoreleasePoolPage对象来管理的
●是和线程一一对应的

@autoreleasepool {
    //...
}
// 编译器会将上面代码改写为一下代码
void *ctx = objc_autoreleasePoolPush();
//{}中的代码
objc_autoreleasePoolPop(ctx)

源码分析
●clang重写@autoreleasepool
●objc4源码:NSObject.mm

imag2212

AutoreleasePoolPage的结构 
●每个AutoreleasePoolPage对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放autorelease对象的地址
●所有的AutoreleasePoolPage对象通过以栈为结点,双向链表的形式连接在一起

imag749274jhjdsng

●调用push方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址
●调用pop方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY
●id *next指向了下一个能存放autorelease对象地址的区域  

Runloop和Autorelease 
App启动后,iOS在主线程的Runloop中注册了2个Observer管理和维护AutoreleasePool,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler(),打印currentRunLoop可以看到。

<CFRunLoopObserver 0x6080001246a0 [0x101f81df0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1020e07ce), context = <CFArray 0x60800004cae0 [0x101f81df0]>{type = mutable-small, count = 0, values = ()}}
<CFRunLoopObserver 0x608000124420 [0x101f81df0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1020e07ce), context = <CFArray 0x60800004cae0 [0x101f81df0]>{type = mutable-small, count = 0, values = ()}}

●第1个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush()创建自动释放池,其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
●第二个 Observer 监视了两个事件,这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池发生在其他所有回调之后
○监听了kCFRunLoopBeforeWaiting(准备进入休眠)时,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush()释放旧的池并创建新池;
○监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()

dc6bff9c5a

主线程的其他操作通常均在这个AutoreleasePool之内(main函数中),以尽可能减少内存维护操作(当然你如果需要显式释放【例如循环】时可以自己创建AutoreleasePool否则一般不需要自己创建)。

常见面试题:

  1. 自动释放池是什么时候创建的?什么时候销毁的?

●创建,运行循环检测到事件并启动后,就会创建自动释放池
●销毁:一次完整的运行循环结束之前,会被销毁

  1. 以上代码是否有问题?如果有,如何解决?
for (long i = 0; i < largeNumber; ++i) {
    NSString *str = [NSString stringWithFormat:@"hello - %ld", i];
    str = [str uppercaseString];
    str = [str stringByAppendingString:@" - world"];
}

解决方法:引入自动释放池

●1> 外面加自动释放池(快?):能够保证for循环结束后,内部产生的自动释放对象,都会被销毁,需要等到 for 结束后,才会释放内存
●2> 里面加自动释放池(慢?):能够每一次 for 都释放产生的自动释放对象!


- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {

    NSLog(@"start");
    CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
    [self answer1];
    NSLog(@"外 %f", CFAbsoluteTimeGetCurrent() - start);

    start = CFAbsoluteTimeGetCurrent();
    [self answer2];
    NSLog(@"内 %f", CFAbsoluteTimeGetCurrent() - start);
}

- (void)answer1 {
    @autoreleasepool {
        for (long i = 0; i < largeNumber; ++i) {
            NSString *str = [NSString stringWithFormat:@"hello - %ld", i];
            str = [str uppercaseString];
            str = [str stringByAppendingString:@" - world"];
        }
    }
}

- (void)answer2 {
    for (long i = 0; i < largeNumber; ++i) {
        @autoreleasepool {
            NSString *str = [NSString stringWithFormat:@"hello - %ld", i];
            str = [str uppercaseString];
            str = [str stringByAppendingString:@" - world"];
        }
    }
}

●实际测试结果,是运行循环放在内部的速度更快!
●日常开发中,如果遇到局部代码内存峰值很高,可以引入自动释放池及时释放延迟释放对象

真心送你一份iOS核心动画总结篇,赶紧上车,学完它才觉得物有所值,还有实战案例!!!

2023年7月3日 18:25

本文案列代码在篇尾,有小伙伴需要代码可以移至篇尾获取。

在iOS开发中,动画是提高用户体验重要的环节之一。一个设计严谨、精细的动画效果能给用户耳目一新的效果,这对于app而言是非常重要的。

简介

iOS动画主要是指Core Animation框架。官方使用文档地址为:Core Animation Guide。Core Animation是iOS和macOS平台上负责图形渲染与动画的基础框架。Core Animation可以作用与动画视图或者其他可视元素,为你完成了动画所需的大部分绘帧工作。你只需要配置少量的动画参数(如开始点的位置和结束点的位置)即可使用Core Animation的动画效果。Core Animation将大部分实际的绘图任务交给了图形硬件来处理,图形硬件会加速图形渲染的速度。这种自动化的图形加速技术让动画拥有更高的帧率并且显示效果更加平滑,不会加重CPU的负担而影响程序的运行速度。

Core Animation

Core Animation是一组非常强大的动画处理API,它的子类主要有4个:CABasicAnimation、CAKeyframeAnimation、CATransition、CAAnimationGroup。 Core Animation类的继承关系图:

Animation

属性

duration:动画的持续时间 beginTime:动画的开始时间 repeatCount:动画的重复次数 autoreverses:动画按照原动画返回执行 timingFunction:控制动画的显示节奏系统提供五种值选择,分别是:

  • kCAMediaTimingFunctionLinear 线性动画
  • kCAMediaTimingFunctionEaseIn 先快后慢
  • kCAMediaTimingFunctionEaseOut 先慢后快
  • kCAMediaTimingFunctionEaseInEaseOut 先慢后快再慢
  • kCAMediaTimingFunctionDefault 默认,也属于中间比较快

delegate:动画代理。能够检测动画的执行和结束。 path:帧动画中的执行路径 type:过渡动画的动画类型。主要有以下4中类型:

  • kCATransitionFade 渐变效果
  • kCATransitionMoveIn 进入覆盖效果
  • kCATransitionPush 推出效果
  • kCATransitionReveal 离开效果

subtype:过渡动画的动画方向。

  • kCATransitionFromRight 从右侧进入
  • kCATransitionFromLeft 从左侧进入
  • kCATransitionFromTop 从顶部进入
  • kCATransitionFromBottom 从底部进入

动画的使用

动画使用步骤:

  1. 初始化一个动画对象(CAAnimation)并设置一些动画相关属性.
  2. 添加动画对象到层(CALayer)中,开始执行动画.

CALayer中很多属性都可以通过CAAnimation实现动画效果, 包括opacity, position, transform, bounds, contents等,具体可以在API文档中查找

通过调用CALayer的addAnimation:forKey:增加动画到层(CALayer)中,这样就能触发动画了.通过调用removeAnimationForKey:可以停止层中的动画.

UIView

_demoView.frame = CGRectMake(0, SCREEN_HEIGHT/2-50, 50, 50); 
[UIView animateWithDuration:1.0f animations:^{
    _demoView.frame = CGRectMake(SCREEN_WIDTH, SCREEN_HEIGHT/2-50, 50, 50); 
} completion:^(BOOL finished) { 
    _demoView.frame = CGRectMake(SCREEN_WIDTH/2-25, SCREEN_HEIGHT/2-50, 50, 50); 
}];

UIView [begin commit]

_demoView.frame = CGRectMake(0, SCREEN_HEIGHT/2-50, 50, 50);
[UIView beginAnimations:nil context:nil]; 
[UIView setAnimationDuration:1.0f];
_demoView.frame = CGRectMake(SCREEN_WIDTH, SCREEN_HEIGHT/2-50, 50, 50); 
[UIView commitAnimations];

Core Animation

CABasicAnimation *anima = [CABasicAnimation animationWithKeyPath:@"position"]; 
anima.fromValue = [NSValue valueWithCGPoint:CGPointMake(0, SCREEN_HEIGHT/2-75)]; 
anima.toValue = [NSValue valueWithCGPoint:CGPointMake(SCREEN_WIDTH, SCREEN_HEIGHT/2-75)];
anima.duration = 1.0f;
[_demoView.layer addAnimation:anima forKey:@"positionAnimation"];

动画详解

CABaseAnimation

基础动画主要提供了对于CALayer对象中的可变属性进行简单动画的操作。比如:位移、透明度、缩放、旋转、背景色等等。 主要提供如下属性: fromValue:keyPath对应的初始值 toValue:keyPath对应的结束值 示例:

CABaseAnimation

1.呼吸动画 
CABasicAnimation *animation =[CABasicAnimation animationWithKeyPath:@"opacity"]; 
animation.fromValue = [NSNumber numberWithFloat:1.0f]; 
animation.toValue = [NSNumber numberWithFloat:0.0f]; 
animation.autoreverses = YES; //回退动画(动画可逆,即循环) 
animation.duration = 1.0f; 
animation.repeatCount = MAXFLOAT; 
animation.removedOnCompletion = NO; 
animation.fillMode = kCAFillModeForwards;//removedOnCompletion,fillMode配合使用保持动画完成效果 
animation.timingFunction=[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
[self.alphaTagButton.layer addAnimation:animation forKey:@"aAlpha"]; 
2.摇摆动画 //设置旋转原点 
self.sharkTagButton.layer.anchorPoint = CGPointMake(0.5, 0); 
CABasicAnimation* rotationAnimation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"]; //角度转弧度(这里用1,-1简单处理一下) 
rotationAnimation.toValue = [NSNumber numberWithFloat:1]; 
rotationAnimation.fromValue = [NSNumber numberWithFloat:-1]; 
rotationAnimation.duration = 1.0f;
rotationAnimation.repeatCount = MAXFLOAT; 
rotationAnimation.removedOnCompletion = NO; 
rotationAnimation.autoreverses = YES; 
rotationAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; 
rotationAnimation.fillMode = kCAFillModeForwards; 
[self.sharkTagButton.layer addAnimation:rotationAnimation forKey:@"revItUpAnimation"];

注意: 如果fillMode=kCAFillModeForwards和removedOnComletion=NO,那么在动画执行完毕后,图层会保持显示动画执行后的状态。但在实质上,图层的属性值还是动画执行前的初始值,并没有真正被改变。这就相当于Android早期的View动画。

CAKeyframeAnimation

CAKeyframeAnimation和CABaseAnimation都属于CAPropertyAnimatin的子类。CABaseAnimation只能从一个数值(fromValue)变换成另一个数值(toValue),而CAKeyframeAnimation则会使用一个NSArray保存一组关键帧。

主要属性: values:就是上述的NSArray对象。里面的元素称为”关键帧”(keyframe)。动画对象会在指定的时间(duration)内,依次显示values数组中的每一个关键帧 path:可以设置一个CGPathRef\CGMutablePathRef,让层跟着路径移动。path只对CALayer的anchorPoint和position起作用。如果你设置了path,那么values将被忽略。 keyTimes:可以为对应的关键帧指定对应的时间点,其取值范围为0到1.0,keyTimes中的每一个时间值都对应values中的每一帧.当keyTimes没有设置的时候,各个关键帧的时间是平分的。 示例:

CAKeyframeAnimation

values属性应用

-(void)setUpCAKeyframeAnimationUseValues { 
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animation]; animation.keyPath = @"position";
    NSValue *value1 = [NSValue valueWithCGPoint:CGPointMake(50, 50)]; 
    NSValue *value2 = [NSValue valueWithCGPoint:CGPointMake(kWindowWidth - 50, 50)];
    NSValue *value3 = [NSValue valueWithCGPoint:CGPointMake(kWindowWidth - 50, kWindowHeight-50)]; 
    NSValue *value4 = [NSValue valueWithCGPoint:CGPointMake(50, kWindowHeight-50)];
    NSValue *value5 = [NSValue valueWithCGPoint:CGPointMake(50, 50)]; 
    animation.values = @[value1,value2,value3,value4,value5]; 
    animation.repeatCount = MAXFLOAT; animation.removedOnCompletion = NO; 
    animation.fillMode = kCAFillModeForwards; 
    animation.duration = 6.0f; animation.timingFunction=[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; 
    [self.keyButton.layer addAnimation:animation forKey:@"values"]; 
}

path方式应用

-(void)setUpCAKeyframeAnimationUsePath { 
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animation]; animation.keyPath = @"position";
    CGMutablePathRef path = CGPathCreateMutable(); 
    //矩形线路 
    CGPathAddRect(path, NULL, CGRectMake(50,50, kWindowWidth - 100,kWindowHeight - 100)); 
    animation.path=path; CGPathRelease(path);
    animation.repeatCount = MAXFLOAT; 
    animation.removedOnCompletion = NO; 
    animation.fillMode = kCAFillModeForwards;
    animation.duration = 10.0f; 
    animation.timingFunction=[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; 
    [self.keyButton.layer addAnimation:animation forKey:@"path"]; 
}

keyTimes属性使用

-(void)setUpCAKeyframeAnimationUsekeyTimes { 
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animation]; 
    animation.keyPath = @"position.x"; 
    animation.values = @[@0, @20, @-20, @20, @0]; 
    animation.keyTimes = @[ @0, @(1 / 6.0), @(3 / 6.0), @(5 / 6.0), @1 ]; 
    animation.duration = 0.5;
    animation.additive = YES; 
    [self.sharkTagButton.layer addAnimation:animation forKey:@"keyTimes"];
}

CAAnimationGroup

CAAnimationGroup(组动画)是CAAnimation的子类,可以保存一组动画对象,将CAAnimationGroup对象加入层后,组中所有动画对象可以同时并发运行。有点类似于Android的帧动画,不过这里的组动画是将一些基础的动画拼接而成的,比如同时缩小、旋转、渐变。

主要属性有: animations:用来保存一组动画对象的NSArray。 示例:

CAAnimationGroup

CABasicAnimation * animationScale = [CABasicAnimation animation]; 
animationScale.keyPath = @"transform.scale"; 
animationScale.toValue = @(0.1); 
CABasicAnimation *animationRota = [CABasicAnimation animation]; animationRota.keyPath = @"transform.rotation"; 
animationRota.toValue = @(M_PI_2); 
CAAnimationGroup * group = [[CAAnimationGroup alloc] init]; 
group.duration = 3.0; 
group.fillMode = kCAFillModeForwards; 
group.removedOnCompletion = NO; 
group.repeatCount = MAXFLOAT; 
group.animations = @[animationScale,animationRota]; 
[self.groupButton.layer addAnimation:group forKey:nil];

CATransition

CAAnimation的子类,用于做过渡动画或者转场动画,能够为层提供移出屏幕和移入屏幕的动画效果。 重要属性有: type:动画过渡类型,官方提供了如下类型:

  • kCATransitionFade 渐变效果
  • kCATransitionMoveIn 进入覆盖效果
  • kCATransitionPush 推出效果
  • kCATransitionReveal 揭露离开效果

subtype:动画过渡方向。

  • kCATransitionFromRight 从右侧进入
  • kCATransitionFromLeft 从左侧进入
  • kCATransitionFromTop 从顶部进入
  • kCATransitionFromBottom 从底部进入
  • startProgress:动画起点(在整体动画的百分比)
  • endProgress:动画终点(在整体动画的百分比)

CATransition

示例:

MyViewController *myVC = [[MyViewController alloc]init]; 
CATransition *animation = [CATransition animation]; 
animation.timingFunction = UIViewAnimationCurveEaseInOut; 
animation.type = @"cube"; animation.duration =0.5f; 
animation.subtype =kCATransitionFromRight; //控制器间跳转动画 
[[UIApplication sharedApplication].keyWindow.layer addAnimation:animation forKey:nil]; 
[self presentViewController:myVC animated:NO completion:nil];

github.com/yongliangP

❌
❌