普通视图

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

黄子韬送车直播点赞数变负数,背后引起程序员技术的思考!

2025年1月17日 14:40

目录:
1. 现象
2. 原因
3. 为什么
4. 优化
5. 总结

image.png

现象

最近,黄子韬的直播间因送车而吸引了大量粉丝的关注,短时间内,粉丝数暴涨千万,堪称一次直播界的“爆发性增长”。然而,令人意外的技术问题也在这次直播中浮现——点赞数竟然出现了负数!这一现象引发了大家对直播平台技术背后细节的热议。

QQ_1737095280875.png

原因

  • 高并发访问:  在直播间的高流量下,短时间内会有极大量的点赞请求。尤其是当黄子韬进行送车等活动时,粉丝互动频繁,点赞数激增。假设某个时刻,直播间的点赞数迅速突破了int类型的最大值,那么就会触发溢出,最终导致点赞数显示为负值。

  • 数据类型选择不当:  由于平台早期的设计没有考虑到如此极端的情况,存储点赞数的字段选择了int类型,最大值是2147483647,而没有使用更大的数据类型,如long(64位整数)。当点赞数远远超出int类型的限制时,会显示负数。

    来看看各个数据类型的取值范围!

    图片

为什么超出int类型的限制会显示负数

因为在计算机内部 int 类型的内存存储采用二进制补码的形式表示,超出范围后会环绕到负数区间。在 int 类型中,通常使用 4 个字节(32 位)来存储整数值。其中,最高位表示符号位,0 表示正数,1 表示负数。

如果int 类型来存储一个int的最大值 2147483647,二进制表示如下

01111111 11111111 11111111 11111111

如果再将这个值加 1,则二进制表示如下(即补码)

10000000 00000000 00000000 00000000

这个值超出了 int 类型能表示的范围,因此会溢出,而由于最高位为 1,表示负数,计算机中的负数用补码表示的。为了将其转换为十进制数,我们需要使用二进制补码的转换规则:补码=反码+1,那么

反码=补码-1

01111111 11111111 11111111 11111111

反码(原码除了符号位,每一位取反,即0变为1,1变为0),那

原码如下,即2的31次方,因为是负数,所以值是-2的31次方

10000000 00000000 00000000 00000000

如何优化

  • 使用更大的数据类型:  将点赞数的存储类型由int升级为longlong类型的最大值为9223372036854775807,远远超过了大多数社交平台可能遇到的点赞数。因此,选择long类型可以确保平台在高并发、高流量的情况下仍能准确存储点赞数。
  • 数据溢出保护机制:  对于这种极端情况,可以引入数据溢出保护机制。比如,当点赞数接近int的最大值时,系统可以提前进行警告,或者通过智能算法限制点赞数的增长速度,防止一次性飙升至不合理的数值。
  • 实时监控与调整:  在直播间这种特殊场景下,可以设立实时监控机制,动态调整点赞数的存储方案。例如,当发现点赞数过快增长时,平台可以临时切换到更大的数据存储类型,避免由于类型限制导致的问题。
  • 缓存机制的优化:  在高并发的环境中,单一数据库存储可能会存在瓶颈。因此,合理使用分布式缓存系统,如Redis自增方案,可以有效降低数据库负载,并提高点赞数更新的效率。通过缓存层的优化,还能减少点赞数数据因并发过高而导致的存储问题。

总结

黄子韬直播间点赞数变负数的现象,不仅仅是一次技术失误的体现,更提醒了我们在面对极端流量时,技术架构的弹性与可扩展性的重要性。随着互联网平台规模的不断扩大,开发者在设计系统时,必须要充分考虑到数据的最大值、并发访问、溢出保护等一系列问题,避免类似情况的发生。

从这次事件可以看到,技术在背后发挥的巨大作用。而我们作为开发者和技术从业者,更应时刻保持敏锐的技术视角,才能在风口浪尖上为平台保驾护航。

Flutter封装的路由工具类RouteUtils,可直接二次开发

2024年12月28日 17:23
/**
 * 路由封装
 */
class RouteUtils {
  
  RouteUtils._();

  static final navigatorKey = GlobalKey<NavigatorState>();

// App 根节点Context
  static BuildContext get context => navigatorKey.currentContext!;

  static NavigatorState get navigator => navigatorKey.currentState!;

  ///普通动态跳转-->page
  static Future push(
    BuildContext context,
    Widget page, {
    bool? fullscreenDialog,
    RouteSettings? settings,
    bool maintainState = true,
  }) {
    return Navigator.push(
        context,
        MaterialPageRoute(
          builder: (_) => page,
          fullscreenDialog: fullscreenDialog ?? false,
          settings: settings,
          maintainState: maintainState,
        ));
  }

  //常规路由导航
  static Future pushForName(
    BuildContext context,
    String routeName, {
    Object? arguments,
  }) {
    return Navigator.pushNamed(context, routeName, arguments: arguments);
  }

  ///白定义route动态跳转
  static Future pushForPageRoute(BuildContext context, Route route) {
    return Navigator.push(context, route);
  }

  //清空栈,只留目标页面
  static Future pushNamedAndRemoveUntil(
    BuildContext context,
    String name, {
    Object? arguments,
  }) {
    return Navigator.pushNamedAndRemoveUntil(context, name, (route) => false,
        arguments: arguments);
  }

  //清空栈,只留目标页面
  static Future pushAndRemoveUntil(
    BuildContext context,
    Widget page, {
    bool? fullscreenDialog,
    RouteSettings? settings,
    bool maintainState = true,
  }) {
    return Navigator.pushAndRemoveUntil(
        context,
        MaterialPageRoute(
          builder: (_) => page,
          fullscreenDialog: fullscreenDialog ?? false,
          settings: settings,
          maintainState: maintainState,
        ),
        (route) => false);
  }

  //用新的路由替换当路由
  static Future pushReplacement(BuildContext context, Route route,
      {Object? result}) {
    return Navigator.pushReplacement(context, route, result: result);
  }

  //用新的路由替换当路由
  static Future pushReplacementNamed(
    BuildContext context,
    String name, {
    Object? result,
    Object? arguments,
  }) {
    return Navigator.pushReplacementNamed(context, name,
        arguments: arguments, result: result);
  }

  //关闭当前页面
  static void pop(BuildContext context) {
    Navigator.pop(context);
  }

  ///关闭当前页面:包含返回值
  static void popOfData<T extends Object?>(BuildContext context, {T? data}) {
    Navigator.of(context).pop(data);
  }
}

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结构,类的方法缓存到此结束了,从上面也可以思考下:如果自己去实现散列表数组,是不是思路就跟清晰了。

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

SwiftUI从入门到精髓

2024年2月28日 17:36

SwiftUI

序言

开年的第一篇文章,今天分享的是SwiftUI,SwiftUI出来好几年,之前一直没学习,所以现在才开始;如果大家还留在 iOS 开发,这门语言也是一个趋势; 目前待业中.... 不得不说已逝的2023,大家开始都抱着一解封,经济都会向上转好,可是现实不是我们想象那样;目前我也在学习 SwiftUI,并且努力找工作中....。至于 2024 年经济如何,咱们作为老百姓在大环境和全球经济影响下;坦然面对,提升自己。 这里不得不说国人坚韧不拔的精神。“卷” -- 努力吧Coding人

SwiftUI体验

Xcode创建项目之后出现工程默认创建的UI界面;如下

swiftUI

一开始心里对自己说:"SwiftUI作为iOS开发新的UI体系,为啥初创的项目这么多代码,给初学者看到,一种压迫感,心想这语法好复杂,不想学了";不管你是不是这样心里,我刚开始看见,这么一坨代码,没什么心思,于是索性删掉;按自己能理解学习的方式来操作;于是做了简化:

import SwiftUI
import SwiftData

struct ContentView: View {
   
    var body: some View {
        Text("hello,word")
    }
}

#Preview {
    ContentView()
        .modelContainer(for: Item.self, inMemory: true)
}

关键字 some

关键字some啥玩意儿,完全陌生;先看看View;点击进入源码结构查看:

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
public protocol View {

    /// The type of view representing the body of this view.
    ///
    /// When you create a custom view, Swift infers this type from your
    /// implementation of the required ``View/body-swift.property`` property.
    associatedtype Body : View

    @ViewBuilder @MainActor var body: Self.Body { get }
}

一堆英文注解估计大家不喜欢看,我就没贴出来了;简单来说: View 是一个泛型协议,它定义了所有视图类型需要遵循的接口,通过some修饰;表示 "我返回一个满足View 协议的某种类型"。some关键字告诉 Swift,虽然我们知道body必须返回一个View,但我们不确定具体是哪种 View(例如,TextImageVStack 等)。

协议里有一个associatedtypebody,其实这种协议就是当作约束形式使用;只要遵守这种协议编译器每次闭包中返回的一定是一个确定,遵守View协议的类型。

那么苹果工程师利用Swift5.1 Opaque return types 特性,为开发者提供了一个灵活的开发模式,抹掉了具体的类型,不需要修改公共API来确定每次闭包的返回类型,也降低了代码书写难度。(学学苹果那些大神思想,不错)

在来看看Preview

struct ContentView_Previews:PreviewProvider{
    static var previews: some View{
        ContentView()
        
    }
}

PreviewProvider就一个协议类,它的作用提供swiftUI不用运行,就能直接看到UI渲染变化,我觉得这个挺好,减少开发人员对UI运行测试次数和时间,而previews就是一个静态属性,返回一个 View 对象,用于在预览面板中展示。

@State属性包装器

@State属性包装器解决UI界面上,数据同步以及及时刷新的功能。一般来说数据更新完,界面 UI 同时更新。在 SwiftUI里面,视图中声明的任何状态、内容和布局,源头一旦发生改变,会自动更新视图,因此,只需要一次布局,这个时候出现了@State,它来解决与UI之间数据状态问题。

它的概念就是:@State 是一个属性包装器 (property wrapper) ,用于声明状态属性 (state property) 当状态属性发生变化时,SwiftUI 会自动更新视图以反映最新的状态。

属性的值被存储在特殊的内存区域中,这个区域与 View struct 是隔离的 至于被它修饰的属性内存存储与分布现在无从得知,还没学习到那么深入,这事儿慢慢来,不是一天两天的,先上个代码看看它怎么使用的:

import SwiftUI

struct StateBootcamp: View {
    
    @State var bgkColor:Color = Color.blue
    @State var cut:Int = 0
    
    var body: some View {
        
        ZStack{
            
            bgkColor
                .ignoresSafeArea(.all)
            
            VStack(spacing: 20){
                
                Text("Hello, World!")
                    .font(.title)
                
                Text("count:\(cut)")
                    .font(.largeTitle)
                
                HStack(spacing: 20){
                    Button("Button01") {
                        cut+=1
                        bgkColor = Color.red
                    }
                    .font(.title)
                    .foregroundColor(.white)
                    
                    Button("Button02") {
                        cut-=1
                        bgkColor = .purple
                    }
                    .font(.title)
                    .foregroundColor(.white)
                }
                Button("默认"){
                    cut=0
                    bgkColor = .blue
                }
                .font(.title)
                .foregroundColor(.white)
            }
        }
    }
}

#Preview {
    StateBootcamp()
}

其实一看代码,就一幕了然,知道它的使用与作用;如果你写过swift代码,这些东西很好理解,但是只会OC,那么我建议你学习下swift;在来看 swiftUI 语法糖才更好理解。

在看看源码:

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@frozen @propertyWrapper public struct State<Value> : DynamicProperty {
    public init(wrappedValue value: Value)
    public init(initialValue value: Value)
    public var wrappedValue: Value { get nonmutating set }
    public var projectedValue: Binding<Value> { get }
}


@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension State where Value : ExpressibleByNilLiteral {

    /// Creates a state property without an initial value.
    ///
    /// This initializer behaves like the ``init(wrappedValue:)`` initializer
    /// with an input of `nil`. See that initializer for more information.
    @inlinable public init()
}


可以看到State是一个结构体,由@propertyWrapper包装的。@propertyWrapper是属性包装器。property wrapper 做的事情大体如下:

-   为底层的存储变量`State<Int>`自动提供一组 **getter****setter** 方法,结构体内保存了`Int`的具体数值;

-   在 body 首次求值前,将`State<Int>`关联到当前`View`上,为它在堆中对应当前`View`分配一个存储位置。

-`@State`修饰的变量设置观察,当值改变时,触发新一次的`body`求值,并刷新 UI。

SwiftUI 基础组件

Spacer垫片 :先贴贴代码

import SwiftUI

struct SpacerBootcampDemo: View {
    var body: some View {
        Text("Spacer UP")
            .font(.largeTitle)
        
        Spacer()
            .frame(width: 37)
            .background(.blue)
        
        Text("Spacer Down")
            .font(.largeTitle)
        
    }
}

#Preview {
    SpacerBootcampDemo()
}

在看看效果图:

Spacer

总结:Spacer 是一个灵活的空间视图,它的主要作用是在布局中自动调整自身的高度和宽度,以填满特定的空间;简单来说,它就是一个垫片,调整自身视图的高度,如果它周围有其他视图,也会受到 Spacer 影响。

ScrollView 如果你之前使用 UIkit 框架开发,在用 SwiftUI ,一下有点不适应,代码和之前的 UIkit 开发模式不太一样,但是大大缩短UI编写时间;先上代码:


import SwiftUI

struct ScollViewBootcamp: View {
    
    var body: some View {
        
        ScrollView{
            LazyVStack{
                ForEach(0..<20){
                    (idx) in
                    
                    VStack {
                        
                        Text("Hello, World!")
                            .font(.title)
                            .foregroundStyle(.white)
                            .frame(width: UIScreen.main.bounds.width-20,height: 350)
                            .background(Color.init(cgColor: CGColor(red: CGFloat.random(in: 0..<215)/255.0, green: CGFloat.random(in: 0..<235)/255.0, blue: CGFloat.random(in: 0...247)/255.0, alpha: 0.9)))
                            .clipShape(RoundedRectangle(cornerRadius: 10))
                        
                        Rectangle()
                            .fill(Color.init(cgColor: CGColor(red: CGFloat.random(in: 0...187)/255.0, green: CGFloat.random(in: 0..<210)/255.0, blue: CGFloat.random(in: 0...237)/255.0, alpha: 0.9)))
                            .frame(width: UIScreen.main.bounds.width-20,height: 530)
                            .clipShape(RoundedRectangle(cornerRadius: 10))
                        
                        
                        ScrollView(.horizontal,showsIndicators: false,content: {
                            LazyHStack{
                                ForEach(0..<10){
                                    idx in
                                    Rectangle()
                                        .fill(Color.init(cgColor: CGColor(red: CGFloat.random(in: 0...167)/255.0, green: CGFloat.random(in: 0...131)/255.0, blue: CGFloat.random(in: 0...89)/255.0, alpha: 0.9)))
                                        .frame(width: 200, height: 300)
                                        .clipShape(RoundedRectangle(cornerRadius: 10))
                                    
                                }
                            }
                        })
                        .padding(.leading,10)
                        .padding(.trailing,10)
                        
                        
                    }
                }
            }
            .frame(width:UIScreen.main.bounds.width)
            
        }
    }
}


#Preview {
    ScollViewBootcamp()
}

上图看看效果:

ScrollView

简单几句就能实现 ScrollView 的滑动效果;非常方便。

LazyVGrid 网格布局,先上代码:

import SwiftUI

struct GridViewBootcamp: View {
    
    let columns=[        GridItem(.flexible(),spacing: 6   ,alignment: .center),        GridItem(.flexible(),spacing: 6    ,alignment: .center),        GridItem(.flexible(),spacing: 6  ,alignment: .center),    ]
    
    var body: some View {
        
        ScrollView{
            LazyVGrid(columns: columns,
                      alignment: .center,
                      spacing: 6,
                      pinnedViews: [.sectionHeaders],content:
                        {
                Section(content: {}, header: {
                    Text("section header 一")
                        .font(.largeTitle)
                        .foregroundStyle(.blue)
                        .frame(width: UIScreen.main.bounds.width,height: 100,alignment: .leading)
                })
                
                ForEach(0..<41){
                    index in
                    Rectangle()
                        .fill(Color.init(cgColor: CGColor(red: CGFloat.random(in: 0..<255)/255.0, green: CGFloat.random(in: 0..<255)/255.0, blue: CGFloat.random(in: 0...255)/255.0, alpha: 0.9)))
                        .frame(height: 50)
                }
                
                //-------
                Section {
                    
                } header: {
                    Text("section header 二")
                        .font(.largeTitle)
                        .foregroundStyle(.blue)
                        .frame(width: UIScreen.main.bounds.width,alignment: .leading)
                    
                }
                
                ForEach(0..<41){
                    index in
                    Rectangle()
                        .fill(Color.init(cgColor: CGColor(red: CGFloat.random(in: 0..<255)/255.0, green: CGFloat.random(in: 0..<255)/255.0, blue: CGFloat.random(in: 0...255)/255.0, alpha: 0.9)))
                        .frame(height: 50)
                }
                
            })
            .padding(.leading,6)
            .padding(.trailing,6)
            .background(.gray)
        }.background(.blue)
    }
}

#Preview {
    GridViewBootcamp()
}

效果图:

LazyVGrid

总结:LazyVGrid 大家看到这个单词有个 Lazy 懒加载的意思,它的内部加载item简单来说,就是当视图需要时,才会执行item内容渲染功能,展示UI上。也就这点注意。

SafeArea 安全区域:

import SwiftUI

struct SafeAreaBootcamp: View {
    var body: some View {
        GeometryReader{
            src in
            Rectangle()
                .fill(.blue)
                .frame(maxWidth: .infinity,
                       maxHeight: .infinity)
        }
    }
}

#Preview {
    SafeAreaBootcamp()
}

效果图:

safeArea

可以看到上下边距存在安全区域的,如果禁用安全区域,使用 ignoresSafeArea(.all) 可以去掉;这个就太简单了。

代码如下:

safeArea

最后说说SwiftUI函数表达

上上代码:


import SwiftUI

struct ExtractFunctionsBootcamp: View {
    
    @State var bgc:Color = .red
    
    var body: some View {
        normolView
    }
    
    var normolView : some View {
        setUI()// 函数调用
    }
    
    func chageColor() -> Void {
        self.bgc = .red
    }
    
    //函数定义
    func setUI()->some View {
        return ZStack{
            
            bgc
                .ignoresSafeArea(.all)
            
            VStack(spacing: 20, content: {
                
                Text("Hello, World!")
                    .font(.largeTitle)
                
                Button(action: {
                    bgc = .brown
                }, label: {
                    Text("Button")
                        .font(.largeTitle)
                        .foregroundStyle(.white)
                })
                
                Button {
                    self.chageColor()
                } label: {
                    Image(systemName: "button.horizontal.top.press")
                        .resizable()
                        .foregroundColor(.white)
                        .aspectRatio(contentMode: .fill)
                        .frame(width: 50,height: 50)
                }
            })
        }
    }
}


#Preview {
    ExtractFunctionsBootcamp()
}

其实函数表达跟我们swift语法糖一样;func 命名;这点和swift语法糖没什么区别。

总结(说说我的感想)

SwiftUI优点:

简洁性:Swift,SwiftUI语法简洁,编写代码变得更加容易和快速。

安全性:是一种类型安全的编程语言,可以在编译时检测类型错误,这帮助我们避免许多常见的错误,提高代码的质量和可靠性。

互操作性:它与Objective-C语言无缝互衔接,是的OC与swift代码混编变的更加便捷。

SwiftUI缺点

功能限制:虽然SwiftUI提供了许多常见的UI组件,但与UIKit相比,功能仍然相对有限。在某些复杂的界面需求下,可能需要使用UIKit来实现。

错误提示不明确:有时SwiftUI, SwiftUI的错误提示可能不够明确,导致难以定位问题。

UIkit与SwiftUI缺乏无缝兼容:两者兼容性不够理想,这在业务开发中,你可能才能发现。

目前苹果与市面大量应用也在使用Swift,SwiftUI开发应用,这们语言在应用中占有读也是成倍增长。

路漫漫其修远兮,吾将上下而求索

提醒大家在的时候,注意身体劳逸结合;毕竟没有谁真的会在互联网这个行业干到退休,最终都会离开,把位置留给年轻人。

后续更新中..........

2023年度总结:我们都在用力的活着,拼尽了全力,却换回了伤痕累累!!!

2024年1月30日 18:57

阅前必读:

2023 你还记得让你听过最扎心的话吗?你印象里记得你做的哪些不如意痛心的事吗?当你的付出得不到回报的时候。你有过绝望吗?

闭上眼睛,想起过往时候,你流泪了吗?

其实我并不害怕黑夜,我只是怕了孤单。走在那条回忆的路上,想我了血肉模糊的风景。

承受过了背叛。其实并不是放不下。更多的只是不甘心。曾经你度过了多少个黑夜,在心里你该有多少的不如意,没人能懂的你;罢了,一切都让他过去吧。 不要让回忆封锁了你现实的生活。

好吧,到此我是不是说出了你在2023的各种不如意,其实我也很苦,但是生活还要继续。

接下来收起我们的眼泪,毕竟男人的泪没有女孩子眼泪那么值钱,除了没人疼,全身上下都疼,下面一张图叙述2023整年过往。

2023完成目标

其实没有什么金玉良言,真实而平凡的人生,每个人都一样普普通,你遭受的磨难每个人都在经历,想逃也逃不开,想躲无处容身,都是枯燥无味的平淡日子里加上了一星半点的小改变。

技术博客

博客园

image.png

我给自己定的目标到2023年文章数达到100篇,我的任务算是完成了

掘金

同样也是定的100篇文章,那么任务也达到了,其他的博客平台就不在一一说了 image.png

技术博客

这期间我想了很多种方式,我无数次的动念思考自己究竟应该去如何总结自己这一年的故事。起初的我想写这一年的收获,但我发现太虚伪,一个人若只会书写自己“完美”的那一面,那么这个人究竟是得多么的弱小。于是后来我又想写低谷,但我总担心会误导别人甚至也会担心再次把不好的情绪带入其中影响自己··· ··· 后来我意识到,我错了

关键词:稳健成长 自我怀疑 略有遗憾

稳健成长: 在生活上,工作,财富上今年没有明显变化。旅游去了一两个地方;开通了技术博客;培养了学习记录的习惯;更愿意分享和记录生活;

自我怀疑: 改变说明之前的思维和习惯是错或者缺少的,这个纠正和学习的过程是需要付出时间和学习成本的,而且会碰到无法解决的问题,所以是带有痛苦的。旅游高原身体不适,频繁的加班;锻炼的腰酸背痛;分享和记录会有质疑和不同的声音都会自我怀疑。

略有遗憾: 今年立的flag只是完成了一小部分,不经感叹2023年太难了,不知道2024会有多大变化。

工作裁员,失业的低谷

来北京3年多了,在23年年中时候,我失业了。我作为一个基层员工,没有任何办法去对抗当时的现状,毕竟我的能力还没有达到公司重视,所以确实是单方面依附于企业。距离被裁也是很久了,裁员的第一想法是这么多人为什么是我!想想原因:公司经济,分的活,技术能力,工作习惯;痛定思痛去改变呗。牺牲玩乐时间去学习技术,好的工作习惯需要去观察和模仿那些优秀的同事并实践,其实说白了我还是不够努力。

在离职过后长达6个月的时间里,我一直找不到任何一个能够维持当下生计的工作。这半年时间的漫长体验,它其实为我打开了新世界的一扇门,在这之前我从没觉得找工作会是一件很难的事情,事实上招聘软件一打开也确实可以有很多,当初就是本着这样的一份认知和年少有为的执着,让我近半年的时间花光了身上几乎所有的积蓄。 对于当时的我来说,快速成长是找工作这件事情本身唯一的目的,也是这个信念让我一直坚持对抗大环境,直到最后一刻。 在那段时间里,我感受到了极度的挫败和沮丧。疫情居家出不去的日子,如今在北京,我是又重新温习了一遍,但唯一与当初不同的是——封闭我的不是疫情,而是我自己。

精力

精力大不如从前,人要服老。这一年我无数次规划运动、早睡,可是每每都会熬夜,也经常加班,我的精力不足以支撑下班十点钟回家还要运动,因为我还得整理业务资料,看看书,备考,做不完的事,凌晨两点睡,白天两杯咖啡,晚上一瓶红牛已成习惯,想要一天25个小时。

情感

情感线进展0%

态度

找到个聊的来的人不容易,找到个聊的来却又对你有兴趣的更不容易。在各自的行业各自为战,却又背靠背相互支撑。

找一个人,她拥有的特质,大部分是我欣赏的,一小部分是她独有的即可。如果把自己变成异性,我会选择和我结婚吗,答案是不会。自己都接受不了自己,何必要求另一半完美。100%的真诚,80%的坦诚。

打破思维,升级重构

我在狭小的空间里,省吃俭用的过日子。当时找工作的我只有一个念头,我绝不将就也绝不妥协。 心想着,既然有了上一份工作的背书名头,那我就一定要坚持找到真正自己想要去做的事。在那前3个月的时间里,我一直在做自己想做的事,沉浸在自己的世界里快活。 我隔三差五的去参加各种线下的沙龙活动,去认识更多有趣的人,去见识更多有趣的事,晚上出去锻炼身体,白天平时我就每天泡在图书馆里学习如何成为一个优秀的人。你说这孩子当时想的慌不慌唐,他甚至连门槛都没迈的进去,就已经开始幻想怎么在这个领域里怎么变得优秀了··· 年轻的时候多少都爱干点蠢事,当然我也不会例外。那段时间里我一边自学,一边去认识新的人和新的世界。悠哉悠哉的过完了前三个月。 直到后来我发现我投出去的所有简历和自荐信都迟迟收不到回应。我开始觉得这事情似乎进展的不太对劲。 我意识到开发这一行业似乎与那些餐饮和销售,完全是两个不同的世界。尤其是在北京这样一个发达的城市里,又基于当下这样一个动荡的市场,在这样的环境当中似乎经验和学历显得尤为重要。而我这个中年人,还尚没有拿到过优秀的成果,想要破局简直是天方夜谭。 还是太天真了 简历当时全部石沉大海无一回应··· 我的心态也是在那段时间里遭受到了重创,开始自闭。我不再去参加线下活动,也不再去图书馆看书,更制止自己再去得意洋洋的自嗨。之所以停掉了这些几乎与求职无关的习惯是因为我曾在那段时间里,感觉自己每天都在进步,每天过得都很充实,回顾每天的复盘,也都能看到每天自己的变化,但那些所有一切的变化,都毫无差异的停留在了意识层面。而自己所生活的当下,却似乎没有迈出去过一步。 难道是我真的没有地方可以迈出去吗? 其实我觉得并不是 而是介于我当时的处境和思维,亲手禁锢了自己 可当时的我却并没有意识到这件事。 因为接连受到持续不断的打击,所以在那后来的一段时间里,我摒弃了所有日积月累中的那些习惯,我开始昼夜颠倒的认真摆烂。 在起初的时候我还会看一些励志演讲、思维课程来激励自己,但越是往后越是没外界正向的反馈。随着我的意识逐渐管窥,那种难以形容的沮丧使我走向了封闭,通讯录里的家人、朋友,我也逐渐开始远离他们淡出了联系。只因为我不想让别人知道那个当初激情高昂的艺凯,如今却把自己搞成了这副模样··· 思想的巨人,行动的矮子 摆烂了1个多月后我发现不能再继续了,因为快要交不上房租了。 那段时间无休止的冲撞南墙,至今我还记得当初朋友的一句话。

我清晰的记得那段时间里“脱不下孔乙己的长衫“,当时很火。我虽然不是孔乙己,但那段时间确实有脱不下的“长衫”,那是我不愿走回头路或者说是下坡路的执念。 那段时间里,其实我早有意识到这件事,尽管审视过自己,但也还是依然选择固执到底。因为我很清楚,在如今这个社会上,想饿死一个手脚健全的年轻人是不可能的。所以哪怕我把自己所有的积蓄都耗光了,只要我愿意,我就依然有能力从头来过。 那时的我其实就已经陷到了沉没成本的陷阱里,那种无意义的复发简历,其实根本就改变不了我当时做事情的能力。我并没有意识到的是“企业聘请员工是请你过来帮公司解决问题的,而不是过来解决你的就业问题的”

那个逃避现实的我,不愿意再次回到现实,经管他看见了,但就是没有改变现状的动力。其实在当时更加让我受挫的是“我明明已经看见了自己,却依然眼睁睁的目睹自己不断的去沉沦和消耗自己”当时的我特别的恨自己。那种对于掌控人生的挫败感,比迟迟找不到工作这件事情本身更具有杀伤力。 长达6个月时间的挫败,也许是因为没钱了所以才让我幡然醒悟,突然跳出思维,才开始慢慢意识到自己因为执念而走错了路。 在当时,我之所以想走实施工程师的路线,是因为我想具备实施所具备的思维,而我却执着于表面职业的名头,走一条被限制的路线。

有那么一瞬间我突然意识到,作为开发人员它就应该具象到是某个人的某项思维,可以发现并解决某种问题。而既然如此,那么我又为什么偏偏就一定要把这种思维固定到某个职业当中去呢?

难道我在生活当中不可以把自己作为产品,不可以把我所提供的信息作为产品,设计交付出去吗? 伴随着思维的转变,我也就不再执着于这件事 因为我很清楚,当下最要紧的是赶快把自己的温饱问题解决掉。近5个月的消耗让我兜里只剩下2千块钱,几乎算是见底了。 于是我把求职简历当中开发这一栏划掉,更换成了实施工程师

原因是:

在一次回家的路上,我曾经走在马路上感受到过“使命的召唤” 这听起来确实很悬,但确实有那么一瞬。 我去年做了一个线上社群,每周三定期交流讨论,尽管它因为我的状态下滑而开始变得慢慢有了水分,但我必须承认它的存在曾给小伙伴们带来过价值,它算是我当初意识觉醒后培育的第一款产品。我想继续做下去,也发现了光是线上的思想碰撞还远远解决不了实质生活中的问题,所以我当时冒出了一个念头,我想要做线下俱乐部的形式。 所以也就产生了想做“线下活动”的念头 但兼顾想法的同时我也要解决当下的问题,于是后来就选择了这个方向

摆脱低谷,如何自救

当时是8月份,23年已经过去了大半,经管我也一直在显著的得到历练和成长。 但渐渐的我也意识到,在这一年里,因为发生的事情太多,受到的打击太多,我脸上的笑容也开始变得越来越少,当初合作的同事和线上的朋友都说我老是一脸严肃的扳着一张脸。 这我也不想,所以我决定得想办法改改~

总结:

2023年在我身上留下了许多的印记。发生了很多事,也正是因为这些奇奇怪怪的事,从而让我遇见了更多有趣却又不一样的人,尽管其中确实出现了许多的问题和坎坷,但却不能够去定义它们是糟糕又或是美好,它来了就是来了,是你人生中本有的那么一部分该有的体验。而这个世界也不再像我从前认知的那般非黑即白,我们也无从去定义它。他人的定义取决于他们如何去看待这件事情,而事实究竟是什么,其实根本就不重要,也同样和我们自身没有太大的关系。

最为关键是:我们如何去解读、去面对、去看待它们。 我们所定义的那个方向感确实很重要,因为它可以引领我们在迷茫、失望、落魄的时候替我们指引前进的方向,当我们分辨不清什么是对什么是错的时候,那就去听从本心,听从自我所认定的那个方向的指引。

做人:

人生所有的体验其实都是一次美好的遇见,让万事万物穿过你的身体,去感受它,去和它拥抱,去和它交流,去向它学习。那些自责、焦虑、迷茫、骄傲、嫉妒、拖延和懒惰,这些每个人都会有,所以也不必排斥。我也曾看到过那些比我更加优秀的年轻人,也曾一度觉得自己怎么就这么的平庸,但那又怎样?这才是我们最真实的模样。 而当初我们所羡慕的那些别人的美好,在他们的身后,其实也都是一个个活生生的普通人,他们也是人不是“神”,我们所感觉到的那些耀眼,只是他们喜欢去书写或呈现自己的一面华丽而已。 他们同样也得拉屎放屁。

所以如果你也曾因为别人的优秀而感到过自卑了,那么完全没有这个必要,摆烂躺平的日子,谁都有过。但如果别人的优秀会变成一束光,会引领你前行的话,那么其实也就无所谓真假了。 与其盲目的去崇拜别人,倒不如接受这个世界最纯粹的模样 人的成长就是祛魅并不断稳定自己内核的一个过程。 当我们重新认识了这个世界之后,你会发现,你已不再是你。你可以轻易坦然的去接受那个曾经的自己,不论他曾经究竟是个什么模样。 你会回过头来高兴的向他打招呼说“感谢你,让我遇见了更好的自己。”

做事:

面对挫折,其实很多时候我们所欠缺的就是那个敢于从头再来的勇气。 哪怕它错了,错了又能怎样呢?

至少我曾证明了它是错的。我不会再后悔,因为我体验过了,因为我感受过了,因为我认真的曾去做过了。 没有失败,没有低谷,没有坎坷,又怎么能迎来我们进一步的成长。 我下半年特别喜欢的两个字叫“痛苦”。 因为只有当一个人他足够深刻的曾感受到过痛苦,他才会去反思,去反省,这究竟是为什么,又究竟是哪里出现了问题。

如果在一个选择路口,大家都想让你往A走,但你就想往B走,而这时你心里总有那么一个声音,就是告诉你“我想要别的”,那个内心的声音就是你本来的样子,本来的特质,它是你人生的底色。 即使你因为恐惧或者想要追求稳定去做了那个大家都希望的A,但实际你内心是不会快乐的。也许在很多个不经意的瞬间,你回想起内心那股灼烧的火焰,燃烧着令你疼痛、内心觉得拧巴,就是你做了一个为别人做的选择而付出的一些代价。

我们都需要经历一些不适,才能够去突破认知,打破原有的思维,刺激自己做出真正的改变。不适感是一个信号,一个常常很有用的信号。人在春风得意时觉得自己什么都很好,甚至有时候会把外部大环境的“好”误认为是自身的能力。 反而越是我们在低谷期,越是我们人生最精进、最懂得反思、最重要的光景。 当你渐渐把“为什么这种事发生在我身上”的思想,转换成“遇到这种事到底想教会我什么”、“凡事发生必有利于我”,你会发现所处的一切都在逐渐的改变。

结语:

每个人的一生,都有属于自己独特的瓶颈,能不能爬出来,不取决于谁拉了你一把。 真正的上帝和贵人,其实一直就住在你心里。它就是你,那个当下存在,每时每刻都陪在你身边的自己。 当你真正觉察意识到他的时候,你可以告诉他“我想要什么,我们去做吧”,而不是跑到佛祖面前祈求上帝的怜悯。其实那个真正的佛祖一直都在你心里,你不需要去向任何人求取任何东西,你本就很好,你本就可以,只是那个当下的你是否愿意。 仅此而已~ 允许不好的事情发生,让万物穿过自己; 接受一切发生的勇气,坦然的面对那个真实的自己; 静下心来,脚踏实地,给自己一点时间,让他慢慢变得强大。

最后的最后:立个flag吧

先给明年的自己定下几个目标吧😤

  1. 坚持不定时运动
  2. 把PMP/项目集成管理/P2/ITIL知识点学好
  3. 每天抽出十分钟背英语单词
  4. 一次旅行✈
  5. 每月1-2篇博客
  6. 找到一个稳定合适的工作

2024 我的本命年:放下你的执念,前行!祝你越来越好🎉

MySql数据库开发中常见问题解决方法

2023年8月5日 14:42

Mysql报错:[ERROR] mysqld File ‘.mysql-bin.010228‘ not found (Errcode 2 “No such file or directory“)

Mysql报错:[ERROR] mysqld File ‘.mysql-bin.010228‘ not found (Errcode 2 “No such file or directory“)

背景:
迁移数据库,源数据库没有启用binlog,将源数据库关闭后整库拷贝到目标数据库并开启binlog

启动mysql报错,查看日志 tail -f -n 200 error.log

有报错:

2022-05-23 19:00:53 0 [ERROR] mysqld: File ‘./mysql-bin.010228’ not found (Errcode: 2 “No such file or directory”)

2022-05-23 19:00:53 0 [ERROR] Failed to open log (file ‘./mysql-bin.010228’, errno 2)

2022-05-23 19:00:53 0 [ERROR] Could not open log file

解决:
将原来发mysql-bin.index文件删除或者将mysql-bin.index移到/tmp目录下。

启动mysql即成功。

当update select from的时候。当子查询返回的数据较多 外层又是张大表的时候 就会产生O(n)的情况。

比如 update table set col = 1 where aid in (select id from a1);

1,UPDATE,test_question_option,,index,,PRIMARY,4,,1398083,100,Using where
2,DEPENDENT SUBQUERY,test_question,,unique_subquery,”PRIMARY,set_id”,PRIMARY,4,func,1,5,Using where

改写成-> update join的方式去解决。

1,SIMPLE,test_question,,ref,”PRIMARY,set_id”,set_id,5,const,190,100,Using index
1,UPDATE,t1,,ref,”PRIMARY,question_id_idx”,question_id_idx,4,func,3,90,Using where

MySQL 单机 部署 多实例 操作手册

1 单机部署多实例概述


MySQL多实例就是在一台机器上开启多个不同的服务端口(如:3306,3307),运行多个MySQL服务进程,通过不同的socket监听不同的服务端口来提供各自的服务,每个实例有自己的配置文件,数据文件,进程及日志文件。

为什么要做多实例:

  1. 资源隔离,减少相互影响
  2. 分担连接数(否则连接数上升性能会下降)
  3. 更充分的利用资源(不同的业务错高峰混跑),从而节约服务器资源

优点:

  1. 有效的利用服务器资源
  2. 当单个服务器资源有剩余时,可以利用多实例来充分利用服务器的资源来提供更多的服务

缺点:
会出现资源互相抢占的现象。当某个实例的并发量很高,或者有慢查询时,会消耗服务器更多的资源,这时就会影响其他实例提供的服务,访问质量下降。

部署mysql多实例的两种方式

  1. 使用多个配置文件启动不同的进程来实现多实例,这种方式的优势逻辑简单,配置简单,缺点是管理起来不太方便。 这种方式就是多次部署多套单实例,只是目录结构和端口不一样。这个和单实例操作一样,这里不再单独演示。
  2. 通过官方自带的mysqld_multi使用单独的配置文件来实现多实例,这种方式定制每个实例的配置不太方面,优点是管理起来很方便,集中管理; 我们这里演示的是这种方法。

2 部署示例


2.1 安装MySQL 软件

MySQL 软件的安装参考之前的博客:

Redhat 7.7 平台 MySQL 5.7.33 安装手册(Tar包)
www.cndba.cn/dave/articl…

2.2 创建多实例

单机环境部署多实例需要注意以下问题:

  1. 配置文件安装路径不能相同
  2. 数据库目录不能相同
  3. 启动脚本不能同名
  4. 端口不能相同
  5. socket文件的生成路径不能相同

2.2.1 创建目录结构

[root@dm8 data]# mkdir -p /data/mysql/{3306,3307,3308}/data
[root@dm8 mysql]# mkdir -p /data/mysql/{3306,3307,3308}/log
[root@dm8 mysql]# touch /data/mysql/3306/log/error.log
[root@dm8 mysql]# touch /data/mysql/3307/log/error.log
[root@dm8 mysql]# touch /data/mysql/3308/log/error.log
[root@dm8 mysql]# chown -R mysql:mysql /data/mysql

2.2.2 修改配置文件

在/etc/my.cnf 文件中添加如下内容:

[root@dm8 3306]# cat /etc/my.cnf
[client]
default-character-set=utf8mb4

[mysqld]
user=mysql
basedir=/usr/local/mysql
lower_case_table_names=1

[mysqld_multi]
mysqld=/usr/local/mysql/bin/mysqld_safe
mysqladmin=/usr/local/mysql/bin/mysqladmin
log=/data/mysql/mysqld_multi.log

# 3306 数据库实例
[mysqld3306]
port=3306
server_id=1
mysqld=mysqld
mysqladmin=mysqladmin
datadir=/data/mysql/3306/data
socket=/tmp/mysql_3306.sock
log-error=/data/mysql/3306/log/error.log
pid-file=/data/mysql/3306/mysql3306.pid

## skip-grant-tables #用于跳过密码登录
character_set_server=utf8mb4
init_connect='SET NAMES utf8mb4'
lower_case_table_names=1
explicit_defaults_for_timestamp=true


# 3307 数据库实例
[mysqld3307]
port=3307
server_id=2
mysqld=mysqld
mysqladmin=mysqladmin
datadir=/data/mysql/3307/data
socket=/tmp/mysql_3307.sock
log-error=/data/mysql/3307/log/error.log
pid-file=/data/mysql/3307/mysql3307.pid

## 默认最大连接数设置
# max_connections=300
character_set_server=utf8mb4
init_connect='SET NAMES utf8mb4'
lower_case_table_names=1
explicit_defaults_for_timestamp=true

# 3308 数据库实例
[mysqld3308]
port=3308
server_id=3
mysqld=mysqld
mysqladmin=mysqladmin
datadir=/data/mysql/3308/data
socket=/tmp/mysql_3308.sock
log-error=/data/mysql/3308/log/error.log
pid-file=/data/mysql/3308/mysql3308.pid

character_set_server=utf8mb4
init_connect='SET NAMES utf8mb4'
lower_case_table_names=1
explicit_defaults_for_timestamp=true

[root@dm8 3306]#

2.2.3 初始化实例

[dave@www.cndba.cn ~]$ mysqld --defaults-file=/etc/my.cnf --datadir=/data/mysql/3306/data/ --initialize
2022-05-28T02:40:22.905532Z 1 [Note] A temporary password is generated for root@localhost: AKL24)n+%qfC

[dave@www.cndba.cn ~]$ mysqld --defaults-file=/etc/my.cnf --datadir=/data/mysql/3307/data/ --initialize
2022-05-28T02:40:33.266853Z 1 [Note] A temporary password is generated for root@localhost: RQbAkX9p*/rH

[dave@www.cndba.cn ~]$ mysqld --defaults-file=/etc/my.cnf --datadir=/data/mysql/3308/data/ --initialize
2022-05-28T02:40:43.185864Z 1 [Note] A temporary password is generated for root@localhost: +tYq1prSkQqv
[dave@www.cndba.cn ~]$

2.2.4 启动实例

启动mysql

[dave@www.cndba.cn ~]$ mysqld_multi --defaults-file=/etc/my.cnf start 3306
[dave@www.cndba.cn ~]$ mysqld_multi --defaults-file=/etc/my.cnf start 3307
[dave@www.cndba.cn ~]$ mysqld_multi --defaults-file=/etc/my.cnf start 3308

查看mysql服务

[dave@www.cndba.cn ~]$ mysqld_multi --defaults-file=/etc/my.cnf report
Reporting MySQL servers
MySQL server from group: mysqld3306 is running
MySQL server from group: mysqld3307 is running
MySQL server from group: mysqld3308 is running
[dave@www.cndba.cn ~]$

2.2.5 修改root用户密码并配置远程登录

Root 用户的密码在刚才初始化的时候有提示。 

设置本地访问密码

[dave@www.cndba.cn ~]$ mysql -uroot -p -S /tmp/mysql_3306.sock
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or /g.
Your MySQL connection id is 3
Server version: 5.7.33

Copyright (c) 2000, 2021, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '/h' for help. Type '/c' to clear the current input statement.

mysql> ALTER USER 'root'@'localhost' IDENTIFIED BY 'root';
Query OK, 0 rows affected (0.00 sec)

mysql> use mysql;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> update user set host='%' where user='root';
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> flush privileges;
Query OK, 0 rows affected (0.01 sec)

授权root用户可以远程登陆

GRANT ALL ON *.* TO 'root'@'%';
ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'root';
flush privileges;

mysql> GRANT ALL ON *.* TO 'root'@'%';
Query OK, 0 rows affected (0.00 sec)

mysql> ALTER USER 'root'@'%' IDENTIFIED WITH mysql_native_password BY 'root';
Query OK, 0 rows affected (0.00 sec)

mysql> flush privileges;
Query OK, 0 rows affected (0.00 sec)

2.3 多实例启停管理

直接使用mysqld_multi进行多实例管理:

[dave@www.cndba.cn ~]$ mysqld_multi --help
mysqld_multi version 2.16 by Jani Tolonen
……
Usage: mysqld_multi [OPTIONS] {start|reload|stop|report} [GNR,GNR,GNR...]
or     mysqld_multi [OPTIONS] {start|reload|stop|report} [GNR-GNR,GNR,GNR-GNR,...]
……

启动/停止全部实例:

/usr/local/mysql/bin/mysqld_multi start
/usr/local/mysql/bin/mysqld_multi stop

/usr/local/mysql/bin/mysqld_multi report

启动/停止单个实例:

/usr/local/mysql/bin/mysqld_multi start 3306 
/usr/local/mysql/bin/mysqld_multi stop 3306 
/usr/local/mysql/bin/mysqld_multi report 3306

2.4 mysqld_multi stop 不生效的解决方法

实际测试stop 并没有停掉:

[dave@www.cndba.cn ~]$ mysqld_multi stop 3306
[dave@www.cndba.cn ~]$ mysqld_multi report
Reporting MySQL servers
MySQL server from group: mysqld3306 is running
MySQL server from group: mysqld3307 is running
MySQL server from group: mysqld3308 is running
[dave@www.cndba.cn ~]$
[dave@www.cndba.cn ~]$ mysqld_multi --defaults-file=/etc/my.cnf stop 3306
[dave@www.cndba.cn ~]$ mysqld_multi report
Reporting MySQL servers
MySQL server from group: mysqld3306 is running
MySQL server from group: mysqld3307 is running
MySQL server from group: mysqld3308 is running
[dave@www.cndba.cn ~]$

在数据库级别停则没有问题:

[dave@www.cndba.cn ~]$ mysql -uroot -proot -S /tmp/mysql_3306.sock
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor.  Commands end with ; or /g.
Your MySQL connection id is 14
Server version: 5.7.33 MySQL Community Server (GPL)

Copyright (c) 2000, 2021, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '/h' for help. Type '/c' to clear the current input statement.

mysql> shutdown;
Query OK, 0 rows affected (0.00 sec)

mysql>

[dave@www.cndba.cn ~]$ mysqld_multi report
Reporting MySQL servers
MySQL server from group: mysqld3306 is not running
MySQL server from group: mysqld3307 is running
MySQL server from group: mysqld3308 is running
[dave@www.cndba.cn ~]$

查看了mysqld_multi stop后的实例日志,提示有认证信息:

2022-05-28T03:10:32.873545Z 13 [Note] Access denied for user 'root'@'localhost' (using password: NO)
2022-05-28T03:13:43.752827Z 14 [Note] Access denied for user 'root'@'localhost' (using password: NO)

也就是说mysqld_multi stop 命令必须凭借有效的 user & password 配置去管理相应的实例。 

因此可以创建一个独立的用户来关闭数据库,或者使用我们之前的root用户:

grant shutdown on  .  to ‘username’@’localhost’ identified by ‘password’

[dave@www.cndba.cn log]$ cat /etc/my.cnf
[client]
default-character-set=utf8mb4

[mysqld]
user=mysql
basedir=/usr/local/mysql
lower_case_table_names=1

[mysqld_multi]
user=root
pass=root
mysqld=/usr/local/mysql/bin/mysqld_safe
mysqladmin=/usr/local/mysql/bin/mysqladmin
log=/data/mysql/mysqld_multi.log

注意这里用的是pass=root。 如果写成password 一样是无效的,重新测试可以正常管理:

[dave@www.cndba.cn ~]$ mysqld_multi stop 3307
[dave@www.cndba.cn ~]$ mysqld_multi report
Reporting MySQL servers
MySQL server from group: mysqld3306 is not running
MySQL server from group: mysqld3307 is not running
MySQL server from group: mysqld3308 is running
[dave@www.cndba.cn ~]$


[dave@www.cndba.cn ~]$ mysqld_multi stop
[dave@www.cndba.cn ~]$ mysqld_multi report
Reporting MySQL servers
MySQL server from group: mysqld3306 is not running
MySQL server from group: mysqld3307 is not running
MySQL server from group: mysqld3308 is not running
[dave@www.cndba.cn ~]$

[dave@www.cndba.cn ~]$ mysqld_multi start
[dave@www.cndba.cn ~]$ mysqld_multi report
Reporting MySQL servers
MySQL server from group: mysqld3306 is running
MySQL server from group: mysqld3307 is running
MySQL server from group: mysqld3308 is running
[dave@www.cndba.cn ~]$

[root@dm8 3306]# netstat -lntup |grep mysqld
tcp6       0      0 :::3306                 :::*                    LISTEN      19009/mysqld
tcp6       0      0 :::3307                 :::*                    LISTEN      19092/mysqld
tcp6       0      0 :::3308                 :::*                    LISTEN      19170/mysqld
[root@dm8 3306]#

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"];
        }
    }
}

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

深入了解swift函数派发机制

2023年7月17日 19:38

函数派发

Swift中函数的派发机制有三种:静态派发,函数表派发,消息派发。

静态派发

静态派发是指在运行时不需要查表,直接跳转到方法进行执行。静态派发的性能也是最高的。c语言采用的是直接派发。

函数表派发

class类型采用函数表派发。当一个对象调用一个函数时,会从对象的头8字节找到该对象的元信息。从元信息的函数表中找到执行的函数地址,并执行函数。

对象的元信息会在编译时写入macho文件中。

class Animal {
    func speak() {
        print("Animal speak")
    }
    func eat() {
        print("Animal eat")
    }
    func sleep() {
        print("Animal sleep")
    }
}
class Dog: Animal {
    override func speak() {
        print("Dog speak")
    }
    override func eat() {
        print("Dog eat")
    }
    func run() {
        print("Dog run")
    }
}class Animal {
    func speak() {
        print("Animal speak")
    }
    func eat() {
        print("Animal eat")
    }
    func sleep() {
        print("Animal sleep")
    }
}
class Dog: Animal {
    override func speak() {
        print("Dog speak")
    }
    override func eat() {
        print("Dog eat")
    }
    func run() {
        print("Dog run")
    }
}

Dog继承于Animal。let dog: Dog = Dog()dog 变量指向Dog对象。Dog对象在堆内存中。Dog对象的前8字节存有Dog对象的元信息。Dog对象的元信息中有该类的函数指针表。Dog中override父类Animal的函数会替换掉父类的函数存于Dogd的函数指针中。

swift### 消息派发

继承于NSObject的采用的是消息派发。Swift可以通过dynamic修饰来支持消息派发机制。

函数派发场景分析

在选择派发方式时,在编译期间就能决定执行哪个函数就采用静态派发。需要在运行期间决定执行哪个函数的就需要用函数表派发或者消息派发。不具动态性的的场景默认采用静态派发,这样派发效率更高。

struct无法继承,也就不具有动态性,函数派发在编译期间就能确定。

class和协议的 extension无法被子类继承,函数派发在编译期间就能确定。

class和协议的初始化方法,因为绝大多数时候需要被override的,所以采用函数表派发。

class的其他方法,如果没有被override,一般是函数表派发,但编译器也可能优化成直接派发。

class的 @objcextension能够继承,函数派发在执行期决定,并且是采用的是消息派发。

不继承NSObject的纯Swift,@objc的extension,采用的消息派发有点迷。消息派发机制需要有Objective-C的运行时,不继承与NSObject能有运行时的信息吗。该类的对象,

class FunctionDispatchObject {
     func test1() {
        print("test1")
    }
}
extension FunctionDispatchObject {
    @objc public func test2() {
        print("test2")
    }
}
// test1采用函数表派发
// test2采用消息机制派发:test2虽然写在extension里,当家里@objc,具有了动态性,可以继承了。class FunctionDispatchObject {
     func test1() {
        print("test1")
    }
}
extension FunctionDispatchObject {
    @objc public func test2() {
        print("test2")
    }
}
// test1采用函数表派发
// test2采用消息机制派发:test2虽然写在extension里,当家里@objc,具有了动态性,可以继承了。

iOS APP启动全流程

2023年7月10日 17:13

1.在用户点击屏幕上的icon时,iOS系统用户体验层进程SpringBoad调用fork创建进程(复制进程),并执行execl函数将新的可执行代码载入内存,执行loadMachine去加载主Mach-o,进行__TEXT,__DATA的映射,加载UUID,创建主线程,代码签名验证,加载动态链器。

2.动态连接器加载完成后,动态链接器先加载主程序,再加载所依赖的动态库,这个加载的过程。

3.主程序进行初始化时,是递归进行的,依赖关系呈现的是一个类似树的有向图。递归过程是树的深度遍历。在向下递归时,遇到image没有依赖了时,也就是叶子节点,树形结构的最左边的叶子节点就是libsystem,会通执行_dyld_objc_notify_register里注册的load_image方法,进而进行category的加载,+load的方法的执行,然后初始化C&C++的静态化变量,然后调用 constructor 函数。而不是像市面是上的千篇一律的+load方法执行后,在初始化C&C++静态变量,然后调用constructor函数。一定要注意是递归。递归到最上层了,就是主程序了,也就是主程序的+load方法,在初始化C&C++静态变量,然后调用constructor函数。

iOS

真心送你一份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

带你从新手快速搭建springboot项目

2023年6月18日 22:49

一、创建项目

1.1、创建项目

1.2、配置编码

1.3、取消无用提示

1.4、取消无用参数提示

二、添加POM父依赖

<!-- 两种方式添加父依赖或者import方式 -->
<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>2.5.7</version>
</parent>

maven的pom文件手动更新

添加完成maven的pom文件之后,会自动更新,也可能不会自动更新,那么我们需要手动更新它。

三、支持SpringMVC

<dependencies>
    <!-- 支持SpringMVC -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

四、创建启动类、rest接口

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class StartApplication {

    public static void main(String[] args) {
        SpringApplication.run(StartApplication.class, args);
    }

}

五、配置插件

配置完成后,maven打包可以生成可执行jar文件

<build>
  <plugins>
      <!-- 打包成可执行jar -->
      <plugin>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
      <!-- 配置跳过测试 -->
      <plugin>
          <artifactId>maven-surefire-plugin</artifactId>
          <configuration>
              <skip>true</skip>
          </configuration>
      </plugin>
      <!-- 配置jdk版本11 -->
      <plugin>
          <groupId>org.apache.maven.plugins</groupId>
          <artifactId>maven-compiler-plugin</artifactId>
          <configuration>
              <source>11</source>
              <target>11</target>
              <encoding>utf-8</encoding>
          </configuration>
      </plugin>
  </plugins>

</build>

六、添加thymeleaf模板

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

七、添加配置

在resources文件夹下,创建application.properties

在resources文件夹下,创建templates文件夹

# 应用名称
spring.application.name=thymeleaf
# 应用服务 WEB 访问端口
server.port=8080
# THYMELEAF (ThymeleafAutoConfiguration)
# 开启模板缓存(默认值: true )
spring.thymeleaf.cache=false
# 检查模板是否存在,然后再呈现
spring.thymeleaf.check-template=true
# 检查模板位置是否正确(默认值 :true )
spring.thymeleaf.check-template-location=true
#Content-Type 的值(默认值: text/html )
spring.thymeleaf.content-type=text/html
# 开启 MVC Thymeleaf 视图解析(默认值: true )
spring.thymeleaf.enabled=true
# 模板编码
spring.thymeleaf.encoding=UTF-8
# 要被排除在解析之外的视图名称列表,⽤逗号分隔
spring.thymeleaf.excluded-view-names=
# 要运⽤于模板之上的模板模式。另⻅ StandardTemplate-ModeHandlers( 默认值: HTML5)
spring.thymeleaf.mode=HTML5
# 在构建 URL 时添加到视图名称前的前缀(默认值: classpath:/templates/ )
spring.thymeleaf.prefix=classpath:/templates/
# 在构建 URL 时添加到视图名称后的后缀(默认值: .html )
spring.thymeleaf.suffix=.html

八、添加controller

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class IndexController {

    @RequestMapping("/index")
    public ModelAndView index(){
        ModelAndView mv = new ModelAndView();

        mv.setViewName("index");

        return mv;
    }

}
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.ModelAndView;

@RestController
public class HelloController {

    @GetMapping("/Hello")
    public String Hello(){
        return "haha";
    }
}
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * rest测试controller
 */
@RestController
public class RestIndexController {
    
    @GetMapping("/restIndex")
    public String index(){
        return "rest";
    }
    
}

九、添加html

在templates下创建index.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" >
<head>
    <meta charset="UTF-8">
    <title>index</title>
</head>
<body>
index
</body>
</html>

十、访问

需要maven执行编译,否则容易404

http://localhost:8080/index

基于Servlet+JSP自实现的MVC模式,学完它你才真正上掌握Servlet与JSP原理

2023年6月12日 13:49

通过结合Servlet和JSP的MVC模式,我们可以发挥二者各自的优点:

  • Servlet实现业务逻辑;
  • JSP实现展示逻辑。

但是,直接把MVC搭在Servlet和JSP之上还是不太好,原因如下:

  • Servlet提供的接口仍然偏底层,需要实现Servlet调用相关接口;
  • JSP对页面开发不友好,更好的替代品是模板引擎;
  • 业务逻辑最好由纯粹的Java类实现,而不是强迫继承自Servlet。

能不能通过普通的Java类实现MVC的Controller?类似下面的代码:

public class UserController {
    @GetMapping("/signin")
    public ModelAndView signin() {
        ...
    }

    @PostMapping("/signin")
    public ModelAndView doSignin(SignInBean bean) {
        ...
    }

    @GetMapping("/signout")
    public ModelAndView signout(HttpSession session) {
        ...
    }
}

上面的这个Java类每个方法都对应一个GET或POST请求,方法返回值是ModelAndView,它包含一个View的路径以及一个Model,这样,再由MVC框架处理后返回给浏览器。

如果是GET请求,我们希望MVC框架能直接把URL参数按方法参数对应起来然后传入:

@GetMapping("/hello")
public ModelAndView hello(String name) {
    ...
}

如果是POST请求,我们希望MVC框架能直接把Post参数变成一个JavaBean后通过方法参数传入:

@PostMapping("/signin")
public ModelAndView doSignin(SignInBean bean) {
    ...
}

为了增加灵活性,如果Controller的方法在处理请求时需要访问HttpServletRequestHttpServletResponseHttpSession这些实例时,只要方法参数有定义,就可以自动传入:

@GetMapping("/signout")
public ModelAndView signout(HttpSession session) {
    ...
}

以上就是我们在设计MVC框架时,上层代码所需要的一切信息。

设计MVC框架

如何设计一个MVC框架?在上文中,我们已经定义了上层代码编写Controller的一切接口信息,并且并不要求实现特定接口,只需返回ModelAndView对象,该对象包含一个View和一个Model。实际上View就是模板的路径,而Model可以用一个Map<String, Object>表示,因此,ModelAndView定义非常简单:

public class ModelAndView {
    Map<String, Object> model;
    String view;
}

比较复杂的是我们需要在MVC框架中创建一个接收所有请求的Servlet,通常我们把它命名为DispatcherServlet,它总是映射到/,然后,根据不同的Controller的方法定义的@Get@Post的Path决定调用哪个方法,最后,获得方法返回的ModelAndView后,渲染模板,写入HttpServletResponse,即完成了整个MVC的处理。

这个MVC的架构如下:

MVC

其中,DispatcherServlet以及如何渲染均由MVC框架实现,在MVC框架之上只需要编写每一个Controller。

我们来看看如何编写最复杂的DispatcherServlet。首先,我们需要存储请求路径到某个具体方法的映射:

@WebServlet(urlPatterns = "/")
public class DispatcherServlet extends HttpServlet {
    private Map<String, GetDispatcher> getMappings = new HashMap<>();
    private Map<String, PostDispatcher> postMappings = new HashMap<>();
}

处理一个GET请求是通过GetDispatcher对象完成的,它需要如下信息:

class GetDispatcher {
    Object instance; // Controller实例
    Method method; // Controller方法
    String[] parameterNames; // 方法参数名称
    Class<?>[] parameterClasses; // 方法参数类型
}

有了以上信息,就可以定义invoke()来处理真正的请求:

class GetDispatcher {
    ...
    public ModelAndView invoke(HttpServletRequest request, HttpServletResponse response) {
        Object[] arguments = new Object[parameterClasses.length];
        for (int i = 0; i < parameterClasses.length; i++) {
            String parameterName = parameterNames[i];
            Class<?> parameterClass = parameterClasses[i];
            if (parameterClass == HttpServletRequest.class) {
                arguments[i] = request;
            } else if (parameterClass == HttpServletResponse.class) {
                arguments[i] = response;
            } else if (parameterClass == HttpSession.class) {
                arguments[i] = request.getSession();
            } else if (parameterClass == int.class) {
                arguments[i] = Integer.valueOf(getOrDefault(request, parameterName, "0"));
            } else if (parameterClass == long.class) {
                arguments[i] = Long.valueOf(getOrDefault(request, parameterName, "0"));
            } else if (parameterClass == boolean.class) {
                arguments[i] = Boolean.valueOf(getOrDefault(request, parameterName, "false"));
            } else if (parameterClass == String.class) {
                arguments[i] = getOrDefault(request, parameterName, "");
            } else {
                throw new RuntimeException("Missing handler for type: " + parameterClass);
            }
        }
        return (ModelAndView) this.method.invoke(this.instance, arguments);
    }

    private String getOrDefault(HttpServletRequest request, String name, String defaultValue) {
        String s = request.getParameter(name);
        return s == null ? defaultValue : s;
    }
}

上述代码比较繁琐,但逻辑非常简单,即通过构造某个方法需要的所有参数列表,使用反射调用该方法后返回结果。

类似的,PostDispatcher需要如下信息:

class PostDispatcher {
    Object instance; // Controller实例
    Method method; // Controller方法
    Class<?>[] parameterClasses; // 方法参数类型
    ObjectMapper objectMapper; // JSON映射
}

和GET请求不同,POST请求严格地来说不能有URL参数,所有数据都应当从Post Body中读取。这里我们为了简化处理,只支持JSON格式的POST请求,这样,把Post数据转化为JavaBean就非常容易。

class PostDispatcher {
    ...
    public ModelAndView invoke(HttpServletRequest request, HttpServletResponse response) {
        Object[] arguments = new Object[parameterClasses.length];
        for (int i = 0; i < parameterClasses.length; i++) {
            Class<?> parameterClass = parameterClasses[i];
            if (parameterClass == HttpServletRequest.class) {
                arguments[i] = request;
            } else if (parameterClass == HttpServletResponse.class) {
                arguments[i] = response;
            } else if (parameterClass == HttpSession.class) {
                arguments[i] = request.getSession();
            } else {
                // 读取JSON并解析为JavaBean:
                BufferedReader reader = request.getReader();
                arguments[i] = this.objectMapper.readValue(reader, parameterClass);
            }
        }
        return (ModelAndView) this.method.invoke(instance, arguments);
    }
}

最后,我们来实现整个DispatcherServlet的处理流程,以doGet()为例:

public class DispatcherServlet extends HttpServlet {
    ...
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html");
        resp.setCharacterEncoding("UTF-8");
        String path = req.getRequestURI().substring(req.getContextPath().length());
        // 根据路径查找GetDispatcher:
        GetDispatcher dispatcher = this.getMappings.get(path);
        if (dispatcher == null) {
            // 未找到返回404:
            resp.sendError(404);
            return;
        }
        // 调用Controller方法获得返回值:
        ModelAndView mv = dispatcher.invoke(req, resp);
        // 允许返回null:
        if (mv == null) {
            return;
        }
        // 允许返回`redirect:`开头的view表示重定向:
        if (mv.view.startsWith("redirect:")) {
            resp.sendRedirect(mv.view.substring(9));
            return;
        }
        // 将模板引擎渲染的内容写入响应:
        PrintWriter pw = resp.getWriter();
        this.viewEngine.render(mv, pw);
        pw.flush();
    }
}

这里有几个小改进:

  • 允许Controller方法返回null,表示内部已自行处理完毕;
  • 允许Controller方法返回以redirect:开头的view名称,表示一个重定向。

这样使得上层代码编写更灵活。例如,一个显示用户资料的请求可以这样写:

@GetMapping("/user/profile")
public ModelAndView profile(HttpServletResponse response, HttpSession session) {
    User user = (User) session.getAttribute("user");
    if (user == null) {
        // 未登录,跳转到登录页:
        return new ModelAndView("redirect:/signin");
    }
    if (!user.isManager()) {
        // 权限不够,返回403:
        response.sendError(403);
        return null;
    }
    return new ModelAndView("/profile.html", Map.of("user", user));
}

最后一步是在DispatcherServletinit()方法中初始化所有Get和Post的映射,以及用于渲染的模板引擎:

public class DispatcherServlet extends HttpServlet {
    private Map<String, GetDispatcher> getMappings = new HashMap<>();
    private Map<String, PostDispatcher> postMappings = new HashMap<>();
    private ViewEngine viewEngine;

    @Override
    public void init() throws ServletException {
        this.getMappings = scanGetInControllers();
        this.postMappings = scanPostInControllers();
        this.viewEngine = new ViewEngine(getServletContext());
    }
    ...
}

如何扫描所有Controller以获取所有标记有@GetMapping@PostMapping的方法?当然是使用反射了。虽然代码比较繁琐,但我们相信各位童鞋可以轻松实现。

这样,整个MVC框架就搭建完毕。

实现渲染

有的童鞋对如何使用模板引擎进行渲染有疑问,即如何实现上述的ViewEngine?其实ViewEngine非常简单,只需要实现一个简单的render()方法:

public class ViewEngine {
    public void render(ModelAndView mv, Writer writer) throws IOException {
        String view = mv.view;
        Map<String, Object> model = mv.model;
        // 根据view找到模板文件:
        Template template = getTemplateByPath(view);
        // 渲染并写入Writer:
        template.write(writer, model);
    }
}

Java有很多开源的模板引擎,常用的有:

他们的用法都大同小异。这里我们推荐一个使用Jinja语法的模板引擎Pebble,它的特点是语法简单,支持模板继承,编写出来的模板类似:

<html>
<body>
  <ul>
  {% for user in users %}
    <li><a href="{{ user.url }}">{{ user.username }}</a></li>
  {% endfor %}
  </ul>
</body>
</html>

即变量用{{ xxx }}表示,控制语句用{% xxx %}表示。

使用Pebble渲染只需要如下几行代码:

public class ViewEngine {
    private final PebbleEngine engine;

    public ViewEngine(ServletContext servletContext) {
        // 定义一个ServletLoader用于加载模板:
        ServletLoader loader = new ServletLoader(servletContext);
        // 模板编码:
        loader.setCharset("UTF-8");
        // 模板前缀,这里默认模板必须放在`/WEB-INF/templates`目录:
        loader.setPrefix("/WEB-INF/templates");
        // 模板后缀:
        loader.setSuffix("");
        // 创建Pebble实例:
        this.engine = new PebbleEngine.Builder()
            .autoEscaping(true) // 默认打开HTML字符转义,防止XSS攻击
            .cacheActive(false) // 禁用缓存使得每次修改模板可以立刻看到效果
            .loader(loader).build();
    }

    public void render(ModelAndView mv, Writer writer) throws IOException {
        // 查找模板:
        PebbleTemplate template = this.engine.getTemplate(mv.view);
        // 渲染:
        template.evaluate(writer, mv.model);
    }
}

最后我们来看看整个工程的结构:

MVC

其中,framework包是MVC的框架,完全可以单独编译后作为一个Maven依赖引入,controller包才是我们需要编写的业务逻辑。

我们还硬性规定模板必须放在webapp/WEB-INF/templates目录下,静态文件必须放在webapp/static目录下,因此,为了便于开发,我们还顺带实现一个FileServlet来处理静态文件:

@WebServlet(urlPatterns = { "/favicon.ico", "/static/*" })
public class FileServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 读取当前请求路径:
        ServletContext ctx = req.getServletContext();
        // RequestURI包含ContextPath,需要去掉:
        String urlPath = req.getRequestURI().substring(ctx.getContextPath().length());
        // 获取真实文件路径:
        String filepath = ctx.getRealPath(urlPath);
        if (filepath == null) {
            // 无法获取到路径:
            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        Path path = Paths.get(filepath);
        if (!path.toFile().isFile()) {
            // 文件不存在:
            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        // 根据文件名猜测Content-Type:
        String mime = Files.probeContentType(path);
        if (mime == null) {
            mime = "application/octet-stream";
        }
        resp.setContentType(mime);
        // 读取文件并写入Response:
        OutputStream output = resp.getOutputStream();
        try (InputStream input = new BufferedInputStream(new FileInputStream(filepath))) {
            input.transferTo(output);
        }
        output.flush();
    }
}

运行代码,在浏览器中输入URLhttp://localhost:8080/hello?name=Bob可以看到如下页面:

MVC

为了把方法参数的名称编译到class文件中,以便处理@GetMapping时使用,我们需要打开编译器的一个参数,在Eclipse中勾选Preferences-Java-Compiler-Store information about method parameters (usable via reflection);在Idea中选择Preferences-Build, Execution, Deployment-Compiler-Java Compiler-Additional command line parameters,填入-parameters;在Maven的pom.xml添加一段配置如下:

<project ...>
    <modelVersion>4.0.0</modelVersion>
    ...
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <compilerArgs>
                        <arg>-parameters</arg>
                    </compilerArgs>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

有些用过Spring MVC的童鞋会发现,本节实现的这个MVC框架,上层代码使用的公共类如GetMappingPostMappingModelAndView都和Spring MVC非常类似。实际上,我们这个MVC框架主要参考就是Spring MVC,通过实现一个“简化版”MVC,可以掌握Java Web MVC开发的核心思想与原理,对将来直接使用Spring MVC是非常有帮助的。

总结

一个MVC框架是基于Servlet基础抽象出更高级的接口,使得上层基于MVC框架的开发可以不涉及Servlet相关的HttpServletRequest等接口,处理多个请求更加灵活,并且可以使用任意模板引擎,不必使用JSP。

青山不改,绿水常流!谢谢大家支持!!!

iOS:零碎整理iOS音视频开发API

2023年5月28日 19:05

在ios开发过程中,音频经常会用到,而音频根据使用场合分为音效和音乐,音效一般只播放1~2秒

  1. ios音效支持的格式

ios 支持的音频格式有:aac、alac、he-aac、iLBc、IMA4、Linea PCM、MP3、CAF,其中,aac、alac、he-aac、mp3、caf支持硬件解码,其他只支持软件解码, 软件界面因为比较耗电,所以,我们在开发过程中,经常采用的是caf、mp3

  1. 音频库

AVFoundation.framework

  1. 代码
    // 打开资源
    NSURL* url =[[NSBundle mainBundle]URLForResource:@"m_03" withExtension:@"wav"];
    SystemSoundID soundID;
    AudioServicesCreateSystemSoundID((__bridge CFURLRef)(url), &soundID);
    // 播放音效
    AudioServicesPlaySystemSound(self.soundID);
    // 删除音效
    AudioServicesDisposeSystemSoundID(self.soundID);
  1. 框架

  1. 加载音乐资源并播放

复制代码

    AVAudioPlayer* player = musicDict[fileName];
    if (!player) {
        NSURL* url = [[NSBundle mainBundle] URLForResource:fileName withExtension:nil];
        NSCAssert(url != nil, @"fileName not found musics");
        
        NSError* error;
        player = [[AVAudioPlayer alloc]initWithContentsOfURL:url error:&error];
        if (error) {
            NSLog(@"load music error");
            return;
        }
        [musicDict setObject:player forKey:fileName];
    }
    if (player.isPlaying == NO) {
        [player play];
    }

复制代码

6.暂停 停止操作

[player pause];// 暂停
[player stop];// 停止
[player isplaying];// 是否在播放

好了,现在能播放音乐了,但我们在看其他的应用的时候,一般当应用切换到后台的时候也能播放音乐,那这个又是如何实现的呢?这个只要设置音频的后台播放,具体为:

1> 在后台开启一个任务

- (void)applicationDidEnterBackground:(UIApplication *)application
{
    // 开启后台任务,让音乐继续播放
    [application beginBackgroundTaskWithExpirationHandler:nil];
}

2> 设置项目配置文件

3> 设置音频链接会话,这个主要告诉设备如何处理音频事件的

1234 // 设置音频会话类型``   ``AVAudioSession* session = [AVAudioSession sharedInstance];``   ``[session setCategory:AVAudioSessionCategorySoloAmbient error:``nil``];``   ``[session setActive:``YES error:``nil``];

这里有很多会话类型,如果想详细了解,可参考:http://blog.csdn.net/daiyelang/article/details/16986059

现在应该可以播放音乐了。

iOSUIKit动画从入门到放弃,简单易懂学习轻松容易掌握

2023年5月21日 18:24

动画 - UIKit

动画原理

  • 视觉残留效应
  • 运动模糊

做动画的时候要达到 60FPS 时候,画面才能流畅,不然用户会感觉界面卡顿。

UIView 提供的动画支持

UIView 动画本质上对 Core Animation 的封装,提供一个简洁好用的动画接口,在要求不复杂的情况下,完全可以实现很多动画。
UIView 动画可以设置的动画属性有:

  • frame / bounds 大小变化
  • center 中心位置
  • transform 旋转平移等
  • alpha 透明度
  • backgroundColor 背景颜色
  • contentStretch 拉伸内容
  • Autolayout环境下的动画要直接修改constraint,注意 setNeedsUpdateConstraints,layoutIfNeeded的用法

UIView 类方法动画

  • 动画的开始和结束方法

  • UIView Block动画

  • Spring 动画

  • Keyframes 动画

  • 转场动画

    • 单个视图的过渡
    • 从旧视图到新视图的过渡
1. 动画的开始和结束方法

基本语句:

动画开始结束标记
UIView.beginAnimations(String?, context: UnsafeMutablePointer<Void>)
第一个参数是动画标识,第二个参数是附加参数,在设置了代理的情况下,此参数将发送setAnimationWillStartSelector 和 setAnimationDidStopSelector 所指定的方法,一般设为 nil。

UIView.commitAnimations() 动画结束

动画参数设置方法

  • UIView.setAnimationDelay(NSTimeInterval) 设置动画的延时

  • UIView.setAnimationDuration(NSTimeInterval) 设置动画持续时间

  • UIView.setAnimationDelegate(AnyObject?) 设置动画代理

  • UIView.setAnimationWillStartSelector(Selector) 设置动画即将开始时代理执行的SEL

  • UIView.setAnimationDidStopSelector(Selector) 设置动画结束时代理对象执行的SEL

  • UIView.setAnimationRepeatCount(Float) 设置动画的重复次数

  • UIView.setAnimationCurve(UIViewAnimationCurve) 设置动画的曲线

  • UIView.setAnimationRepeatAutoreverses(Bool) 设置动画是否继续执行相反的动画

  • UIView.setAnimationsEnabled(Bool) 是否禁用动画效果(对象属性依然会被改变,只是没有动画效果)

  • UIView.setAnimationBeginsFromCurrentState(Bool) 设置是否从当前状态开始播放动画

    • 假设上一个动画正在播放,且尚未播放完毕,我们将要进行一个新的动画:
    • 当为true时:动画将从上一个动画所在的状态开始播放
    • 当为false时:动画将从上一个动画所指定的最终状态开始播放(此时上一个动画马上结束)

以下是简单的例子:

animation01

2. UIView Block 动画

1)最简单的 Block 动画 包含时间和动画

UIView.animateWithDuration(NSTimeInterval) { // 动画持续时间 
            <#code#>//执行动画
        }

2)带有动画完成回调的 Block 动画

UIView.animateWithDuration(NSTimeInterval, animations: { 
            <#code#>//执行动画
            }) { (Bool) in
                <#code#>// 动画完毕后执行的操作
        }

3)可以设置延迟和过渡效果 Block 动画

 UIView.animateWithDuration(NSTimeInterval, delay: NSTimeInterval, options: UIViewAnimationOptions, animations: { 
            <#code#>
            }) { (<#Bool#>) in
                <#code#>
        }

注意,此处的 UIViewAnimationOptions 可以组合使用,在 swift 中写法 options: [UIViewAnimationOptions.CurveEaseInOut, UIViewAnimationOptions.Repeat]
具体的枚举值,看官方文档即可。

4)Spring 动画
iOS7 后新增 Spring 动画,iOS 系统动画大部分采用 Spring Animation。

UIView.animateWithDuration(NSTimeInterval, delay: NSTimeInterval, usingSpringWithDamping: CGFloat, initialSpringVelocity: CGFloat, options: UIViewAnimationOptions, animations: {
            <#code#>
            }) { (<#Bool#>) in
                <#code#>
        }
  • Duration: 动画持续时间
  • delay: 动画执行延时
  • usingSpringWithDamping: 震动效果,范围 0~1,数值越小,震动效果越明显
  • initialSpringVelocity: 初始速度
  • options: 动画的过渡效果

5)Keyframes 关键帧动画

UIView.animateKeyframesWithDuration(NSTimeInterval, delay: NSTimeInterval, options: UIViewKeyframeAnimationOptions, animations: { 
            <#code#>
            }) { (<#Bool#>) in
                <#code#>
        }

增加关键帧的方法

UIView.addKeyframeWithRelativeStartTime(Double, relativeDuration: Double, animations: { 
                <#code#>
            })

注意开始时间和持续时间均是占总时间的比例

UIViewKeyframeAnimationOptions 的枚举值如下,可以组合使用

UIViewAnimationOptionLayoutSubviews           //进行动画时布局子控件
UIViewAnimationOptionAllowUserInteraction     //进行动画时允许用户交互
UIViewAnimationOptionBeginFromCurrentState    //从当前状态开始动画
UIViewAnimationOptionRepeat                   //无限重复执行动画
UIViewAnimationOptionAutoreverse              //执行动画回路
UIViewAnimationOptionOverrideInheritedDuration //忽略嵌套动画的执行时间设置
UIViewAnimationOptionOverrideInheritedOptions //不继承父动画设置

UIViewKeyframeAnimationOptionCalculationModeLinear     //运算模式 :连续
UIViewKeyframeAnimationOptionCalculationModeDiscrete   //运算模式 :离散
UIViewKeyframeAnimationOptionCalculationModePaced      //运算模式 :均匀执行
UIViewKeyframeAnimationOptionCalculationModeCubic      //运算模式 :平滑
UIViewKeyframeAnimationOptionCalculationModeCubicPaced //运算模式 :平滑均匀

关键帧动画:

private func blockAni5() {
        UIView.animateKeyframesWithDuration(5, delay: 0.0, options: UIViewKeyframeAnimationOptions.CalculationModeLinear, animations: {
            UIView.addKeyframeWithRelativeStartTime(0, relativeDuration: 1.0/4, animations: {
                self.greenView.backgroundColor = UIColor.redColor()
            })
            
            UIView.addKeyframeWithRelativeStartTime(1.0/4, relativeDuration: 1.0/4, animations: { 
                self.greenView.backgroundColor = UIColor.blackColor()
                self.greenView.frame.size = CGSize(width: 50, height: 50)
            })
            
            UIView.addKeyframeWithRelativeStartTime(2.0/4, relativeDuration: 1.0/4, animations: { 
                self.greenView.backgroundColor = UIColor.yellowColor()
            })
            UIView.addKeyframeWithRelativeStartTime(3.0/4, relativeDuration: 1.0/4, animations: { 
                self.greenView.backgroundColor = UIColor.blueColor()
                self.greenView.frame.size = CGSize(width: 250, height: 250)
            })
            
            
            
            }) { (_) in
                print("动画完成blockAni5()")
        }
    }

简单的例子:

imageng

3. UIView 转场动画

在进行示例之前,大家需要注意一点过渡转变动画与动画属性动画的不同之处。我们在创建动画属性动画时只需要在animations闭包中添加对视图动画属性修改的代码即可,它没有作用域或作用视图的概念。而在过渡转变动画中有作用视图的概念,也就是说我们调用过渡转变动画方法时需要指定一个作用视图

过渡转变动画中的作用视图并不是我们的目标视图,而是目标视图的容器视图,那么大家不难想象,如果该容器视图中有多个子视图,那么这些子视图都会有过渡转变动画效果。

1)从旧视图到新视图的转场

 UIView.transitionFromView(UIView, toView: UIView, duration: NSTimeInterval, options: UIViewAnimationOptions) { (<#Bool#>) in
            <#code#>
        }

在该动画过程中,fromView 会从父视图中移除,并将 toView 添加到父视图中。转场动画的作用对象是父视图,过渡效果体现在父视图上

2)单个试图的过渡

UIView.transitionWithView(UIView, duration: NSTimeInterval, options: UIViewAnimationOptions, animations: { 
            <#code#>
            }) { (<#Bool#>) in
                <#code#>
        }

简单的例子

animation

核心动画 CoreAnimations

Core Animation(核心动画)是一组强大的动画 API,是直接操作 CALayer 层来产生动画,相比上述的 UIView 动画,可以实现更复杂的动画效果。

事务管理 CATransaction

CALayer 的可用于动画的属性成为 Animatable properties,苹果官方有详细的列表,显示了所有了可以动画的属性 CALayer Animatable Properties。如果一个Layer对象对应着 View,则称这个 Layer 是一个 Root Layer, 非 Root Layer 一般是通过 CALayer 或者其子类直接创建的。
所有的非 Root Layer 在设置 Amimation Properties 的时候都存在隐式动画,默认的 duration 是0.25秒

事务(transaction)实际上是Core Animation用来包含一系列属性动画集合的机制,用指定事务去改变可以做动画的图层属性,不会立刻发生变化,而是提交事务时用一个动画过渡到新值。任何 Layer 的可动画属性的设置都属于某个 CATransaction,事务的作用是为了保证多个属性的变化同时进行。事务可以嵌套,当事务嵌套时候,只有最外层的事务 commit 之后,整个动画才开始。

CATransaction没有任何实例方法,只有类型方法。CATransaction.begin()CATransaction.commit()构成了一个动画块:

CATransaction.begin()
/* animation block */
CATransaction.commit()

其他的方法

func animationDuration() -> CFTimeInterval  // get duration, defaults to 0.25s
func setAnimationDuration(dur: CFTimeInterval)  // set duration
func animationTimingFunction() -> CAMediaTimingFunction?  // get timing function
func setAnimationTimingFunction(function: CAMediaTimingFunction?)  // set timing function
func disableActions() -> Bool  // get disable actions state
func setDisableActions(flag: Bool)  // set disable actions state
func completionBlock() -> (() -> Void)?  // get completion block
func setCompletionBlock(block: (() -> Void)?)  // set completion block

以上四组的方法可以用以下两个方法代替

func valueForKey(key: String) -> AnyObject?
func setValue(anObject: AnyObject?, forKey key: String)

CATransaction 动画块只能处理CALayer相关动画,无法正确处理UIView的动画,甚至UIView的 Root layer(与UIView相关联的CALayer)也不行。
UIView 的 Root layer动画为什么会在CATransaction动画块中失效?
隐式动画的查找过程如下:

imagng

禁止隐式动画:

imagpng

我们把改变属性时CALayer自动应用的动画称作行为,当CALayer的属性被修改时候,它会调用-actionForKey:方法,传递属性的名称。剩下的操作都在CALayer的头文件中有详细的说明,实质上是如下几步:

    • 图层首先检测它是否有委托,并且是否实现CALayerDelegate协议指定的-actionForLayer:forKey方法。如果有,直接调用并返回结果。
  • 如果没有委托,或者委托没有实现-actionForLayer:forKey方法,图层接着检查包含属性名称对应行为映射的actions字典。

如果actions字典没有包含对应的属性,那么图层接着在它的style字典接着搜索属性名。

  • 最后,如果在style里面也找不到对应的行为,那么图层将会直接调用定义了每个属性的标准行为的-defaultActionForKey:方法。

所以一轮完整的搜索结束之后,-actionForKey:要么返回空(这种情况下将不会有动画发生),要么是CAAction协议对应的对象,最后CALayer拿这个结果去对先前和当前的值做动画。

于是这就解释了 UIKit 是如何禁用隐式动画的:每个 UIView 对它关联的图层都扮演了一个委托,并且提供了-actionForLayer:forKey 的实现方法。当不在一个动画块的实现中,UIView 对所有图层行为返回 nil,但是在动画 block 范围之内,它就返回了一个非空值。

  • UIView关联的图层禁用了隐式动画,对这种图层做动画的唯一办法就是使用UIView的动画函数(而不是依赖CATransaction),或者继承UIView,并覆盖-actionForLayer:forKey:方法,或者直接创建一个显式动画。
  • 对于单独存在的图层,我们可以通过实现图层的-actionForLayer:forKey:委托方法,或者提供一个actions字典来控制隐式动画。

参考资料 iOS Actions

时间系统

参考谈谈 iOS Animation

CAMediaTiming 协议定义了在一段动画内用来控制逝去时间的属性的集合。CALayer 通过CAMediaTiming协议实现了一个有层级关系的时间系统。

几个重要属性(都是CALayer的属性):

  • beginTime 是相对于父级对象的开始时间
  • timeOffset是active local time的偏移量
  • speed 设置当前对象的时间流逝相对于父级对象时间流的流逝速度
  • fillMode 决定了当前对象过了非 active 时间段的行为

显示动画

当需要对非 Root Layer 进行动画或者需要对动画做更多的自定义的行为的时候,需要使用显示动画,基类为 CAAnimation 

imageng

核心动画类中可以直接使用的类有:

  • CABasicAnimation
  • CAKeyframeAnimation
  • CATransition
  • CAAnimationGroup
  • CASpringAnimation

CABasicAnimation有三个比较重要的属性,fromValue,toValue,byValue,这三个属性都是可选的,但不能同时多于两个为非空.最终都是为了确定animation变化的起点和终点.中间的值都是通过插值方式计算出来的.插值计算的结果由timingFunction指定,默认timingFunction为nil,会使用liner的,也就是变化是均匀的.

1. 核心动画类的核心方法

  • 初始化CAAnimation对象

    • 一般使用animation方法生成实例 let animation = CABasicAnimation()
    • 如果是CAPropertyAnimation的子类,可以使用'let animation = CABasicAnimation(keyPath: String?)'来生成
  • 设置动画的相关属性

    • 执行时间
    • 执行曲线
    • keyPath 的目标值
    • 代理等
animation.duration = 2.0
//        animation.fromValue = UIColor.blackColor()
        animation.toValue = NSValue(CGPoint: CGPointMake(300, 300))
        animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
        animation.removedOnCompletion = false
        animation.fillMode = kCAFillModeForwards
  • 动画的添加和移除

    • 调用 CALayer 的 view2.layer.addAnimation(animation, forKey: "color")
    • 停止动画 view2.layer.removeAnimationForKey(String)view2.layer.removeAllAnimations()

防止动画结束后回到初始状态

只需设置removedOnCompletion、fillMode两个属性就可以了。

transformAnima.removedOnCompletion = NO;
transformAnima.fillMode = kCAFillModeForwards;

解释:为什么动画结束后返回原状态?

给一个视图添加layer动画时,真正移动并不是我们的视图本身,而是 presentation layer 的一个缓存。动画开始时 presentation layer开始移动,原始layer隐藏,动画结束时,presentation layer从屏幕上移除,原始layer显示。这就解释了为什么我们的视图在动画结束后又回到了原来的状态,因为它根本就没动过。
这个同样也可以解释为什么在动画移动过程中,我们为何不能对其进行任何操作。
所以在我们完成layer动画之后,最好将我们的layer属性设置为我们最终状态的属性,然后将presentation layer 移除掉。

2. 核心动画类的常用属性

  • KeyPath:可以指定 KeyPath 为 CALayer 的属性值,并对它修改,注意部分属性是不支持动画的
  • duration:动画的持续时间
  • repeatCount: 动画的重复次数
  • timingFunction:动画的时间节奏控制
  • fillMode:视图在非Active时的行为
  • removedOnCompletion:动画执行完毕后是否从图层上移除,默认为YES(视图会恢复到动画前的状态),可设置为NO(图层保持动画执行后的状态,前提是fillMode设置为kCAFillModeForwards)
  • beginTime:动画延迟执行时间(通过CACurrentMediaTime() + your time 设置)
  • delegate:代理
func animationDidStart(anim: CAAnimation)
 func animationDidStop(anim: CAAnimation, finished flag: Bool)

Timing Function对应的类是CAMediaTimingFunction,它提供了两种获得时间函数的方式,一种是使用预定义的五种时间函数,一种是通过给点两个控制点得到一个时间函数. 相关的方法为

CAMediaTimingFunction(name: String)
CAMediaTimingFunction(controlPoints: Float, c1y: Float, c2x: Float, c2y: Float)

五种预定义的时间函数名字的常量变量分别为

  • kCAMediaTimingFunctionLinear,
  • kCAMediaTimingFunctionEaseIn,
  • kCAMediaTimingFunctionEaseOut,
  • kCAMediaTimingFunctionEaseInEaseOut,
  • kCAMediaTimingFunctionDefault

自定义的 Timing Function 的函数图像就是一条三次的贝塞尔曲线。

CAKeyframeAnimation动画

两个决定动画关键帧的属性:

  • values: 关键帧数组对象,里面每一个元素就是一个关键帧,动画会在相应时间段内,依次执行数组中每一个关键帧动画

  • path: 动画路径对象,可以指定一个路径,在执行动画时会沿着路径移动,path只能对CALayer的 anchorPoint 和 position 属性起作用

  • keyTimes: 设置关键帧对应的时间点。范围0 ~ 1,默认每一帧时间平分,keyTimes数组中的每个元素定义了相应的keyframe的持续时间值作为动画的总持续时间的一小部分,每个元素的值必须大于、或等于前一个值。keyframeAni.keyTimes = [0.1,0.5,0.7,0.8,1]

  • calculationMode 计算模式,其主要针对的是每一帧的内容为一个座标点的情况,也就是对anchorPoint 和 position 进行的动画,表示插值计算的模式

    • kCAAnimationLinear 默认值 直线相连来差值
    • kCAAnimationDiscrete 离散的,不进行插值计算,所有关键帧逐个显示
    • kCAAnimationPaced 动画均匀的,此时keytimes和timeFunctions无效
    • kCAAnimationCubic 对关键帧为坐标点的关键帧进行圆滑曲线相连后插值计算,对于曲线的形状还可以通过tensionValues,continuityValues,biasValues来进行调整自定义
    • kCAAnimationCubicPaced 在kCAAnimationCubic的基础上使得动画运行变得均匀,就是系统时间内运动的距离相同,此时keyTimes以及timingFunctions也是无效的.

简单例子

imagepng

CATransition

转场动画,比 UIView 的转场动画具有更多的动画效果。

CATransition的属性:

  • type: 过渡动画的类型

    • kCATransitionFade 渐变
    • kCATransitionMoveIn 覆盖
    • kCATransitionPush 推出
    • kCATransitionReveal 揭开

    私有动画类型的值有:"cube"、"suckEffect"、"oglFlip"、 "rippleEffect"、"pageCurl"、"pageUnCurl"等等

  • subtype: 过渡动画的方向

    • kCATransitionFromRight 从右边
    • kCATransitionFromLeft 从左边
    • kCATransitionFromTop 从顶部
    • kCATransitionFromBottom 从底部

imagepng

CASpringAnimation

CASpringAnimation是iOS9新加入动画类型,是CABasicAnimation的子类,用于实现弹簧动画。

CASpringAnimation的重要属性:

  • mass:质量(影响弹簧的惯性,质量越大,弹簧惯性越大,运动的幅度越大)
  • stiffness:弹性系数(弹性系数越大,弹簧的运动越快)
  • damping:阻尼系数(阻尼系数越大,弹簧的停止越快)
  • initialVelocity:初始速率(弹簧动画的初始速度大小,弹簧运动的初始方向与初始速率的正负一致,若初始速率为0,表示忽略该属性)
  • settlingDuration:结算时间(根据动画参数估算弹簧开始运动到停止的时间,动画设置的时间最好根据此时间来设置)
   private func springAni() {
        
        let ani = CASpringAnimation(keyPath: "bounds")
        ani.mass = 10.0 //质量,影响图层运动时的弹簧惯性,质量越大,弹簧拉伸和压缩的幅度越大
        ani.stiffness = 5000 //刚度系数(劲度系数/弹性系数),刚度系数越大,形变产生的力就越大,运动越快
        ani.damping = 100.0//阻尼系数,阻止弹簧伸缩的系数,阻尼系数越大,停止越快
        ani.initialVelocity = 5.0//初始速率,动画视图的初始速度大小;速率为正数时,速度方向与运动方向一致,速率为负数时,速度方向与运动方向相反
        ani.duration = ani.settlingDuration
        ani.toValue = NSValue(CGRect: view4.bounds)
        ani.removedOnCompletion = false
        ani.fillMode = kCAFillModeForwards
        ani.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
        view2.layer.addAnimation(ani, forKey: "boundsAni")
        
    }

imagepngCAAnimationGroup

使用Group可以将多个动画合并一起加入到层中,Group中所有动画并发执行,可以方便地实现需要多种类型动画的场景,group动画以数组表示。

imagepng

private func groupAni() {
    
    let posAni = CABasicAnimation(keyPath: "position")
    posAni.toValue = NSValue(CGPoint: CGPoint(x: 310, y: 400))
    let boundAni = CABasicAnimation(keyPath: "bounds")
    boundAni.toValue = NSValue(CGRect: CGRectMake(0, 0, 200, 200))
    let colorAni = CABasicAnimation(keyPath: "backgroundColor")
    colorAni.toValue = UIColor.redColor().CGColor
        
    let groupAni = CAAnimationGroup()
    groupAni.animations = [posAni, boundAni, colorAni]
    groupAni.duration = 1.5
    groupAni.fillMode = kCAFillModeForwards
    groupAni.removedOnCompletion = false
    groupAni.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
    view1.layer.addAnimation(groupAni, forKey: "groupAni")
        
}

文中demo的地址:Github 动画demo

整理了一篇非常全的iOS面试题,值得你收藏,为您的面试助力

2023年5月15日 16:06

目录

1. 一、基础知识点
2. 二、第三方框架
3. 三、算法
4. 四、编码格式(优化细节)
5. 五、其他知识点

础知识点

设式是什么? 你知道哪些设计模式,并简要叙述?

设计模式是一种编码经验,就是用比较成熟的逻辑去处理某一种类型的事情。
1). MVC模式:Model View Control,把模型 视图 控制器 层进行解耦合编写。
2). MVVM模式:Model View ViewModel 把模型 视图 业务逻辑 层进行解耦和编写。
3). 单例模式:通过static关键词,声明全局变量。在整个进程运行期间只会被赋值一次。
4). 观察者模式:KVO是典型的观察者模式,观察某个属性的状态,状态发生变化时通知观察者。
5). 委托模式:代理+协议的组合。实现1对1的反向传值操作。
6). 工厂模式:通过一个类方法,批量的根据已有模板生产对象。

MVC 和 MVVM 的区别

MVVM是对胖模型进行的拆分,其本质是给控制器减负,将一些弱业务逻辑放到VM中去处理。
MVC是一切设计的基础,所有新的设计模式都是基于MVC进行的改进。
参考:iOS MVVM架构总结

#import跟 #include 有什么区别,@class呢,#import<> 跟 #import””有什么区别? 1). #import是Objective-C导入头文件的关键字,#include是C/C++导入头文件的关键字,使用#import头文件会自动只导入一次,不会重复导入。
2). @class告诉编译器某个类的声明,当执行时,才去查看类的实现文件,可以解决头文件的相互包含。
3). #import<>用来包含系统的头文件,#import””用来包含用户头文件。

frame 和 bounds 有什么不同?

frame指的是:该view在父view坐标系统中的位置和大小。(参照点是父view的坐标系统)
bounds指的是:该view在本身坐标系统中的位置和大小。(参照点是本身坐标系统)

Objective-C的类可以多重继承么?可以实现多个接口么?Category是什么?重写一个类的方法用继

承好还是分类好?为什么?

答:Objective-C的类不可以多重继承;可以实现多个接口(协议);Category是类别;一般情况用分类好,用Category去重写类的方法,仅对本Category有效,不会影响到其他类与原有类的关系。

@property 的本质是什么?ivar、getter、setter 是如何生成并添加到这个类中的

@property 的本质是什么?

@property = ivar + getter + setter;
“属性” (property)有两大概念:ivar(实例变量)、getter+setter(存取方法)
“属性” (property)作为 Objective-C 的一项特性,主要的作用就在于封装对象中的数据。 Objective-C 对象通常会把其所需要的数据保存为各种实例变量。实例变量一般通过“存取方法”(access method)来访问。其中,“获取方法” (getter)用于读取变量值,而“设置方法” (setter)用于写入变量值。

@property中有哪些属性关键字?/ @property 后面可以有哪些修饰符?

属性可以拥有的特质分为四类:
1.原子性--- nonatomic 特质
2.读/写权限---readwrite(读写)、readonly (只读)
3.内存管理语义---assign、strong、 weak、unsafe_unretained、copy
4.方法名---getter= 、setter=
5.不常用的:nonnull,null_resettable,nullable

属性关键字 readwrite,readonly,assign,retain,copy,nonatomic 各是什么作用,在那种情况下用?

1). readwrite 是可读可写特性。需要生成getter方法和setter方法。
2). readonly 是只读特性。只会生成getter方法,不会生成setter方法,不希望属性在类外改变。
3). assign 是赋值特性。setter方法将传入参数赋值给实例变量;仅设置变量时,assign用于基本数据类型。
4). retain(MRC)/strong(ARC) 表示持有特性。setter方法将传入参数先保留,再赋值,传入参数的retaincount会+1。
5). copy 表示拷贝特性。setter方法将传入对象复制一份,需要完全一份新的变量时。
6). nonatomic 非原子操作。不写的话默认就是atomic。atomic 和 nonatomic 的区别在于,系统自动生成的 getter/setter 方法不一样。对于atomic的属性,系统生成的 getter/setter 会保证 get、set 操作的完整性,而nonatomic就没有这个保证了。所以,nonatomic的速度要比atomic快。
不过atomic可并不能保证线程安全。
参考:[爆栈热门 iOS 问题] atomic 和 nonatomic 有什么区别?

什么情况使用 weak 关键字,相比 assign 有什么不同?

1.在 ARC 中,在有可能出现循环引用的时候,往往要通过让其中一端使用 weak 来解决,比如: delegate 代理属性。
2.自身已经对它进行一次强引用,没有必要再强引用一次,此时也会使用 weak,自定义 IBOutlet 控件属性一般也使用 weak;当然,也可以使用strong。

IBOutlet连出来的视图属性为什么可以被设置成weak?

因为父控件的subViews数组已经对它有一个强引用。
 
不同点:
assign 可以用非 OC 对象,而 weak 必须用于 OC 对象。
weak 表明该属性定义了一种“非拥有关系”。在属性所指的对象销毁时,属性值会自动清空(nil)。

怎么用 copy 关键字?

用途:

  1. NSString、NSArray、NSDictionary 等等经常使用copy关键字,是因为他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary;
  2. block 也经常使用 copy 关键字。
     
    说明:
    block 使用 copy 是从 MRC 遗留下来的“传统”,在 MRC 中,方法内部的 block 是在栈区的,使用 copy 可以把它放到堆区.在 ARC 中写不写都行:对于 block 使用 copy 还是 strong 效果是一样的,但写上 copy 也无伤大雅,还能时刻提醒我们:编译器自动对 block 进行了 copy 操作。如果不写 copy ,该类的调用者有可能会忘记或者根本不知道“编译器会自动对 block 进行了 copy 操作”,他们有可能会在调用之前自行拷贝属性值。这种操作多余而低效。

用@property声明的 NSString / NSArray / NSDictionary 经常使用 copy 关键字,为什么?如果改用strong关键字,可能造成什么问题?

答:用 @property 声明 NSString、NSArray、NSDictionary 经常使用 copy 关键字,是因为他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary,他们之间可能进行赋值操作(就是把可变的赋值给不可变的),为确保对象中的字符串值不会无意间变动,应该在设置新属性值时拷贝一份。

  1. 因为父类指针可以指向子类对象,使用 copy 的目的是为了让本对象的属性不受外界影响,使用 copy 无论给我传入是一个可变对象还是不可对象,我本身持有的就是一个不可变的副本。
  2. 如果我们使用是 strong ,那么这个属性就有可能指向一个可变对象,如果这个可变对象在外部被修改了,那么会影响该属性。
     
    总结:使用copy的目的是,防止把可变类型的对象赋值给不可变类型的对象时,可变类型对象的值发送变化会无意间篡改不可变类型对象原来的值。

浅拷贝和深拷贝的区别?

浅拷贝:只复制指向对象的指针,而不复制引用对象本身。
深拷贝:复制引用对象本身。内存中存在了两份独立对象本身,当修改A时,A_copy不变。

系统对象的 copy 与 mutableCopy 方法

不管是集合类对象(NSArray、NSDictionary、NSSet ... 之类的对象),还是非集合类对象(NSString, NSNumber ... 之类的对象),接收到copy和mutableCopy消息时,都遵循以下准则:

  1. copy 返回的是不可变对象(immutableObject);如果用copy返回值调用mutable对象的方法就会crash。
  2. mutableCopy 返回的是可变对象(mutableObject)。
1. 一、非集合类对象的copy与mutableCopy
2.       在非集合类对象中,对不可变对象进行copy操作,是指针复制,mutableCopy操作是内容复制;
3.       对可变对象进行copy和mutableCopy都是内容复制。用代码简单表示如下: NSString *str = @"hello word!"; NSString *strCopy = [str copy] // 指针复制,strCopy与str的地址一样 NSMutableString *strMCopy = [str mutableCopy] // 内容复制,strMCopy与str的地址不一样   NSMutableString *mutableStr = [NSMutableString stringWithString: @"hello word!"]; NSString *strCopy = [mutableStr copy] // 内容复制 NSMutableString *strMCopy = [mutableStr mutableCopy] // 内容复制
1. 二、集合类对象的copy与mutableCopy (同上)
2.       在集合类对象中,对不可变对象进行copy操作,是指针复制,mutableCopy操作是内容复制;
3.       对可变对象进行copy和mutableCopy都是内容复制。但是:集合对象的内容复制仅限于对象本身,对集合内的对象元素仍然是指针复制。(即单层内容复制) NSArray *arr = @[@[@"a", @"b"], @[@"c", @"d"]; NSArray *copyArr = [arr copy]; // 指针复制 NSMutableArray *mCopyArr = [arr mutableCopy]; //单层内容复制 NSMutableArray *array = [NSMutableArray arrayWithObjects:[NSMutableString stringWithString:@"a"],@"b",@"c",nil]; NSArray *copyArr = [mutableArr copy]; // 单层内容复制 NSMutableArray *mCopyArr = [mutableArr mutableCopy]; // 单层内容复制

【总结一句话】:
只有对不可变对象进行copy操作是指针复制(浅复制),其它情况都是内容复制(深复制)!

这个写法会出什么问题:@property (nonatomic, copy) NSMutableArray *arr;

问题:添加,删除,修改数组内的元素的时候,程序会因为找不到对应的方法而崩溃。 //如:-[__NSArrayI removeObjectAtIndex:]: unrecognized selector sent to instance 0x7fcd1bc30460
// copy后返回的是不可变对象(即 arr 是 NSArray 类型,NSArray 类型对象不能调用 NSMutableArray 类型对象的方法)
原因:是因为 copy 就是复制一个不可变 NSArray 的对象,不能对 NSArray 对象进行添加/修改。

如何让自己的类用 copy 修饰符?如何重写带 copy 关键字的 setter?

若想令自己所写的对象具有拷贝功能,则需实现 NSCopying 协议。如果自定义的对象分为可变版本与不可变版本,那么就要同时实现 NSCopying 与 NSMutableCopying 协议。
具体步骤:

  1. 需声明该类遵从 NSCopying 协议
  2. 实现 NSCopying 协议的方法。
    // 该协议只有一个方法:
  • (id)copyWithZone:(NSZone *)zone;
    // 注意:使用 copy 修饰符,调用的是copy方法,其实真正需要实现的是 “copyWithZone” 方法。

写一个 setter 方法用于完成 @property (nonatomic, retain) NSString *name,写一个 setter 方法用于完成 @property (nonatomic, copy) NSString *name

1. // retain - (void)setName:(NSString *)str {
2.       [str retain];
3.       [_name release];
4.       _name = str;
5.     } // copy - (void)setName:(NSString *)str {
6.       id t = [str copy];
7.       [_name release];
8.       _name = t;
9.     }

@synthesize 和 @dynamic 分别有什么作用?

@property有两个对应的词,一个是@synthesize(合成实例变量),一个是@dynamic。
如果@synthesize和@dynamic都没有写,那么默认的就是 @synthesize var = _var;
// 在类的实现代码里通过 @synthesize 语法可以来指定实例变量的名字。(@synthesize var = _newVar;)

  1. @synthesize 的语义是如果你没有手动实现setter方法和getter方法,那么编译器会自动为你加上这两个方法。
  2. @dynamic 告诉编译器,属性的setter与getter方法由用户自己实现,不自动生成(如,@dynamic var)。

常见的 Objective-C 的数据类型有那些,和C的基本数据类型有什么区别?如:NSInteger和int

Objective-C的数据类型有NSString,NSNumber,NSArray,NSMutableArray,NSData等等,这些都是class,创建后便是对象,而C语言的基本数据类型int,只是一定字节的内存空间,用于存放数值;NSInteger是基本数据类型,并不是NSNumber的子类,当然也不是NSObject的子类。NSInteger是基本数据类型Int或者Long的别名(NSInteger的定义typedef long NSInteger),它的区别在于,NSInteger会根据系统是32位还是64位来决定是本身是int还是long。

id 声明的对象有什么特性?

id 声明的对象具有运行时的特性,即可以指向任意类型的Objcetive-C的对象。

Objective-C 如何对内存管理的,说说你的看法和解决方法?

答:Objective-C的内存管理主要有三种方式ARC(自动内存计数)、手动内存计数、内存池。
1). 自动内存计数ARC:由Xcode自动在App编译阶段,在代码中添加内存管理代码。
2). 手动内存计数MRC:遵循内存谁申请、谁释放;谁添加,谁释放的原则。
3). 内存释放池Release Pool:把需要释放的内存统一放在一个池子中,当池子被抽干后(drain),池子中所有的内存空间也被自动释放掉。内存池的释放操作分为自动和手动。自动释放受runloop机制影响。

Objective-C 中创建线程的方法是什么?如果在主线程中执行代码,方法是什么?如果想延时执行代码、方法又是什么?

答:线程创建有三种方法:使用NSThread创建、使用GCD的dispatch、使用子类化的NSOperation,然后将其加入NSOperationQueue;在主线程执行代码,方法是performSelectorOnMainThread,如果想延时执行代码可以用performSelector:onThread:withObject:waitUntilDone:

Category(类别)、 Extension(扩展)和继承的区别

区别:

  1. 分类有名字,类扩展没有分类名字,是一种特殊的分类。
  2. 分类只能扩展方法(属性仅仅是声明,并没真正实现),类扩展可以扩展属性、成员变量和方法。
  3. 继承可以增加,修改或者删除方法,并且可以增加属性。

我们说的OC是动态运行时语言是什么意思?

答:主要是将数据类型的确定由编译时,推迟到了运行时。简单来说, 运行时机制使我们直到运行时才去决定一个对象的类别,以及调用该类别对象指定方法。

为什么我们常见的delegate属性都用是week而不是retain/strong?

答:是为了防止delegate两端产生不必要的循环引用。
@property (nonatomic, weak) id delegate;

什么时候用delete,什么时候用Notification?

Delegate(委托模式):1对1的反向消息通知功能。
Notification(通知模式):只想要把消息发送出去,告知某些状态的变化。但是并不关心谁想要知道这个。

什么是 KVO 和 KVC?

1). KVC(Key-Value-Coding):键值编码 是一种通过字符串间接访问对象的方式(即给属性赋值)
举例说明:
stu.name = @"张三" // 点语法给属性赋值
[stu setValue:@"张三" forKey:@"name"]; // 通过字符串使用KVC方式给属性赋值
stu1.nameLabel.text = @"张三";
[stu1 setValue:@"张三" forKey:@"nameLabel.text"]; // 跨层赋值
2). KVO(key-Value-Observing):键值观察机制 他提供了观察某一属性变化的方法,极大的简化了代码。
KVO只能被KVC触发,包括使用setValue:forKey:方法和点语法。
// 通过下方方法为属性添加KVO观察

  • (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
     
    // 当被观察的属性发送变化时,会自动触发下方方法
  • (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;
     
    KVC 和 KVO 的 keyPath 可以是属性、实例变量、成员变量。
    iOS 成员变量,属性变量,局部变量,实例变量,全局变量 详解

KVC的底层实现?

当一个对象调用setValue方法时,方法内部会做以下操作:
1). 检查是否存在相应的key的set方法,如果存在,就调用set方法。
2). 如果set方法不存在,就会查找与key相同名称并且带下划线的成员变量,如果有,则直接给成员变量属性赋值。
3). 如果没有找到_key,就会查找相同名称的属性key,如果有就直接赋值。
4). 如果还没有找到,则调用valueForUndefinedKey:和setValue:forUndefinedKey:方法。
这些方法的默认实现都是抛出异常,我们可以根据需要重写它们。

ViewController生命周期

按照执行顺序排列:

  1. initWithCoder:通过nib文件初始化时触发。
  2. awakeFromNib:nib文件被加载的时候,会发生一个awakeFromNib的消息到nib文件中的每个对象。
  3. loadView:开始加载视图控制器自带的view。
  4. viewDidLoad:视图控制器的view被加载完成。
  5. viewWillAppear:视图控制器的view将要显示在window上。
  6. updateViewConstraints:视图控制器的view开始更新AutoLayout约束。
  7. viewWillLayoutSubviews:视图控制器的view将要更新内容视图的位置。
  8. viewDidLayoutSubviews:视图控制器的view已经更新视图的位置。
  9. viewDidAppear:视图控制器的view已经展示到window上。
  10. viewWillDisappear:视图控制器的view将要从window上消失。
  11. viewDidDisappear:视图控制器的view已经从window上消失。

方法和选择器有何不同?

selector是一个方法的名字,方法是一个组合体,包含了名字和实现。

你是否接触过OC中的反射机制?简单聊一下概念和使用

1). class反射
通过类名的字符串形式实例化对象。
Class class = NSClassFromString(@"student");
Student *stu = [[class alloc] init];
将类名变为字符串。
Class class =[Student class];
NSString className = NSStringFromClass(class); 2). SEL的反射 通过方法的字符串形式实例化方法。 SEL selector = NSSelectorFromString(@"setName"); [stu performSelector:selector withObject:@"Mike"]; 将方法变成字符串。NSStringFromSelector(@selector(setName:));

调用方法有两种方式:

1). 直接通过方法名来调用。[person show];
2). 间接的通过SEL数据来调用 。SEL aaa = @selector(show); [person performSelector:aaa];

如何对iOS设备进行性能测试?

答: Profile-> Instruments ->Time Profiler

开发项目时你是怎么检查内存泄露?

1). 静态分析 analyze。
2). instruments工具里面有个leak可以动态分析。

什么是懒加载?

答:懒加载就是只在用到的时候才去初始化。也可以理解成延时加载。
我觉得最好也最简单的一个例子就是tableView中图片的加载显示了, 一个延时加载, 避免内存过高,一个异步加载,避免线程堵塞提高用户体验。

类变量的 @public,@protected,@private,@package 声明各有什么含义?

@public 任何地方都能访问;
@protected 该类和子类中访问,是默认的;
@private 只能在本类中访问;
@package 本包内使用,跨包不可以。

什么是谓词?

谓词就是通过NSPredicate给定的逻辑条件作为约束条件,完成对数据的筛选。
//定义谓词对象,谓词对象中包含了过滤条件(过滤条件比较多)
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"age<%d",30];
//使用谓词条件过滤数组中的元素,过滤之后返回查询的结果
NSArray *array = [persons filteredArrayUsingPredicate:predicate];

isa指针问题

isa:是一个Class 类型的指针. 每个实例对象有个isa的指针,他指向对象的类,而Class里也有个isa的指针, 指向meteClass(元类)。元类保存了类方法的列表。当类方法被调 用时,先会从本身查找类方法的实现,如果没有,元类会向他父类查找该方法。同时注意的是:元类(meteClass)也是类,它也是对象。元类也有isa指针,它的isa指针最终指向的是一个根元类(root meteClass)。根元类的isa指针指向本身,这样形成了一个封闭的内循环。

如何访问并修改一个类的私有属性?

1). 一种是通过KVC获取。
2). 通过runtime访问并修改私有属性。

一个objc对象的isa的指针指向什么?有什么作用?

答:指向他的类对象,从而可以找到对象上的方法。

下面的代码输出什么?

@implementation Son : Father

  • (id)init {
    if (self = [super init]) {
    NSLog(@"%@", NSStringFromClass([self class])); // Son
    NSLog(@"%@", NSStringFromClass([super class])); // Son
    }
    return self;
    }
    @end
    // 解析:
    self 是类的隐藏参数,指向当前调用方法的这个类的实例。
    super是一个Magic Keyword,它本质是一个编译器标示符,和self是指向的同一个消息接收者。
    不同的是:super会告诉编译器,调用class这个方法时,要去父类的方法,而不是本类里的。
    上面的例子不管调用[self class]还是[super class],接受消息的对象都是当前 Son *obj 这个对象。

写一个完整的代理,包括声明、实现

1. // 创建 @protocol MyDelagate @required -(void)eat:(NSString *)foodName; 
2.     @optional -(void)run;
3.     @end //  声明 .h @interface person: NSObject
4. 
5.     @end //  实现 .m @implementation person - (void)eat:(NSString *)foodName { NSLog(@"吃:%@!", foodName);
6.     } 
7.     - (void)run { NSLog(@"run!");
8.     }
9. 
10.     @end

isKindOfClass、isMemberOfClass、selector作用分别是什么

isKindOfClass:作用是某个对象属于某个类型或者继承自某类型。
isMemberOfClass:某个对象确切属于某个类型。
selector:通过方法名,获取在内存中的函数的入口地址。

delegate 和 notification 的区别

1). 二者都用于传递消息,不同之处主要在于一个是一对一的,另一个是一对多的。
2). notification通过维护一个array,实现一对多消息的转发。
3). delegate需要两者之间必须建立联系,不然没法调用代理的方法;notification不需要两者之间有联系。

什么是block?

闭包(block):闭包就是获取其它函数局部变量的匿名函数。

block反向传值

1. *   在控制器间传值可以使用代理或者block,使用block相对来说简洁。
2. 
3. *  在前一个控制器的touchesBegan:方法内实现如下代码。 // OneViewController.m TwoViewController *twoVC = [[TwoViewController alloc] init];
4.       twoVC.valueBlcok = ^(NSString *str) { NSLog(@"OneViewController拿到值:%@", str); 
5.       };
6.       [self presentViewController:twoVC animated:YES completion:nil]; // TwoViewController.h   (在.h文件中声明一个block属性) @property (nonatomic ,strong) void(^valueBlcok)(NSString *str); // TwoViewController.m   (在.m文件中实现方法) - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { // 传值:调用block if (_valueBlcok) {
7.             _valueBlcok(@"123456");
8.         }
9.     }

block的注意点

1). 在block内部使用外部指针且会造成循环引用情况下,需要用__week修饰外部指针:
__weak typeof(self) weakSelf = self;
2). 在block内部如果调用了延时函数还使用弱指针会取不到该指针,因为已经被销毁了,需要在block内部再将弱指针重新强引用一下。
__strong typeof(self) strongSelf = weakSelf;
3). 如果需要在block内部改变外部栈区变量的话,需要在用__block修饰外部变量。

BAD_ACCESS在什么情况下出现?

答:这种问题在开发时经常遇到。原因是访问了野指针,比如访问已经释放对象的成员变量或者发消息、死循环等。

lldb(gdb)常用的控制台调试命令?

1). p 输出基本类型。是打印命令,需要指定类型。是print的简写
p (int)[[[self view] subviews] count]
2). po 打印对象,会调用对象description方法。是print-object的简写
po [self view]
3). expr 可以在调试时动态执行指定表达式,并将结果打印出来。常用于在调试过程中修改变量的值。
4). bt:打印调用堆栈,是thread backtrace的简写,加all可打印所有thread的堆栈
5). br l:是breakpoint list的简写

你一般是怎么用Instruments的?

Instruments里面工具很多,常用:
1). Time Profiler: 性能分析
2). Zombies:检查是否访问了僵尸对象,但是这个工具只能从上往下检查,不智能。
3). Allocations:用来检查内存,写算法的那批人也用这个来检查。
4). Leaks:检查内存,看是否有内存泄露。

iOS中常用的数据存储方式有哪些?

数据存储有四种方案:NSUserDefault、KeyChain、File、DB。
其中File有三种方式:writeToFile:atomically:、Plist、NSKeyedAchiever(归档)
DB包括:SQLite、FMDB、CoreData

iOS的沙盒目录结构是怎样的?

沙盒结构:

  1. AppName.app 目录:这是应用程序的程序包目录,包含应用程序的本身。由于应用程序必须经过签名,所以您在运行时不能对这个目录中的内容进行修改,否则可能会使应用程序无法启动。
  2. Documents:您应该将所有的应用程序数据文件写入到这个目录下。这个目录用于存储用户数据。iCloud备份目录。(这里不能存缓存文件,否则上架不被通过)
  3. Library 目录:这个目录下有两个子目录:
    Preferences 目录:包含应用程序的偏好设置文件。您不应该直接创建偏好设置文件,而是应该使用NSUserDefaults类来取得和设置应用程序的偏好.
    Caches 目录:用于存放应用程序专用的支持文件,保存应用程序再次启动过程中需要的信息。
    可创建子文件夹。可以用来放置您希望被备份但不希望被用户看到的数据。该路径下的文件夹,除Caches以外,都会被iTunes备份。
  4. tmp:存放临时文件,不会被备份,而且这个文件下的数据有可能随时被清除的可能。

iOS多线程技术有哪几种方式?

答:pthread、NSThread、GCD、NSOperation

GCD 与 NSOperation 的区别:

GCD 和 NSOperation 都是用于实现多线程:
GCD 基于C语言的底层API,GCD主要与block结合使用,代码简洁高效。
NSOperation 属于Objective-C类,是基于GCD更高一层的封装。复杂任务一般用NSOperation实现。

写出使用GCD方式从子线程回到主线程的方法代码

答:dispatch_sync(dispatch_get_main_queue(), ^{ });

如何用GCD同步若干个异步调用?(如根据若干个url异步加载多张图片,然后在都下载完成后合成一张整图)

// 使用Dispatch Group追加block到Global Group Queue,这些block如果全部执行完毕,就会执行Main Dispatch Queue中的结束处理的block。
// 创建队列组
dispatch_group_t group = dispatch_group_create();
// 获取全局并发队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_async(group, queue, ^{ /*加载图片1 / });dispatch_group_async(group, queue, ^{ /加载图片2 / });dispatch_group_async(group, queue, ^{ /加载图片3 */ });
// 当并发队列组中的任务执行完毕后才会执行这里的代码
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 合并图片
});

dispatch_barrier_async(栅栏函数)的作用是什么?

1. 函数定义:dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);
2.     作用: 1.在它前面的任务执行结束后它才执行,它后面的任务要等它执行完成后才会开始执行。 2.避免数据竞争 // 1.创建并发队列 dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT); // 2.向队列中添加任务 dispatch_async(queue, ^{ // 1.2是并行的 NSLog(@"任务1, %@",[NSThread currentThread]);
3.     }); dispatch_async(queue, ^{ NSLog(@"任务2, %@",[NSThread currentThread]);
4.     });
5. 
6.     dispatch_barrier_async(queue, ^{ NSLog(@"任务 barrier, %@", [NSThread currentThread]);
7.     }); dispatch_async(queue, ^{ // 这两个是同时执行的 NSLog(@"任务3, %@",[NSThread currentThread]);
8.     }); dispatch_async(queue, ^{ NSLog(@"任务4, %@",[NSThread currentThread]);
9.     }); // 输出结果: 任务1 任务2 ——》 任务 barrier ——》任务3 任务4  // 其中的任务1与任务2,任务3与任务4 由于是并行处理先后顺序不定。

以下代码运行结果如何?

1. - (void)viewDidLoad {
2.     [super viewDidLoad]; NSLog(@"1"); dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(@"2");
3.     }); NSLog(@"3");
4. } // 只输出:1。(主线程死锁)

什么是 RunLoop

  • 从字面上看,就是运行循环,跑圈
  • 其实它内部就是do-while循环,在这个循环内部不断地处理各种任务(比如Source、Timer、Observer)
  • 一个线程对应一个RunLoop,基本作用就是保持程序的持续运行,处理app中的各种事件。
  • 通过runloop,有事运行,没事就休息,可以节省cpu资源,提高程序性能。
     
    主线程的run loop默认是启动的。iOS的应用程序里面,程序启动后会有一个如下的main()函数
    int main(int argc, char * argv[]) {
    @autoreleasepool {
    return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
    }
    RunLoop学习总结

说说你对 runtime 的理解

Runtime又叫运行时,是一套底层的C语言API,其为iOS内部的核心之一,我们平时编写的OC代码,底层都是基于它来实现的。

Runtime实现的机制是什么,怎么用,一般用于干嘛?

1). 使用时需要导入的头文件 
2). Runtime 运行时机制,它是一套C语言库。
3). 实际上我们编写的所有OC代码,最终都是转成了runtime库的东西。
比如:
类转成了 Runtime 库里面的结构体等数据类型,
方法转成了 Runtime 库里面的C语言函数,
平时调方法都是转成了 objc_msgSend 函数(所以说OC有个消息发送机制)
// OC是动态语言,每个方法在运行时会被动态转为消息发送,即:objc_msgSend(receiver, selector)。
// [stu show]; 在objc动态编译时,会被转意为:objc_msgSend(stu, @selector(show));
4). 因此,可以说 Runtime 是OC的底层实现,是OC的幕后执行者。

有了Runtime库,能做什么事情呢?

Runtime库里面包含了跟类、成员变量、方法相关的API。
比如:
(1)获取类里面的所有成员变量。
(2)为类动态添加成员变量。
(3)为类动态添加新的方法。
(4)动态改变类的方法实现等。(Method Swizzling)
因此,有了Runtime,想怎么改就怎么改。

什么是 Method Swizzle(黑魔法),什么情况下会使用?

1). 在没有一个类的实现源码的情况下,想改变其中一个方法的实现,除了继承它重写、和借助类别重名方法暴力抢先之外,还有更加灵活的方法 Method Swizzle。
2). Method Swizzle 指的是改变一个已存在的选择器对应的实现的过程。OC中方法的调用能够在运行时通过改变,通过改变类的调度表中选择器到最终函数间的映射关系。
3). 在OC中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用OC的动态特性,可以实现在运行时偷换selector对应的方法实现。
4). 每个类都有一个方法列表,存放着selector的名字和方法实现的映射关系。IMP有点类似函数指针,指向具体的方法实现。
5). 我们可以利用 method_exchangeImplementations 来交换2个方法中的IMP。
6). 我们可以利用 class_replaceMethod 来修改类。
7). 我们可以利用 method_setImplementation 来直接设置某个方法的IMP。
8). 归根结底,都是偷换了selector的IMP。

_objc_msgForward 函数是做什么的,直接调用它将会发生什么?

答:_objc_msgForward是 IMP 类型,用于消息转发的:当向一个对象发送一条消息,但它并没有实现的时候,_objc_msgForward会尝试做消息转发。

什么是 TCP / UDP ?

TCP:传输控制协议。
UDP:用户数据协议。
 
TCP 是面向连接的,建立连接需要经历三次握手,是可靠的传输层协议。
UDP 是面向无连接的,数据传输是不可靠的,它只管发,不管收不收得到。
简单的说,TCP注重数据安全,而UDP数据传输快点,但安全性一般。

通信底层原理(OSI七层模型)

OSI采用了分层的结构化技术,共分七层:
物理层、数据链路层、网络层、传输层、会话层、表示层、应用层。

介绍一下XMPP?

XMPP是一种以XML为基础的开放式实时通信协议。
简单的说,XMPP就是一种协议,一种规定。就是说,在网络上传东西,XMM就是规定你上传大小的格式。

OC中创建线程的方法是什么?如果在主线程中执行代码,方法是什么?

1. // 创建线程的方法 - [NSThread detachNewThreadSelector:nil toTarget:nil withObject:nil]
2.    - [self performSelectorInBackground:nil withObject:nil];
3.    - [[NSThread alloc] initWithTarget:nil selector:nil object:nil];
4.    - dispatch_async(dispatch_get_global_queue(0, 0), ^{});
5.    - [[NSOperationQueue new] addOperation:nil]; // 主线程中执行代码的方法 - [self performSelectorOnMainThread:nil withObject:nil waitUntilDone:YES];
6.    - dispatch_async(dispatch_get_main_queue(), ^{});
7.    - [[NSOperationQueue mainQueue] addOperation:nil];

tableView的重用机制?

答:UITableView 通过重用单元格来达到节省内存的目的: 通过为每个单元格指定一个重用标识符,即指定了单元格的种类,当屏幕上的单元格滑出屏幕时,系统会把这个单元格添加到重用队列中,等待被重用,当有新单元格从屏幕外滑入屏幕内时,从重用队列中找看有没有可以重用的单元格,如果有,就拿过来用,如果没有就创建一个来使用。

用伪代码写一个线程安全的单例模式

1. static id _instance;
2.     + (id)allocWithZone:(struct _NSZone *)zone { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{
3.            _instance = [super allocWithZone:zone];
4.        }); return _instance;
5.     }
6. 
7.     + (instancetype)sharedData { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{
8.            _instance = [[self alloc] init];
9.        }); return _instance;
10.     }
11. 
12.     - (id)copyWithZone:(NSZone *)zone { return _instance;
13.     }

如何实现视图的变形?

答:通过修改view的 transform 属性即可。

在手势对象基础类UIGestureRecognizer的常用子类手势类型中哪两个手势发生后,响应只会执行一次?

答:UITapGestureRecognizer,UISwipeGestureRecognizer是一次性手势,手势发生后,响应只会执行一次。

字符串常用方法:

NSString str = @"abc123";
NSArray arr = [str componentsSeparatedByString:@""]; //以目标字符串把原字符串分割成两部分,存到数组中。@[@"abc", @"123"];

如何高性能的给 UIImageView 加个圆角?

1. *   不好的解决方案:使用下面的方式会`强制Core Animation提前渲染屏幕的离屏绘制, 而离屏绘制就会给性能带来负面影响`,会有卡顿的现象出现。 self.view.layer.cornerRadius = 5.0f; self.view.layer.masksToBounds = YES; 
2. 
3.     *   正确的解决方案:使用绘图技术
4. 
5.         - (UIImage *)circleImage { // NO代表透明 UIGraphicsBeginImageContextWithOptions(self.size, NO, 0.0); // 获得上下文 CGContextRef ctx = UIGraphicsGetCurrentContext(); // 添加一个圆 CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height); CGContextAddEllipseInRect(ctx, rect); // 裁剪 CGContextClip(ctx); // 将图片画上去 [self drawInRect:rect]; UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); // 关闭上下文 UIGraphicsEndImageContext(); return image;
6.         } 
7. 
8.     *   还有一种方案:使用了贝塞尔曲线"切割"个这个图片, 给UIImageView 添加了的圆角,其实也是通过绘图技术来实现的。 UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
9.         imageView.center = CGPointMake(200, 300); UIImage *anotherImage = [UIImage imageNamed:@"image"]; UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, 1.0);
10.         [[UIBezierPath bezierPathWithRoundedRect:imageView.bounds
11.                                cornerRadius:50] addClip];
12.         [anotherImage drawInRect:imageView.bounds];
13.         imageView.image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext();
14.         [self.view addSubview:imageView];

你是怎么封装一个view的

1. 1). 可以通过纯代码或者xib的方式来封装子控件 2). 建立一个跟view相关的模型,然后将模型数据传给view,通过模型上的数据给view的子控件赋值 /**
2.      *  纯代码初始化控件时一定会走这个方法
3.      */ - (instancetype)initWithFrame:(CGRect)frame { if(self = [super initWithFrame:frame]) {
4.             [self setupUI];
5.         } return self;
6.     } /**
7.      *  通过xib初始化控件时一定会走这个方法
8.      */ - (id)initWithCoder:(NSCoder *)aDecoder { if(self = [super initWithCoder:aDecoder]) {
9.             [self setupUI];
10.         } return self;
11.     }
12. 
13.     - (void)setupUI { // 初始化代码 }

HTTP协议中 POST 方法和 GET 方法有那些区别?

  1. GET用于向服务器请求数据,POST用于提交数据
  2. GET请求,请求参数拼接形式暴露在地址栏,而POST请求参数则放在请求体里面,因此GET请求不适合用于验证密码等操作
  3. GET请求的URL有长度限制,POST请求不会有长度限制

请简单的介绍下APNS发送系统消息的机制

APNS优势:杜绝了类似安卓那种为了接受通知不停在后台唤醒程序保持长连接的行为,由iOS系统和APNS进行长连接替代。
APNS的原理:
1). 应用在通知中心注册,由iOS系统向APNS请求返回设备令牌(device Token)
2). 应用程序接收到设备令牌并发送给自己的后台服务器
3). 服务器把要推送的内容和设备发送给APNS
4). APNS根据设备令牌找到设备,再由iOS根据APPID把推送内容展示

77. ios开发逆向传值的几种方法整理

第一种:代理传值

1. 第二个控制器: @protocol WJSecondViewControllerDelegate  - (void)changeText:(NSString*)text; @end @property(nonatomic,assign)iddelegate;
2. 
3.   - (IBAction)buttonClick:(UIButton*)sender {
4.   _str = sender.titleLabel.text;
5.   [self.delegate changeText:sender.titleLabel.text];
6.   [self.navigationController popViewControllerAnimated:YES];
7.   }
8. 
9.   第一个控制器:
10. 
11.   - (IBAction)pushToSecond:(id)sender {
12.   WJSecondViewController *svc = [[WJSecondViewController alloc]initWithNibName:@"WJSecondViewController" bundle:nil];
13.   svc.delegate = self;
14.   svc.str = self.navigationItem.title;
15.   [self.navigationController pushViewController:svc animated:YES];
16.   [svc release];
17.   }
18.   - (void)changeText:(NSString *)text{ self.navigationItem.title = text;
19.   }

第二种:通知传值

1. 第一个控制器: //注册监听通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(limitDataForModel:) name:@"NOV" object:nil];
2.   - (void)limitDataForModel:(NSNotification *)noti{ self.gamesInfoArray = noti.object;
3.   }
4. 
5.   第二个控制器: //发送通知 [[NSNotificationCenter defaultCenter]     postNotificationName:@"NOV" object:gameArray];

第三种:单例传值

1. Single是一个单例类,并且有一个字符串类型的属性titleName
2.   在第二个控制器:
3. 
4.   - (IBAction)buttonClick:(UIButton*)sender {
5.   Single *single = [Single sharedSingle];
6.   single.titleName = sender.titleLabel.text;
7.   [self.navigationController popViewControllerAnimated:YES];
8.   }
9.   
10.   第一个控制器:
11. 
12.   - (void)viewWillAppear:(BOOL)animated{
13.   [super viewWillAppear:animated];
14.   Single *single = [Single sharedSingle]; self.navigationItem.title = single.titleName;
15.   }

第四种:block传值

1. 第二个控制器: @property (nonatomic,copy) void (^changeText_block)(NSString*);
2.   - (IBAction)buttonClick:(UIButton*)sender {
3.   _str = sender.titleLabel.text; self.changeText_block(sender.titleLabel.text);
4.   [self.navigationController popViewControllerAnimated:YES];
5.   }
6. 
7.   第一个控制器:
8. 
9.   - (IBAction)pushToSecond:(id)sender {
10.   WJSecondViewController *svc = [[WJSecondViewController alloc]initWithNibName:@"WJSecondViewController" bundle:nil];
11.   svc.str = self.navigationItem.title;
12.   [svc setChangeText_block:^(NSString *str) {
13.       >self.navigationItem.title = str;
14.   }];
15.   [self.navigationController pushViewController:svc animated:YES];
16.   }

第五种:extern传值

1. 第二个控制器: extern NSString *btn;
2.   - (IBAction)buttonClick:(UIButton*)sender {
3.   btn = sender.titleLabel.text;
4.   [self.navigationController popViewControllerAnimated:YES];
5.   }
6.  
7.   第一个控制器: NSString *btn = nil;
8.   - (void)viewWillAppear:(BOOL)animated{
9.   [super viewWillAppear:animated]; self.navigationItem.title = btn;
10.   }

第六种:KVO传值

1. 第一个控制器:
2. 
3.   - (void)viewDidLoad {
4.   [super viewDidLoad];
5.    _vc =[[SecondViewController alloc]init]; //self监听vc里的textValue属性 [_vc addObserver:self forKeyPath:@"textValue" options:0 context:nil];   
6.   }
7. 
8.   第二个控制器:
9. 
10.   - (IBAction)buttonClicked:(id)sender { self.textValue = self.textField.text;
11.   [self.navigationController popViewControllerAnimated:YES];
12.   }

78. 浅谈iOS开发中方法延迟执行的几种方式

Method1. performSelector方法
Method2. NSTimer定时器
Method3. NSThread线程的sleep
Method4. GCD

公用延迟执行方法

  • (void)delayMethod{ NSLog(@"delayMethodEnd");

Method1: performSelector

1. [self performSelector:@selector(delayMethod) withObject:nil/*可传任意类型参数*/ afterDelay:2.0];`
2. 注:此方法是一种非阻塞的执行方式,未找到取消执行的方法。
3. 
4. > 程序运行结束
5. > 2015-08-31 10:56:59.361 CJDelayMethod[1080:39604] delayMethodStart2015-08-31 10:56:59.363 CJDelayMethod[1080:39604] nextMethod2015-08-31 10:57:01.364 CJDelayMethod[1080:39604] delayMethodEnd

Method2: NSTimer定时器

1. NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(delayMethod) userInfo:nil repeats:NO];`
2.   注:此方法是一种非阻塞的执行方式,
3.   取消执行方法:`- (void)invalidate;`即可
4. 
5.   > 程序运行结束
6.   > 2015-08-31 10:58:10.182 CJDelayMethod[1129:41106] delayMethodStart2015-08-31 10:58:10.183 CJDelayMethod[1129:41106] nextMethod2015-08-31 10:58:12.185 CJDelayMethod[1129:41106] delayMethodEnd

Method3: NSThread线程的sleep

1. [NSThread sleepForTimeInterval:2.0];
2.   注:此方法是一种阻塞执行方式,建议放在子线程中执行,否则会卡住界面。但有时还是需要阻塞执行,如进入欢迎界面需要沉睡3秒才进入主界面时。
3.   没有找到取消执行方式。
4. 
5.   > 程序运行结束
6.   > 2015-08-31 10:58:41.501 CJDelayMethod[1153:41698] delayMethodStart2015-08-31 10:58:43.507 CJDelayMethod[1153:41698] nextMethod

Method4: GCD

1. __block ViewController/*主控制器*/ *weakSelf = self; dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0/*延迟执行时间*/ * NSEC_PER_SEC));
2. 
3.   dispatch_after(delayTime, dispatch_get_main_queue(), ^{
4.       [weakSelf delayMethod];
5.   });
6. 
7.   注:此方法可以在参数中选择执行的线程,是一种非阻塞执行方式。没有找到取消执行方式。
8. 
9.   > 程序运行结束
10.   > 2015-08-31 10:59:21.652 CJDelayMethod[1181:42438] delayMethodStart2015-08-31 10:59:21.653 CJDelayMethod[1181:42438] nextMethod2015-08-31 10:59:23.653 CJDelayMethod[1181:42438] delayMethodEnd
1. 完整代码参见:
2. 
3.   > // > // ViewController.m > // CJDelayMethod > // > // Created by 陈杰 on 8/31/15. > // Copyright (c) 2015 chenjie. All rights reserved. > // > 
4.   > # import "ViewController.h" > 
5.   > @interface ViewController () > @property (nonatomic, strong) NSTimer *timer;
6.   > @end > @implementation ViewController* > 
7.   >  *`- (void)viewDidLoad { `*
8.   > 
9.   > *` [super viewDidLoad]; `*
10.   > 
11.   > *` NSLog(@"delayMethodStart"); `*
12.   > 
13.   > *` [self methodOnePerformSelector];// `* > 
14.   > *` [self methodTwoNSTimer];// `* > 
15.   > *` [self methodThreeSleep];//`* > 
16.   > *` [self methodFourGCD]; `*
17.   > 
18.   > *` NSLog(@"nextMethod");`*
19.   > 
20.   > *`}`*
21.   > 
22.   >  *`- (void)methodFiveAnimation{ `*
23.   > 
24.   > *` [UIView animateWithDuration:0 delay:2.0 options:UIViewAnimationOptionAllowUserInteraction animations:^{ } completion:^(BOOL finished) { `*
25.   > 
26.   > *` [self delayMethod]; `*
27.   > 
28.   > *` }];`*
29.   > 
30.   > *`}`
31.   > `- (void)methodFourGCD{ `*
32.   > 
33.   > *` __block ViewController`*`weakSelf = self; dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)); dispatch_after(delayTime, dispatch_get_main_queue(), ^{ `
34.   > 
35.   > ` [weakSelf delayMethod]; `
36.   > 
37.   > ` });`
38.   > 
39.   > `}`
40.   > 
41.   >  `- (void)methodThreeSleep{ `
42.   > 
43.   > ` [NSThread sleepForTimeInterval:2.0];`
44.   > 
45.   > `}`
46.   > 
47.   >  `- (void)methodTwoNSTimer{`
48.   > 
49.   > ` NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(delayMethod) userInfo:nil repeats:NO];`
50.   > 
51.   > `}`
52.   > 
53.   >  `- (void)methodOnePerformSelector{`
54.   > 
55.   > ` [self performSelector:@selector(delayMethod) withObject:nil/*可传任意类型参数*/ afterDelay:2.0];`
56.   > 
57.   > `}`
58.   > 
59.   >  `- (void)delayMethod{`
60.   > 
61.   > ` NSLog(@"delayMethodEnd");`
62.   > 
63.   > `}`
64.   > `- (void)didReceiveMemoryWarning { `
65.   > 
66.   > ` [super didReceiveMemoryWarning]; `
67.   > 
68.   > ` // Dispose of any resources that can be recreated.` > 
69.   > `}`
70.   > 
71.   > `@end`

NSPersistentStoreCoordinator , NSManaged0bjectContext 和NSManaged0bject中的那些需要在线程中创建或者传递

答:NSPersistentStoreCoordinator是持久化存储协调者,主要用于协调托管对象上下文和持久化存储区之间的关系。NSManagedObjectContext使用协调者的托管对象模型将数据保存到数据库,或查询数据。

您是否做过一部的网络处理和通讯方面的工作?如果有,能具体介绍一下实现策略么?

答:使用NSOperation发送异步网络请求,使用NSOperationQueue管理线程数目及优先级,底层是用NSURLConnetion

你使用过Objective-C的运行时编程(Runtime Programming)么?如果使用过,你用它做了什么?你还能记得你所使用的相关的头文件或者某些方法的名称吗?

答:Objecitve-C的重要特性是Runtime(运行时),在#import 下能看到相关的方法,用过objc_getClass()和class_copyMethodList()获取过私有API;使用
Method method1 = class_getInstanceMethod(cls, sel1);
Method method2 = class_getInstanceMethod(cls, sel2);
method_exchangeImplementations(method1, method2);
 
代码交换两个方法,在写unit test时使用到。

Core开头的系列的内容。是否使用过CoreAnimation和CoreGraphics。UI框架和CA,CG框架的联系是什么?分别用CA和CG做过些什么动画或者图像上的内容。(有需要的话还可以涉及Quartz的一些内容)

答:UI框架的底层有CoreAnimation,CoreAnimation的底层有CoreGraphics。

UIKit
Core Animation
Core Graphics
Graphics Hardware
使用CA做过menu菜单的展开收起(太逊了)

是否使用过CoreText或者CoreImage等?如果使用过,请谈谈你使用CoreText或者CoreImage的体验。

答:CoreText可以解决复杂文字内容排版问题。CoreImage可以处理图片,为其添加各种效果。体验是很强大,挺复杂的。

自动释放池是什么,如何工作

答:当您向一个对象发送一个autorelease消息时,Cocoa就会将该对象的一个引用放入到最新的自动释放.它仍然是个OC的对象,因此自动释放池定义的作用域内的其它对象可以向它发送消息。当程序执行到作用域结束的位置时,自动释放池就会被释放,池中的所有对象也就被释放。

NSNotification和KVO的区别和用法是什么?什么时候应该使用通知,什么时候应该使用KVO,它们的实现上有什么区别吗?如果用protocol和delegate(或者delegate的Array)来实现类似的功能可能吗?如果可能,会有什么潜在的问题?如果不能,为什么?(虽然protocol和delegate这种东西面试已经面烂了…)

答:NSNotification是通知模式在iOS的实现,KVO的全称是键值观察(Key-value observing),其是基于KVC(key-value coding)的,KVC是一个通过属性名访问属性变量的机制。例如将Module层的变化,通知到多个Controller对象时,可以使用NSNotification;如果是只需要观察某个对象的某个属性,可以使用KVO。
对于委托模式,在设计模式中是对象适配器模式,其是delegate是指向某个对象的,这是一对一的关系,而在通知模式中,往往是一对多的关系。委托模式,从技术上可以现在改变delegate指向的对象,但不建议这样做,会让人迷惑,如果一个delegate对象不断改变,指向不同的对象。

你用过NSOperationQueue么?如果用过或者了解的话,你为什么要使用NSOperationQueue,实现了什么?请描述它和G.C.D的区别和类似的地方(提示:可以从两者的实现机制和适用范围来描述)。

答:使用NSOperationQueue用来管理子类化的NSOperation对象,控制其线程并发数目。GCD和NSOperation都可以实现对线程的管理,区别是 NSOperation和NSOperationQueue是多线程的面向对象抽象。项目中使用NSOperation的优点是NSOperation是对线程的高度抽象,在项目中使用它,会使项目的程序结构更好,子类化NSOperation的设计思路,是具有面向对象的优点(复用、封装),使得实现是多线程支持,而接口简单,建议在复杂项目中使用。
项目中使用GCD的优点是GCD本身非常简单、易用,对于不复杂的多线程操作,会节省代码量,而Block参数的使用,会是代码更为易读,建议在简单项目中使用。

既然提到G.C.D,那么问一下在使用G.C.D以及block时要注意些什么?它们两是一回事儿么?block在ARC中和传统的MRC中的行为和用法有没有什么区别,需要注意些什么?

答:使用block是要注意,若将block做函数参数时,需要把它放到最后,GCD是Grand Central Dispatch,是一个对线程开源类库,而Block是闭包,是能够读取其他函数内部变量的函数。

对于Objective-C,你认为它最大的优点和最大的不足是什么?对于不足之处,现在有没有可用的方法绕过这些不足来实现需求。如果可以的话,你有没有考虑或者实践过重新实现OC的一些功能,如果有,具体会如何做?

答:最大的优点是它的运行时特性,不足是没有命名空间,对于命名冲突,可以使用长命名法或特殊前缀解决,如果是引入的第三方库之间的命名冲突,可以使用link命令及flag解决冲突。
你实现过一个框架或者库以供别人使用么?如果有,请谈一谈构建框架或者库时候的经验;如果没有,请设想和设计框架的public的API,并指出大概需要如何做、需要注意一些什么方面,来使别人容易地使用你的框架。
答:抽象和封装,方便使用。首先是对问题有充分的了解,比如构建一个文件解压压缩框架,从使用者的角度出发,只需关注发送给框架一个解压请求,框架完成复杂文件的解压操作,并且在适当的时候通知给是哦难过者,如解压完成、解压出错等。在框架内部去构建对象的关系,通过抽象让其更为健壮、便于更改。其次是API的说明文档。

二、 第三方框架

AFNetworking 底层原理分析

AFNetworking主要是对NSURLSession和NSURLConnection(iOS9.0废弃)的封装,其中主要有以下类:
1). AFHTTPRequestOperationManager:内部封装的是 NSURLConnection, 负责发送网络请求, 使用最多的一个类。(3.0废弃)
2). AFHTTPSessionManager:内部封装是 NSURLSession, 负责发送网络请求,使用最多的一个类。
3). AFNetworkReachabilityManager:实时监测网络状态的工具类。当前的网络环境发生改变之后,这个工具类就可以检测到。
4). AFSecurityPolicy:网络安全的工具类, 主要是针对 HTTPS 服务。
 
5). AFURLRequestSerialization:序列化工具类,基类。上传的数据转换成JSON格式
(AFJSONRequestSerializer).使用不多。
6). AFURLResponseSerialization:反序列化工具类;基类.使用比较多:
7). AFJSONResponseSerializer; JSON解析器,默认的解析器.
8). AFHTTPResponseSerializer; 万能解析器; JSON和XML之外的数据类型,直接返回二进
制数据.对服务器返回的数据不做任何处理.
9). AFXMLParserResponseSerializer; XML解析器;

描述下SDWebImage里面给UIImageView加载图片的逻辑

SDWebImage 中为 UIImageView 提供了一个分类UIImageView+WebCache.h, 这个分类中有一个最常用的接口sd_setImageWithURL:placeholderImage:,会在真实图片出现前会先显示占位图片,当真实图片被加载出来后再替换占位图片。
 
加载图片的过程大致如下:
1.首先会在 SDWebImageCache 中寻找图片是否有对应的缓存, 它会以url 作为数据的索引先在内存中寻找是否有对应的缓存
2.如果缓存未找到就会利用通过MD5处理过的key来继续在磁盘中查询对应的数据, 如果找到了, 就会把磁盘中的数据加载到内存中,并将图片显示出来
3.如果在内存和磁盘缓存中都没有找到,就会向远程服务器发送请求,开始下载图片
4.下载后的图片会加入缓存中,并写入磁盘中
5.整个获取图片的过程都是在子线程中执行,获取到图片后回到主线程将图片显示出来
 
SDWebImage原理:
调用类别的方法:

  1. 从内存(字典)中找图片(当这个图片在本次使用程序的过程中已经被加载过),找到直接使用。
  2. 从沙盒中找(当这个图片在之前使用程序的过程中被加载过),找到使用,缓存到内存中。
  3. 从网络上获取,使用,缓存到内存,缓存到沙盒。

友盟统计接口统计的所有功能

APP启动速度,APP停留页面时间等

三、算法

1.不用中间变量,用两种方法交换A和B的值

1. // 1.中间变量 void swap(int a, int b) { int temp = a;
2.        a = b;
3.        b = temp;
4.     } // 2.加法 void swap(int a, int b) {
5.        a = a + b;
6.        b = a - b;
7.        a = a - b;
8.     } // 3.异或(相同为0,不同为1\. 可以理解为不进位加法) void swap(int a, int b) {
9.        a = a ^ b;
10.        b = a ^ b;
11.        a = a ^ b;
12.     }

2.求最大公约数

1. /** 1.直接遍历法 */ int maxCommonDivisor(int a, int b) { int max = 0; for (int i = 1; i <=b; i++) { if (a % i == 0 && b % i == 0) {
2.                 max = I;
3.             }
4.         } return max;
5.     } /** 2.辗转相除法 */ int maxCommonDivisor(int a, int b) { int r; while(a % b > 0) {
6.             r = a % b;
7.             a = b;
8.             b = r;
9.         } return b;
10.     } // 扩展:最小公倍数 = (a * b)/最大公约数

3.模拟栈操作

1. /**
2.      *  栈是一种数据结构,特点:先进后出
3.      *  练习:使用全局变量模拟栈的操作
4.      */ #include  #include  #include  //保护全局变量:在全局变量前加static后,这个全局变量就只能在本文件中使用 static int data[1024];//栈最多能保存1024个数据 static int count = 0;//目前已经放了多少个数(相当于栈顶位置) //数据入栈 push void push(int x){
5.         assert(!full());//防止数组越界 data[count++] = x;
6.     } //数据出栈 pop int pop(){
7.         assert(!empty()); return data[--count];
8.     } //查看栈顶元素 top int top(){
9.         assert(!empty()); return data[count-1];
10.     } //查询栈满 full bool full() { if(count >= 1024) { return 1;
11.         } return 0; 
12.     } //查询栈空 empty bool empty() { if(count <= 0) { return 1;
13.         } return 0;
14.     } int main(){ //入栈 for (int i = 1; i <= 10; i++) {
15.             push(i);
16.         } //出栈 while(!empty()){ printf("%d ", top()); //栈顶元素 pop(); //出栈 } printf("\n"); return 0;
17.     }

4.排序算法

1. 选择排序、冒泡排序、插入排序三种排序算法可以总结为如下:
2. 
3.     *   都将数组分为已排序部分和未排序部分。 1\. 选择排序将已排序部分定义在左端,然后选择未排序部分的最小元素和未排序部分的第一个元素交换。 2\. 冒泡排序将已排序部分定义在右端,在遍历未排序部分的过程执行交换,将最大元素交换到最右端。 3\. 插入排序将已排序部分定义在左端,将未排序部分元的第一个元素插入到已排序部分合适的位置。

选择排序*

【选择排序】:最值出现在起始端    第1趟:在n个数中找到最小(大)数与第一个数交换位置       第2趟:在剩下n-1个数中找到最小(大)数与第二个数交换位置      重复这样的操作...依次与第三个、第四个...数交换位置       第n-1趟,最终可实现数据的升序(降序)排列。

void selectSort(int  * arr,  int length)  { 
for  (int i  =  0; i  <  length  -  1; i++)  { 
        //趟数 for (int j = i + 1; j < length; j++) { //比较次数 
        if (arr[i] > arr[j]) {                    
        int temp = arr[i];                   
        arr[i] = arr[j];                   
        arr[j] = temp;                
        }            
      }       
    }   
}      

冒泡排序         【冒泡排序】:相邻元素两两比较,比较完一趟,最值出现在末尾      第1趟:依次比较相邻的两个数,不断交换(小数放前,大数放后)逐个推进,最值最后出现在第n个元素位置      第2趟:依次比较相邻的两个数,不断交换(小数放前,大数放后)逐个推进,最值最后出现在第n-1个元素位置       第n-1趟:依次比较相邻的两个数,不断交换(小数放前,大数放后)逐个推进,最值最后出现在第2个元素位置

void bublleSort(int  * arr,  int length)  { 
for (int i  =  0; i  <  length  -  1; i++)  {  
        //趟数 for(int j = 0; j < length - i - 1; j++) { 
        //比较次数 if(arr[j] > arr[j+1]) {                     
        int temp = arr[j];       
        arr[j] = arr[j+1];        
        arr[j+1] = temp;               
        }             
      }          
   }
}  

5.折半查找(二分查找)

1. /**
2.      *  折半查找:优化查找时间(不用遍历全部数据)
3.      *
4.      *  折半查找的原理:
5.      *   1> 数组必须是有序的
6.      *   2> 必须已知min和max(知道范围)
7.      *   3> 动态计算mid的值,取出mid对应的值进行比较
8.      *   4> 如果mid对应的值大于要查找的值,那么max要变小为mid-1
9.      *   5> 如果mid对应的值小于要查找的值,那么min要变大为mid+1
10.      *
11.      */ // 已知一个有序数组, 和一个key, 要求从数组中找到key对应的索引位置  int findKey(int *arr, int length, int key) { int min = 0, max = length - 1, mid; while (min <= max) {
12.             mid = (min + max) / 2; //计算中间值 if (key > arr[mid]) {
13.                 min = mid + 1;
14.             } else if (key < arr[mid]) {
15.                 max = mid - 1;
16.             } else { return mid;
17.             }
18.         } return -1;
19.     }

四、编码格式(优化细节)

1.在 Objective-C 中,enum 建议使用NS_ENUM和NS_OPTIONS宏来定义枚举类型。

1. //定义一个枚举(比较严密) typedef NS_ENUM(NSInteger, BRUserGender) {
2.         BRUserGenderUnknown, // 未知 BRUserGenderMale, // 男性 BRUserGenderFemale, // 女性 BRUserGenderNeuter // 无性 }; @interface BRUser : NSObject @property (nonatomic, readonly, copy) NSString *name; @property (nonatomic, readonly, assign) NSUInteger age; @property (nonatomic, readonly, assign) BRUserGender gender;
3. 
4.     - (instancetype)initWithName:(NSString *)name age:(NSUInteger)age gender:(BRUserGender)gender; @end //说明: //既然该类中已经有一个“初始化方法” ,用于设置 name、age 和 gender 的初始值: 那么在设计对应 @property 时就应该尽量使用不可变的对象:其三个属性都应该设为“只读”。用初始化方法设置好属性值之后,就不能再改变了。 //属性的参数应该按照下面的顺序排列: (原子性,读写,内存管理)

2.避免使用C语言中的基本数据类型,建议使用 Foundation 数据类型,对应关系如下:

int -> NSInteger
unsigned -> NSUInteger
float -> CGFloat
动画时间 -> NSTimeInterval

五、其它知识点

HomeKit,是苹果2014年发布的智能家居平台。

什么是 OpenGL、Quartz 2D?

Quatarz 2d 是Apple提供的基本图形工具库。只是适用于2D图形的绘制。
OpenGL,是一个跨平台的图形开发库。适用于2D和3D图形的绘制。

ffmpeg框架:

ffmpeg 是音视频处理工具,既有音视频编码解码功能,又可以作为播放器使用。

谈谈 UITableView 的优化

1). 正确的复用cell;
2). 设计统一规格的Cell;
3). 提前计算并缓存好高度(布局),因为heightForRowAtIndexPath:是调用最频繁的方法;
4). 异步绘制,遇到复杂界面,遇到性能瓶颈时,可能就是突破口;
5). 滑动时按需加载,这个在大量图片展示,网络加载的时候很管用!
6). 减少子视图的层级关系;
7). 尽量使所有的视图不透明化以及做切圆操作;
8). 不要动态的add 或者 remove 子控件。最好在初始化时就添加完,然后通过hidden来控制是否显示;
9). 使用调试工具分析问题。

如何实行cell的动态的行高

如果希望每条数据显示自身的行高,必须设置两个属性,1.预估行高,2.自定义行高。
设置预估行高 tableView.estimatedRowHeight = 200。
设置定义行高 tableView.estimatedRowHeight = UITableViewAutomaticDimension。
如果要让自定义行高有效,必须让容器视图有一个自下而上的约束。

说说你对 block 的理解

栈上的自动复制到堆上,block 的属性修饰符是 copy,循环引用的原理和解决方案。
 
block的循环引用;block的代码实现;为什么会造成循环引用;block是如何强引用self的;

什么是野指针、空指针?

野指针:不知道指向了哪里的指针叫野指针。即指针指向不确定,指针存的地址是一个垃圾值,未初始化。
空指针:不指向任何位置的指针叫空指针。即指针没有指向,指针存的地址是一个空地址,NULL。
什么是 OOA / OOD / OOP ?
OOA(Object Oriented Analysis) --面向对象分析
OOD(Object Oriented Design) --面向对象设计
OOP(Object Oriented Programming)--面向对象编程

多线程是什么

多线程是个复杂的概念,按字面意思是同步完成多项任务,提高了资源的使用效率,从硬件、操作系统、应用软件不同的角度去看,多线程被赋予不同的内涵,对于硬件,现在市面上多数的CPU都是多核的,多核的CPU运算多线程更为出色;从操作系统角度,是多任务,现在用的主流操作系统都是多任务的,可以一边听歌、一边写博客;对于应用来说,多线程可以让应用有更快的回应,可以在网络下载时,同时响应用户的触摸操作。在iOS应用中,对多线程最初的理解,就是并发,它的含义是原来先做烧水,再摘菜,再炒菜的工作,会变成烧水的同时去摘菜,最后去炒菜。

iOS 中的多线程

iOS中的多线程,是Cocoa框架下的多线程,通过Cocoa的封装,可以让我们更为方便的使用线程,做过C++的同学可能会对线程有更多的理解,比如线程的创立,信号量、共享变量有认识,Cocoa框架下会方便很多,它对线程做了封装,有些封装,可以让我们创建的对象,本身便拥有线程,也就是线程的对象化抽象,从而减少我们的工程,提供程序的健壮性。
 
GCD是(Grand Central Dispatch)的缩写 ,从系统级别提供的一个易用地多线程类库,具有运行时的特点,能充分利用多核心硬件。GCD的API接口为C语言的函数,函数参数中多数有Block,关于Block的使用参看这里,为我们提供强大的“接口”,对于GCD的使用参见本文
 
NSOperation与Queue
 
NSOperation是一个抽象类,它封装了线程的细节实现,我们可以通过子类化该对象,加上NSQueue来同面向对象的思维,管理多线程程序。具体可参看这里:一个基于NSOperation的多线程网络访问的项目。
 
NSThread
 
NSThread是一个控制线程执行的对象,它不如NSOperation抽象,通过它我们可以方便的得到一个线程,并控制它。但NSThread的线程之间的并发控制,是需要我们自己来控制的,可以通过NSCondition实现。
 
参看 iOS多线程编程之NSThread的使用 
 
其他多线程
 
在Cocoa的框架下,通知、Timer和异步函数等都有使用多线程,(待补充).

在项目什么时候选择使用GCD,什么时候选择NSOperation?

项目中使用NSOperation的优点是NSOperation是对线程的高度抽象,在项目中使用它,会使项目的程序结构更好,子类化NSOperation的设计思路,是具有面向对象的优点(复用、封装),使得实现是多线程支持,而接口简单,建议在复杂项目中使用。
 
项目中使用GCD的优点是GCD本身非常简单、易用,对于不复杂的多线程操作,会节省代码量,而Block参数的使用,会是代码更为易读,建议在简单项目中使用。

KVO,NSNotification,delegate及block区别

  • KVO就是cocoa框架实现的观察者模式,一般同KVC搭配使用,通过KVO可以监测一个值的变化,比如View的高度变化。是一对多的关系,一个值的变化会通知所有的观察者。
  • NSNotification是通知,也是一对多的使用场景。在某些情况下,KVO和NSNotification是一样的,都是状态变化之后告知对方。NSNotification的特点,就是需要被观察者先主动发出通知,然后观察者注册监听后再来进行响应,比KVO多了发送通知的一步,但是其优点是监听不局限于属性的变化,还可以对多种多样的状态变化进行监听,监听范围广,使用也更灵活。
  • delegate 是代理,就是我不想做的事情交给别人做。比如狗需要吃饭,就通过delegate通知主人,主人就会给他做饭、盛饭、倒水,这些操作,这些狗都不需要关心,只需要调用delegate(代理人)就可以了,由其他类完成所需要的操作。所以delegate是一对一关系。
  • block是delegate的另一种形式,是函数式编程的一种形式。使用场景跟delegate一样,相比delegate更灵活,而且代理的实现更直观。
  • KVO一般的使用场景是数据,需求是数据变化,比如股票价格变化,我们一般使用KVO(观察者模式)。delegate一般的使用场景是行为,需求是需要别人帮我做一件事情,比如买卖股票,我们一般使用delegate。
    Notification一般是进行全局通知,比如利好消息一出,通知大家去买入。delegate是强关联,就是委托和代理双方互相知道,你委托别人买股票你就需要知道经纪人,经纪人也不要知道自己的顾客。Notification是弱关联,利好消息发出,你不需要知道是谁发的也可以做出相应的反应,同理发消息的人也不需要知道接收的人也可以正常发出消息。

将一个函数在主线程执行的4种方法

1. *   GCD方法,通过向主线程队列发送一个block块,使block里的方法可以在主线程中执行。 dispatch_async(dispatch_get_main_queue(), ^{ //需要执行的方法 });
2.   
3. 
4.     * NSOperation 方法 NSOperationQueue *mainQueue = [NSOperationQueue mainQueue]; //主队列 NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ //需要执行的方法 }];
5.     [mainQueue addOperation:operation];
6.   
7. 
8.     * NSThread 方法
9. 
10.     [self performSelector:@selector(method) onThread:[NSThread mainThread] withObject:nil waitUntilDone:YES modes:nil];
11. 
12.     [self performSelectorOnMainThread:@selector(method) withObject:nil waitUntilDone:YES];
13. 
14.     [[NSThread mainThread] performSelector:@selector(method) withObject:nil];
15.     
16. 
17.     *   RunLoop方法
18. 
19.     [[NSRunLoop mainRunLoop] performSelector:@selector(method) withObject:nil];

如何让计时器调用一个类方法

  • 计时器只能调用实例方法,但是可以在这个实例方法里面调用静态方法。
  • 使用计时器需要注意,计时器一定要加入RunLoop中,并且选好model才能运行。scheduledTimerWithTimeInterval方法创建一个计时器并加入到RunLoop中所以可以直接使用。
  • 如果计时器的repeats选择YES说明这个计时器会重复执行,一定要在合适的时机调用计时器的invalid。不能在dealloc中调用,因为一旦设置为repeats 为yes,计时器会强持有self,导致dealloc永远不会被调用,这个类就永远无法被释放。比如可以在viewDidDisappear中调用,这样当类需要被回收的时候就可以正常进入dealloc中了。
1. [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
2. 
3. -(void)timerMethod
4. { //调用类方法 [[self class] staticMethod];
5. }
6. 
7. -(void)invalid
8. {
9.     [timer invalid];
10.     timer = nil;
11. }

如何重写类方法

○1、在子类中实现一个同基类名字一样的静态方法
○2、在调用的时候不要使用类名调用,而是使用[self class]的方式调用。原理,用类名调用是早绑定,在编译期绑定,用[self class]是晚绑定,在运行时决定调用哪个方法。

NSTimer创建后,会在哪个线程运行

○用scheduledTimerWithTimeInterval创建的,在哪个线程创建就会被加入哪个线程的RunLoop中就运行在哪个线程
○自己创建的Timer,加入到哪个线程的RunLoop中就运行在哪个线程。

id和NSObject*的区别

○id是一个 objc_object 结构体指针,定义是

typedef struct objc_object *id

○id可以理解为指向对象的指针。所有oc的对象 id都可以指向,编译器不会做类型检查,id调用任何存在的方法都不会在编译阶段报错,当然如果这个id指向的对象没有这个方法,该崩溃还是会崩溃的。
○NSObject *指向的必须是NSObject的子类,调用的也只能是NSObjec里面的方法否则就要做强制类型转换。
○不是所有的OC对象都是NSObject的子类,还有一些继承自NSProxy。NSObject *可指向的类型是id的子集。

static关键字的作用

回答一:
1.在函数体内定义的static他的作用域为该函数体,该变量在内存中只被分配一次,因此,其值在下次调用时仍维持上次的值不变;
2.在模块内的static全局变量可以被模块内所有函数访问,但是不能被模块外的其他函数访问;
3.在模块内的staic全局变量可以被这一模块内的其他函数调用,这个函数的使用范围被限制在这个模块内;
4.在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝,也就是说只要是该类的对象,那么该对象的中被static修饰的成员变量都指向同一块地址。

回答二:
修饰局部变量:
1.延长局部变量的生命周期,程序结束才会销毁。
2.局部变量只会生成一份内存,只会初始化一次。
3.改变局部变量的作用域。
 
修饰全局变量:
1.只能在本文件中访问,修改全局变量的作用域,生命周期不会改
2.避免重复定义全局变量

在OC中static关键字使用误区

1.使用static修饰实例变量是不被允许的
2.使用static修饰了方法,也是错误的
参考:
如何正确使用const,static,extern
OC中 static 与 const 的作用

使用 Swift 语言编程的优缺点 总的来说,我认为使用 Swift 来作为编程语言的优点还是要远远大于缺点的,而且很多缺点苹果也在逐渐改善。

优点:

1、简洁的语法
2、更强的类型安全
3、函数式编程的支持
   Swift 语言本身提供了对函数式编程的支持。
   Objc 本身是不支持的,但是可以通过引入 ReactiveCocoa 这个库来支持函数式编程。
4、编写 OS X 下的自动化脚本

缺点

1、App体积变大
    使用Swift 后, App 体积大概增加 5-8 M 左右,对体积大小敏感的慎用。
    体积变大的原因是因为 Swift 还在变化,所以 Apple 没有在 iOS 系统里放入 Swift 的运行库,反而是每个 App 里都要包含其对应的 Swift 运行库。
2、Xcode 支持不够好
    如果你是使用 Xcode经常卡住或者崩溃想必你是肯定碰到过了,这个是目前使用 Swift 最让人头疼的事情,即使是到现在XCode 9, 有时候也会遇到这种问题,所以要看你的承受力了……
3、第三方库的支持不够多
    目前确实 Swift 编写的第三方库确实不多,但可以通过桥接的方式来使用 Objc 的三方库,基本上没有太大问题。现在已经改善很多了…
4、语言版本更新带来的编译问题
   语言本身还在发展,所以每次版本更新后都会出现编译不过的情况(至少到目前为止还是),但是自从 4.0 版本发布后,改动没有 beta 时候那么大了,而且根据 Xcode 提示基本就可以解决语法变动导致的编译问题了。

青山不改,绿水常流

iOS Swift开发面试题总结

2023年5月6日 10:46

Swift 优点 (相对 OC)

  • Swift 更加安全,是类型安全的语言
  • 代码少,语法简洁,可以省去大量冗余代码
  • Swift 速度更快,运算性能更高,(Apple 专门对编译器进行了优化)

Swift 中 类(class) 和 结构体(struct) 的区别,以及各自优缺点?

  • 类:
    • 引用类型
      • 在进行变量赋值时,是通过指针copy,属于浅拷贝(shallow copy)
      • 数据的存储是在堆空间
    • 可以被继承(前提是类没有被 final 关键字修饰),子类可以使用父类的属性和方法
    • (当class继承自 Object,拥有runtime机制)类型转换可以在运行时检查和解释一个实例对象
    • 用 deinit(析构函数)来释放资源 类似OC(dealloc)
    • 类的方法地址 是不确定的,只有在具体运行时,才能确定调用的具体值
  • 结构体
    • 值类型
      • 在进行变量赋值是,是深拷贝(deep copy),产生了新的副本
      • 数据的存储时在栈空间(大部分情况下,不需要考虑内存泄露问题,栈空间的特点是用完即释放)
    • 结构体调用方法,在编译完成就可以确定方法具体的地址值,以便直接调用

综上,在满足程序要求的情况下 优先使用 结构体


Swift中strong 、weak和unowned是什么意思?二者有什么不同?何时使用unowned?

Swift 的内存管理机制与 Objective-C一样为 ARC(Automatic Reference Counting)。它的基本原理是,一个对象在没有任何强引用指向它时,其占用的内存会被回收。反之,只要有任何一个强引用指向该对象,它就会一直存在于内存中。

  • strong 代表着强引用,是默认属性。当一个对象被声明为 strong 时,就表示父层级对该对象有一个强引用的指向。此时该对象的引用计数会增加1。

  • weak 代表着弱引用。当对象被声明为 weak 时,父层级对此对象没有指向,该对象的引用计数不会增加1。它在对象释放后弱引用也随即消失。继续访问该对象,程序会得到 nil,不亏崩溃

  • unowned 与弱引用本质上一样。不同的是,unowned 无主引用 实例销毁后仍然存储着实例的内存地址(类似于OC中的unsafe_unretained), 试图在实例销毁后访问无主引用,会产生运行时错误(野指针)

  • weak unowned 只能用在 类实例上面

  • weakunowned 都能解决 循环引用,unowned 要比 weak 性能 稍高

    • 在生命周期中可能会 变成 nil 的使用 weak
    • 初始化赋值以后再也不会变成 nil 使用 unowned

Swift 中什么是可选类型?

  • Swift中可选类型为了表示 一个变量 允许为 空(nil)的情况
  • 类型名称后 加 ? 定义 可选项
  • 选项的本质是 枚举类型

Swift 中什么 是 泛型?

  • 跟JS和Dart 类似,泛型 可以将类型 参数化,提高代码复用率,减少代码量
  • Swift泛型函数 并不会 在底层 生成 若干个 (匹配类型)函数 ,产生函数重载,而是: 在函数调用时,会将 参数的类型 传递给 目标函数
  • Swift泛型应用在协议上时,需要使用关联类型(associatedtype)

怎么理解 Swift中的泛型约束

泛型约束 可以 更精确的知道 参数 需要 遵循什么标准

//someT遵循的是某个class,someU遵循的是某个协议,这样在传参的时候明确参数类型
func someFunction<T: SomeClass, U: SomeProtocol>(someT: T, someU: U) {
    // 这里是泛型函数的函数体部分
}

Swift 中 static 和 class 关键字的区别

在 Swift 中 static 和 class 都是表示「类型范围作用域」的关键字。

在所有类型(class[类]、struct、enum )中使用

  • static 修饰都可以表示类方法类与属性(包括存储属性和计算属性)。
  • class 是专门用在 calss 类型中修饰类方法和类的计算属性(注意:无法使用 class 修饰存储属性)。

结构体只能用 static 修饰 类方法或属性

class类型 static class 的区别

在 class 类型中 static 和 class 都可以表示类型范围作用域,那区别是

  1. class 无法修饰存储属性,而 static 可以。
  2. 使用 static 修饰的类方法和类属性无法在子类中重载。也就是说 static 修饰的类方法和类属性包含了 final 关键字的特性。相当于 final class 。

一般在 protocol定义一个类方法或者类计算属性推荐使用 static 关键字来修饰。使用 protocol 时,在 struct 和 enum 中仍然使用 static,在 class 类型中 class 和 static 关键字都可以使用。


Swift 中的模式匹配?

模式 是用于 匹配的规则,比如 switch 的 case 、捕捉错误的 catch 、 if guard while for 语句条件等


Swift 中的访问控制?

Swift 提供了 5个 不同的访问级别,从高到低排列如下:

  • open
    • 允许在任意模块中访问、继承、重写
    • open 只能用在 类 、 类成员上
  • public
    • 允许在任意模块中访问
    • 修饰类时不允许其他模块进行继承、重写
  • internal - 默认
    • 只允许在定义实体的模块中访问,不允许在其他模块中访问
  • fileprivate
    • 只允许在定义实体的源文件中访问
  • private:
    • 只允许在定义实体的封闭声明中(作用域)访问

怎么理解 copy - on - write? 或者 理解Swift中的写时复制

值类型在复制时,复制对象 与 原对象 实际上在内存中指向同一个对象,**当且仅当 ** 修改复制的对象时,才会在内存中创建一个新的对象,

  • 为了提升性能,Struct, String、Array、Dictionary、Set采取了Copy On Write的技术
  • 比如仅当有“写”操作时,才会真正执行拷贝操作
  • 对于标准库值类型的赋值操作,Swift 能确保最佳性能,所有没必要为了保证最佳性能来避免赋值

原理

在结构体内部用一个引用类型来存储实际的数据,

  • 在不进行写入操作的普通传递过程中,都是将内部的reference的应用计数+1,
  • 当进行写入操作时,对内部的 reference 做一次 copy 操作用来存储新的数据;防止和之前的reference产生意外的数据共享。

swift中提供[isKnownUniquelyReferenced]函数,他能检查一个类的实例是不是唯一的引用,如果是,我们就不需要对结构体实例进行复制,如果不是,说明对象被不同的结构体共享,这时对它进行更改就需要进行复制。

Swift 为什么将 Array,String,Dictionary,Set,设计为值类型?

值类型 相比 引用类型的优点

  • 值类型和引用类型相比,最大优势可以高效的使用内存;
  • 值类型在栈上操作,引用类型在堆上操作;
  • 栈上操作仅仅是单个指针的移动,
  • 堆上操作牵涉到合并,位移,重链接

Swift 这样设计减少了堆上内存分配和回收次数,使用 copy-on-write将值传递与复制开销降到最低

String,Array,Dictionary设计成值类型,也是为了线程安全考虑。通过Swift的let设置,使得这些数据达到了真正意义上的“不变”,它也从根本上解决了多线程中内存访问和操作顺序的问题


什么是属性观察器?

属性观察是指在当前类型内对特性属性进行监测,并作出响应,属性观察是 swift 中的特性,具有2种, willset和 didset

  • willSet 传递新值 newValue

  • didSet 传递旧值 oldValue

  • 在初始化器中对属性初始化时,不会触发观察器

  • 属性观察器 只能用在 存储属性 ,不可用在 计算属性

  • 可以 为  lazy (即延迟存储属性)的 var 存储属性 设置 属性观察器

  • willSet 会传递新值,默认叫 newValue
  • didSet 会传递旧值,默认叫 oldValue

注意:

  • 在初始化器中设置属性值不会触发 属性观察器
    • 属性定义时设置初始值也不会出发 属性观察,原因是
      • 属性定义时设置初始值,本质跟在 初始化器中设置值是一样的
  • 属性观察器 只能用在 存储属性 ,不可用在 计算属性
struct Cicle {
    /// 存储属性
    var radius :Double {
        willSet {
            print("willSet -- ",newValue,"radius == ",radius)
        }
        didSet {
            print("didSet ++ ",oldValue,"radius == ",radius)
        }
    }
    /*
     上述代码 跟下面等价,不推荐
     var radius :Double {
         willSet(jk_newValue) {
             print("willSet -- ",jk_newValue,"radius == ",radius)
         }
         didSet(jk_oldValue) {
             print("didSet ++ ",jk_oldValue,"radius == ",radius)
         }
     }
     */
}

var circle = Cicle.init(radius: 10.0)

circle.radius = 20.0
/*
 willSet --  20.0 radius ==  10.0
 didSet ++  10.0 radius ==  20.0
 */

print("result == ",circle.radius)
//result ==  20.0

拓展:
●属性观察器,计算属性这两个功能,同样可以应用在全局变量/局部变量
●属性观察器,计算属性 不可以同时 应用在同一个类(不包括继承)的属性中

Swift 异常捕获

do - try - catch 机制

  • Swift中可以通过 Error 协议自定义运行时的错误信息
  • 函数内部通过throw 抛出 自定义  Error , 抛出 Error 的函数必须加上 throws 声明(逻辑通过不会抛出,反之可能抛出)
  • 需要使用 try 调用 可能会 抛出 的Error 函数
  • 通过 try 尝试调用 函数 抛出的异常 必须要 处理异常;否在会编译报错; 反之 运行时 在 top level (main) 报错,闪退

defer 的用法

  • 使用defer代码块来表示在函数返回前,函数中最后执行的代码。无论函数是否会抛出错误,这段代码都将执行。

如何将Swift中协议 部分方法 设计成可选?

  • 方案一(不推荐,除非需要暴露给OC用)
    • 在协议和方法前面添加 @objc,然后在方法前面添加 optional关键字,改方式实际上是将协议转为了OC的方式
@objc protocol someProtocol {
  @objc  optional func test()
}

协议 可以 用来定义 属性 方法 下标 的 声明,协议 可以被  类 结构体 枚举 遵守(多个协议之间用, 隔开)

protocol Drawable {
    func draw()
    var x:Int { get set }
    var y:Int { get }
    subscript(index:Int) -> Int {get set}
}

protocol Test1 {}
protocol Test2 {}
protocol Test3 {}

class TestClass: Test1,Test2,Test3 {
    
}

注意:

  • 协议中定义的方法,不能有默认参数
  • 默认情况下,协议中定义的内容需要全部实现

Swift和OC中的 protocol 有什么不同?

  • 相同点,两者都是用作代理
  • 不同点
    • Swift
      • Swift中的 protocol 还可以对接口进行抽象,可以实现面向协议,从而大大提高编程效率
      • Swift中的 protocol 可以用于值类型,结构体,枚举;

比较Swift 和OC中的初始化方法 (init) 有什么不同?

swift 的初始化方法,因为引入了两段式初始化和安全检查因此更加严格和准确,

swift初始化方法需要保证所有的非optional的成员变量都完成初始化,

同时 swfit 新增了convenience和 required两个修饰初始化器的关键字

  • convenience只提供一种方便的初始化器,必须通过一个指定初始化器来完成初始化
  • required是强制子类重写父类中所修饰的初始化方法

Swift 和OC 中的自省 有什么区别?

  • OC
    • 自省在OC中就是判断某一对象是否属于某一个类的操作,有以下2中方式
      • [obj iskinOfClass:[SomeClass class]] : obj 必须是 SomeClass 的 对象或 其子类对象;return YES;
      • [obj isMemberOfClass:[SomeClass class]] : obj 必须是 SomeClass 的 对象;return YES;
  • Swift
    • Swift 中由于很多 class 并非继承自 NSObject, 故而 Swift 使用 is 来判断是否属于某一类型, is 不仅可以作用于class, 还可以作用于enum和struct

Swift 与 OC 如何相互调用

  • Swift -> OC
    • 需要创建一个 Target-BriBridging-Header.h (默认在OC项目中,会提示自动创建)的桥文件,在该文件中,导入需要调用的OC代码的头文件即可
  • OC -> Swift
    • 直接导入Target-Swift.h(该文件是Xcode自动创建) Swift如何需要被OC调用,需要使用 @objc 对方法或属性进行修饰

Swift 中特殊的标记

imagepng

Swift调用OC
●新建一个桥接文件,文件格式默认为:{targetName}-Bridging-Header.h;(一般在OC项目中,创建Swift文件,Xcode会自动提示生成该文件,仅需点击确认即可)

imagepng

●在{targetName}-Bridging-Header.h 文件中 #import OC 需要 暴露 给 Swift的内容

OC 调用 Swift
●Xcode 已经默认 生成 一个 用于 OC 调用 Swift的头文件,文件名格式是: {targetName}-Swift.h

imagepng

●Swift 暴露给 OC的 类 一定要继承 NSObject

●使用 @objc 修饰 需要暴露 给 OC的成员

●使用@objcMembers 修饰类
○代表 默认所有的 成员 都会 暴露给 OC(包括扩展中定义的成员)
○最终 是否成功 暴露,还需要考虑 成员自身的 访问级别

拓展
●为什么Swift 暴露给 OC 的类 要最终 继承 NSObject?
因为 OC 中的方法调用 是通过 Runtime 机制,需要通过 isa 指针 去完成 一些列消息的发送等, 而 只有继承自 NSObject 的类 才具有 isa 指针,才具备 Runtime 消息 发送的能力
●p.run() 底层是如何调用的? 反过来,OC调用Swift 又是如何调用?
○JKPerson 是 OC 的类,以及OC 中定义的初始化 和 run 方法
○在Swift中 调用 JKPerson 对象的 run 方法 ,底层是如何调用的?

Swift复制代码

var p = JKPerson(age: 10,name:"Jack")
p.run()

答:走 Runtime 运行时机制, 反过来 OC 调用 Swift中的类 跟 问题一 一样,也是通过 Runtime 机制
●car.run() 底层是如何调用的?

swift

答 : (虽然 Car 类 被暴露给 OC使用)在Swift中 car.run(),底层依然是 通过 类似 C++ 的虚表 机制 来调用的;

拓展:
如果想要 Swift中的方法 调用 也使用 Runtime 机制,需要在方法名称前面 加上 dynamic关键字


Swift定义常量 和 OC定义常量的区别?

//OC:
const int price = 0;
//Swift:
let price = 0
  • OC中 const 常量类型和数值是在编译时确定的
  • Swift 中 let 常量(只能赋值一次),其类型和值既可以是静态的,也可以是一个动态的计算方法,它们在运行时确定的。

Swift 中的函数重载

构成函数重载的规则

  • 函数名相同
  • 参数个数不同 || 参数类型不同 || 参数标签不同

注意: 返回值类型 与函数重载无关;返回值类型不同时,函数重载会报错:

func overloadsum(v1 : Int,v2:Int) -> Int {
    v2 + v1
}

// 参数个数不同
func overloadsum(v1 : Int,v2:Int,v3:Int) -> Int {
    v2 + v1 + v3
}

// 参数类型不同
func overloadsum(v1 : Int,v2:Double) -> Double {
    v2 + Double(v1)
}

// 参数标签不同
func overloadsum(_ v1 : Int,_ v2:Int) -> Int {
    v2 + v1
}

func overloadsum(a : Int,_ b:Int) -> Int {
    a + b
}

/**
 返回值类型不同时,在函数重载时,会报错:
 Ambiguous use of 'overloadsum(v1:v2:)'
 
 func overloadsum(v1 : Int,v2:Int) {
 }
 */

public func overloadtest() {
    let result1 = overloadsum(v1: 10, v2: 20)
    let result2 = overloadsum(v1: 10, v2: 20, v3: 30)
    let result3 = overloadsum(v1: 10, v2: 20)
    let result4 = overloadsum(10, 20)
    let resutt4_1 = overloadsum(a: 10, 20)
    
    print(result1,result2,result3,result4,resutt4_1)
    //30 60 30 30 30
}


Swift 中的枚举,关联值 和 原始值的区分?

    • 将 枚举的成员值 跟 其他类型的值 关联 存储在一起
    • 存储在枚举变量中,占用枚举变量内存
enum Score {
    case points(Int)
    case grade(Character)
}
    • 枚举成员可以使用相同类型的默认值预先关联,这个默认值叫做:原始值
    • 不会存储在 枚举变量中,不占用枚举变量内存
enum PokerSuit : Character {
    case spade = "♠"
    case heart = "♥"
    case diamond = "♦"
    case club = "♣"
}

闭包是引用类型吗?

闭包和函数都是是引用类型。如果一个闭包被分配给一个变量,这个变量复制给另一个变量,那么他们引用的是同一个闭包,他们的捕捉列表也会被复制。

swift 中的闭包结构是什么样子的?

{
    (参数列表) -> 返回值类型 in 函数体代码
}

什么是尾随闭包

  • 尾随闭包 是一个被 书写在 函数调用 括号 后面的 闭包表达式

基本定义
●Swift中可通过 func 定义一个函数,也可以通过 闭包表达式 定义一个函数

闭包表达式的格式:

{
    (参数列表) -> 返回值类型  in
    函数体代码
}

闭包表达式与定义函数的语法相对比,有区别如下:
1没有func
2没有函数名
3返回值类型添加了关键字in

let fn1 = {
    (v1 : Int,v2 : Int) -> Int in
    return v1 + v2
}

let result1 = fn1(10,5)

let result2 = {
    (v1:Int,v2:Int) -> Int in
    return v2 + v1
}(10,6)

print(result1,result2) // 15 16

闭包表达式的简写

func exec(v1:Int,v2:Int,fn:(Int,Int)->Int) {
    print(fn(v1,v2))
}

闭包表达式的简写
private func test2() {
    // 1: 没有简写
    exec(v1:10, v2:20, fn: {
        (v1:Int,v2:Int) -> Int in
        return v1 + v2
    })
    
    // 2: 简写1
    exec(v1: 2, v2: 3, fn: {
        v1,v2 in return v1 + v2
    })
    
    // 3:简写 2
    exec(v1: 3, v2: 4, fn: {
        v1,v2 in v1 + v2
    })
    
    // 4: 简写3
    exec(v1: 5, v2: 6, fn: {$0 + $1})
    
    // 5: 简写4
    exec(v1: 7, v2: 8, fn: +)
}

尾随闭包

  • 将一个很长的闭包表达式作为函数的最后一个实参,使用尾随闭包可以增强代码的可读性
    • 尾随闭包 是一个被 书写在 函数调用 括号 后面的 闭包表达式
func test3() {
    
    exec(v1: 8, v2: 7) { a, b in
        a + b
    }
    
    // or     { 书写在 函数调用 括号 后面的 闭包表达式}
    exec(v1: 9, v2: 10) {
        $0 + $1
    }
}
  • 如果 闭包表达式 是函数的唯一 实参,且使用了尾随闭包的 语法,则在函数名后面的 () 可省略
// fn:就是尾随闭包
func exec1(fn:(Int,Int)->Int) {
    print(fn(1,2))
}

exec1(fn: {$0 + $1})
exec1() {$0 + $1}
exec1{$0 + $1}

什么是逃逸闭包

  • 闭包有可能在函数结束后调用,闭包调用 逃离了函数的作用域,需要通过@escaping 声明

注意:逃逸闭包 不可以 捕获 inout 参数

原因是: 逃逸闭包不确定 何时开始执行,有可能 在执行逃逸闭包时,可变参数已经被程序回收,造成野指针访问

什么是自动闭包

自动闭包是一种自动创建的用来把作为实际参数传递给函数的表达式打包的闭包。

它不接受任何实际参数,并且当它被调用时,它会返回内部打包的表达式的值。

这个语法的好处在于通过写普通表达式代替显式闭包而使你省略包围函数形式参数的括号。

func getFirstPositive(_ v1: Int, _ v2: @autoclosure () -> Int) -> Int? {
    return v1 > 0 ? v1 : v2()
}
getFirstPositive(10, 20)
  • 为了避免与期望冲突,使用了@autoclosure的地方最好明确注释清楚:这个值会被推迟执行
  • @autoclosure 会自动将 20 封装成闭包 { 20 }
  • @autoclosure 只支持 () -> T 格式的参数
  • @autoclosure 并非只支持最后1个参数
  • 有@autoclosure、无@autoclosure,构成了函数重载

如果你想要自动闭包允许逃逸,就同时使用 @autoclosure 和 @escaping 标志。


Swift 中的存储属性与计算属性

存储属性(Stored Property)

  • 类似于成员变量这个概念
  • 存储在实例对象的内存中
  • 结构体、类可以定义存储属性
  • 枚举不可以定义存储属性

关于 存储属性, Swift 中有个明确的规定

  • 在创建 类 或者 结构体 的实例时,必须为所有的存储属性设置一个合适的初始值
    • 可以在初始化器里为存储属性设置一个初始值
    • 可以分配一个默认的属性值作为属性定义的一部分

计算属性(Computed Property)

  • 本质就是方法(函数)
  • 不占用实例对象的内存
  • 枚举、结构体、类都可以定义计算属性

计算属性(Computed Property)
○本质就是方法(函数)
○不占用实例的内存
○枚举、结构体、类 都可以定义计算属性

理解计算属性与存储属性:

如果两个属性之间存在一定的逻辑关系,使用计算属性,原因如下:
●如果都用存储属性的话,其逻辑对应关系可能有误
●而使用计算属性,则可以准确的描述 这种逻辑关系
具体案例参考 下面的 Cicle 中的 radius(半径) 和 diameter(直径) 的逻辑关系

同时因为计算属性不占用实例的内存,可以有效的节省实例的内存空间

●set 传入的新值 默认叫做 newValue,也可以自定义

struct Cicle {
    /// 存储属性
    var radius :Double
    /// 计算属性
    var diameter: Double {
        get {
            radius * 2
        }
        set (jkNewValue){
            radius = jkNewValue / 2.0
        }
    }
}
  • 只读计算属性:只有 get , 没有 set
struct Cicle {
    /// 存储属性
    var radius :Double
    /// 计算属性
    /*
     var diameter: Double {
         get {
             radius * 2
         }
     }
     */

    // 上述代码与下面的代码等价
    var diameter: Double {radius * 2}
}

var cicle = Cicle.init(radius: 12)

print(cicle.radius)//12.0
print(cicle.diameter)//24.0

// cicle.diameter = 10.0 //Cannot assign to property: 'diameter' is a get-only property
  • 定义计算属性只能用 var,不可以是 let
    • let 表示常量
    • 计算属性的值是可能会发生变化的(包括只读计算属性)

什么是[延迟存储属性](Lazy Stored Property)

  • 使用 lazy 可以定义一个 延迟存储属性,在第一次用到属性的时候才会进行初始化(类似 OC 中的懒加载)

注意点:

  • lazy 属性 必须是 var
    • 因为,let 属性 必须在实例的初始化方法 完成之前 就拥有值
  • 如果多条线程同时第一次访问 lazy 属性,无法保证 属性 只被 初始化 一次 (即线程不是安全的)

  • 使用 lazy 可以定义一个 延迟存储属性,在第一次用到属性的时候才会进行初始化(类似 OC 中的懒加载)

注意点:

  • lazy 属性 必须是 var
    • 因为Swift 规定:let 属性 必须在实例的初始化方法 完成之前 就拥有值
  • 如果多条线程同时第一次访问 lazy 属性,无法保证 属性 只被 初始化 一次 (即线程不是安全的)
class Car {
    init() {
        print("car has init")
    }

    func run() {
        print("car running")
    }
}

class Person {
    lazy var car = Car.init()

    init() {
        print("person has init")
    }

    func go_out() {
        car.run()
    }
}

let p = Person.init() //person has init
print("*******")
p.go_out()//  car has init ---->   car running
  • 当结构体 包含 一个延迟存储属性时,只有 var 才能访问延迟 存储属性
    • 因为延迟属性 初始化时 要改变 结构体的内存
struct Point {
    var x = 0
    var y = 0
    lazy var z = 0
}

let p = Point.init()
print(p.z)//Cannot use mutating getter on immutable value: 'p' is a 'let' constant

什么是 类型 属性?

  • 类型属性(Type Property) :通过类型去访问
    • 存储类型属性(Stored Type Property):整个程序运行过程中,就只有1份内存(类似于全局变量,底层采用了 gcd_once 操作)
    • 计算类型数据(Computed Type Property):

属性可分为
●实例属性(Instance Property):通过实例去访问
○存储实例属性(Stored Instance Property):存储在实例的内存中,每个实例都有1分
○计算实例属性(Computed Instance Property):不占用实例的内存,本质是方法

●类型属性(Type Property) :通过类型去访问
○存储类型属性(Stored Type Property):整个程序运行过程中,就只有1份内存(类似于全局变量,底层采用了 gcd_once 操作,保证只初始化一次)
○计算类型数据(Computed Type Property):

注意:
存储类型属性不会 占用 实例对象 的内存,整个程序运行过程中,就只有1份内存

存储类型属性 本质就是全局变量(该全局变量加了一些类型控制,只能通过类型去访问),

可以通过 static 定义类型属性
如果 是类, 也可以使用 关键字 class
结构体 就只能使用 关键字 static

struct Car {
    static var count:Int = 0
    init(){
        Car.count += 1
    }
}

let c1 = Car.init()
let c2 = Car.init()
let c3 = Car.init()
print(Car.count)//3

类型属性的细节:
●不同于 存储实例属性 ,必须给 存储类型属性 设定初始值
○因为类型没有想实例那样的 init 初始化器来初始化 存储属性
●存储类型属性 默认就是 lazy。会在第一次使用的时候 初始化
○就算 被 多个线程 同时访问,保证只会被 初始化 一次,线程是安全的
○存储类型 属性 也可以是 let
●枚举类型 也可以 定义 类型属性(存储类型属性,计算类型属性)


Swift 中如何定义单例模式

可以通过类型属性+let+private 来写单例; 代码如下如下:

// 方式一
public class SingleManager{
    public static let shared = SingleManager()
    private init(){}
}

// 方式二
public class SingleManager{
    public static let shared = {
        //...
        //...
        return SingleManager()
    }()
    private init(){}
}

// 上述两个方法等价,一般推荐 方式二

Swift 中 下标是什么?

  • 使用subscript 可以给任意类型(枚举,结构体,类) 增加下标的功能

subscript 的语法 类似于 实例方法、计算属性,本质就是方法(函数)

func xiabiaoTest() {
    class Point {
        var x = 0.0
        var y = 0.0
        
        subscript (index:Int) -> Double {
            set{
                if index == 0 {
                    x = newValue
                } else if index == 1 {
                    y = newValue
                }
            }
            get{
                if index == 0 {
                    return x
                } else if index == 1 {
                    return y
                }
                return 0
            }
        }
    }
    
    let p = Point()
    p[0] = 11.1 // 调用subscript
    p[1] = 22.2 // 调用subscript
    print(p.x)//11.1 不会调用 subscript
    print(p.y)//22/2 不会调用 subscript
    print(p[0])//11.1 // 调用subscript
    print(p[1])//22.2 // 调用subscript
}

简要说明Swift中的初始化器?

一图胜千言 针对类

  • 结构体会默认生成 含有参数的初始化器,一旦自定义初始化器,默认的初始化器则不可用
  • 类默认只会生成无参的指定初始化器

11

  • 类、结构体、枚举都可以定义初始化器
  • 类有2种初始化器: 指定初始化器(designated initializer)、便捷初始化器(convenience initializer)

什么是可选链?

可选链是一个调用和查询可选属性、方法和下标的过程,它可能为 nil 。

  • 如果可选项包含值,属性、方法或者下标的调用成功;
  • 如果可选项是 nil ,属性、方法或者下标的调用会返回 nil 。
  • 多个查询可以链接在一起,如果链中任何一个节点是 nil ,那么整个链就会得体地失败。

可选链(Optional Chaining)

如果 可选项 不会nil ,调用 方法 ,下标,属性成功,结果会被包装成 可选项,反之调用失败,返回nil

class Car { var price = 0}
class Dog { var weight = 0}
class Person {
    var name:String = ""
    var dog :Dog = Dog()
    var car :Car? = Car()
    func age() -> Int { 18 }
    func eat() {print("Person Eat")}
    subscript (index:Int) ->Int {index}
}


var person :Person? = Person()
var age1 = person!.age() //Int
var age2 = person?.age() // Int?
var name = person?.name // String?
var index = person?[6] // Int?


func getName() -> String {"jackie"}

/*
 如果 person 对象 是 nil  ,将不会调用 getName() 方法
 */
person?.name = getName()

  • 如果结果本来就是可选项,不会进行再次包装
if let _ = person?.eat() {
    /*
    Person Eat
eat success
    */
    print("eat success")
} else {
    print("eat failure")
}
  • 多个  可以连接在一起,其中任何一个节点 如果为 nil,那么整条链就会 调用失败
var dog = person?.dog // Dog?
var weight = person?.dog.weight // Int?
var price = person?.car?.price // Int?

什么是运算符重载?

类、结构体、枚举可以为现有的运算符提供自定义的实现,这个操作叫做:运算符重载

struct Point {
    var x: Int,y: Int
    static func + (p1: Point,p2: Point) -> Point {
        Point(x: p1.x + p2.x ,y: p1.y + p2.y)
    }
}
let p = Point(x:10,y: 20) + Point(x: 30,y: 40)
print(p) //Point(x: 40, y: 60)

Swift中函数的柯里化

将一个 接受 多个参数的 函数,变成 只接受 一个参数的一系列 操作

示例

func add(_ v1: Int,_ v2: Int) -> Int {
    v1 + v2
}

func difference(_ v1: Int,_ v2: Int) -> Int {
    v1 - v2
}

func add2(_ v1: Int,_ v2: Int,_ v3 :Int ,_ v4 :Int) -> Int {
    v1 + v2 - v3 + v4
}
  • 伪柯里化
func currying_add(_ v1:Int) -> (Int) -> Int {
    return {$0 + v1}
}

/*
 将任意一个 接受两个 参数的函数 柯里化
 */
func curring_fun_tow_params1(_ f1 :@escaping (Int,Int) -> Int, _ v1 :Int) -> (Int) -> Int {
//    return {
//        f1($0,v1)
//    }
    return { (v2) in
        return f1(v1 , v2)
    }
}

print(add(10, 20)) // 30
print(currying_add(10)(20)) // 30 // 30
print(curring_fun_tow_params1(add, 10)(20))
  • 正宗柯里化
func curring_fun_two_params2<A,B,C>(_ f1: @escaping (A,B) -> C) -> (A) -> ((B) -> C) {
    /*
     return {
         (a) in  // a = 3
         return {
             (b) in  // b = 8
             return  f1(a,b)
         }
     }
     */
    
    { a in { b in f1(a, b)} }
    
}

let result = curring_fun_two_params2(add)(3)(5)
print("result == ",result) //8
  • 柯里化拓展

/*
 -> (A) -> (B) -> (C) -> (D) -> E
 
 实际是 一连串 闭包的 组合  如下所示:
 
 -> (A) -> (  (B) ->    ((C)  ->   ((D) -> E))  )
 
 
 传入 A   >  一个 闭包    (B)   ->   (  (C) -> ((D) -> E)  )

 
 传入 B   >>  一个 闭包   (C)   ->   (  (D) -> E  )
 
 
 传入 C   >>  一个闭包    (D) -> E
 
 
 */


//func curring_fun_more_params<A,B,C,D,E>(_ fn: @escaping (A,B,C,D) -> (E)) -> (A) -> ((B) -> ((C) -> ((D) -> E))) {
  func curring_fun_more_params<A,B,C,D,E>(_ fn: @escaping (A,B,C,D) -> (E)) -> (A) -> (B) -> (C) -> (D) -> E {
   /*
    return {
        (a) in
        return {
            (b) in
            return {
                (c) in
                return {
                    d in
                    return fn(a,b,c,d)
                }
            }
        }
    }
    */
    
    {a in { b in { c in { d in fn(a,b,c,d)}}}}
}

let resutl2 = curring_fun_more_params(add2)(10)(20)(30)(40)
print(resutl2) // 40

let resutl2_func = curring_fun_more_params(add2)
let resutl2_func_value = resutl2_func(10)(20)(30)(40)
print(resutl2_func_value) // 40

记录一次swift函数式编程的探讨

2023年4月22日 18:02

本文适合哪些人?

本文针对的是已经有一部分Swift开发的基础,同时对函数式范式比较感兴趣的开发者。 当然,如果只对函数式范式感兴趣,我觉得这篇文章也值得一看。

函数式编程是什么?

首先来看这个词语”Functional Programming“,它是什么?

当需要去查一个专业术语的定义的时候,我的第一反应是来查询Wikipedia:

In computer science, fucnitonal programming is a programming paradigm where programs are constructed by applying and composing fucntions.

在这个定义里,有一个很熟悉的词——programming paradigm, 一般翻译为编程范式,可是我对这个翻译还是有些迷糊,于是我又在wikipedia中查找这个词语的含义:

Programming paradigms are a way to classify programming languages based on their features.

编程范式(编程范例)是一种基于语言自身的特性来给编程语言分类的方式。

同时wikipedia中还总结了常见的编程范式的分类:

  • imperative
    • procedural
    • object-oriented
  • declarative
    • functional
    • logic
    • mathematical
    • reactive

那么究竟什么是编程范式呢?我们知道编程是一门工程学,它的目的是去解决问题,而解决问题可以有很多的方法,编程范例就是代表着解决问题的不同思路。如果说我们是编程世界的造物主的话,那么编程范例应该就是我们创造这个世界的方法论。所以我非常喜欢台湾那边对programming paradigm 的翻译:程式設計法。

为什么我要强调编程范例是什么东西,而且还分门别类的列举了出来这些编程范例呢?

因为编程本身是抽象的,编程范例其实就是我们如何抽象这个世界的方法,我只是想通过这个具体的定义来说明函数式本身就是一种方法论。 所以我们学习的时候没必要害怕它,遇到引用透明,副作用,科里化,函子,单子,惰性求值等等等等这些概念的时候,畏惧的原因只是不熟悉而已,就想我们学习面向对象的时候:继承,封装,多态,动态绑定,消息传递等等等等,这些概念我们一开始也不熟悉,所以当我们熟悉了函数式这些概念的时候,一切自然水到渠成。 在我们熟悉的面向对象的编程范式中,我们知道它的思想是:一切皆对象,而在纯函数式的编程范式中,可以说:一切皆函数。在函数式编程中,函数是一等公民,那什么是一等公民呢?就是它可以作为参数,返回值,也可以赋值给变量,也就是说它的地位其实是和Int,String, Double等基本类型是一样的,换言之,要像使用基本类型一样去使用它!

不同的思想就是创建世界的方法论的不同之处,这里我举个例子,那就是状态,比如登录的各种状态,维护状态会大大增加系统的复杂性,特别是状态很多的时候,而且引入状态这个概念之后,会带来很多复杂的问题:状态持久化,环境模型等等等,而如果使用面向对象的编程范例,可以将每一个状态都定义为一个对象如C#中的状态机的实现,而在函数式编程里呢? 在SICP中提到,状态是随着时间改变的,所以状态是否可以使用f(t)来表示呢?这就是使用函数式的思路来抽象状态。

当然,我这里并不是说只能使用一种编程范式,我也并不鼓吹函数式就一直是好的,但是掌握函数式可以让我们在解决问题的时候提供更多的选择,更有效率的解决问题,事实上,我们解决问题(创造世界)肯定会使用很多种方法论即多种编程范式,一般情况下,更现代的编程语言都支持多范式编程,这里用swift里的RxSwift来举例:

public class Observable<Element> : ObservableType {
    internal init()
    
    public func subscribe<Observer>(_ observer: Observer) -> Disposable where Element == Observer.Element, Observer : RxSwift.ObserverType

    public func asObservable() -> Observable<Element>
}

// 观察者
final internal class AnonymousObserver<Element> : ObserverBase<Element> {

    internal typealias EventHandler = (Event<Element>) -> Void

    internal init(_ eventHandler: @escaping EventHandler)

    override internal func onCore(_ event: Event<Element>)
}



extension ObservableType {
    public func flatMap<Source>(_ selector: @escaping (Element) throws -> Source) -> Observable<Source.Element> where Source : RxSwift.ObservableConvertibleType
}

extension ObservableType {
    public func map<Result>(_ transform: @escaping (Element) throws -> Result) -> Observable<Result>
}

它的Observable和Observer都抽象成了类,并且添加了相应的行为,承担了相应的职责,这是面向对象范式;它实现了OberveableType协议,并且拓展了该协议,添加了大量的默认实现,这是面向协议范式;它实现了map,和flatMap方法,可以说Observable是一个函数单子(Monad),同时也提供了大量的操作符可供使用和组合,这是函数式范式;同时,总所周知,Reactive框架是一个响应式的框架,所以它也是响应式范式......

更何况,编程能力不就是抽象能力的体现吗?所以我认为掌握函数式是非常必要的!那么具体来说为什么重要呢?

在1984年的时候,John Hughes 有一篇很著名的论文《Why Functional Programming Matters》, 它解答了我们的疑问。

为什么函数式编程重要?

通常网络上的一些文章都会总结它的优点:它没有赋值,没有副作用,没有控制流等等等等,不同的只是它们对于各个关键词诸如引用透明,无副作用的种种解释,单是这只是列出了很多函数式程序 "没有" 什么,却没有说它 “有” 什么,所以这些优点其实没有太大的说服力。而且我们实际上去写程序的时候,也不可能特意去写一个 缺少了赋值语句或者特别引用透明的程序,这也不是衡量质量的尺度,那么真正重要的是什么呢?

在这篇论文中提到,模块化设计是成功的程序化设计的关键,这一观点已经被普遍接受了,但有一点经常容易被忽略,那就是编写一个模块化程序解决问题的时候,程序员首先要把问题分解为子问题,然后解决这些子问题并把解决方案合并。程序员能够以什么方式分解问题,直接取决于他能以什么方式把解决方案粘起来。而函数式范式其实提供给我们非常重要的粘合剂,它可以让我们设计一些更小、更简洁、更通用的模块,同时使用黏合剂粘合起来。

那么它提供了哪些黏合剂呢?这篇论文介绍了两种:

黏合函数:高阶函数

The first of the two new kinds of glue enables simple functions to be glued together to make more complex ones.

黏合简单的函数变为更复杂的函数。这样的好处是我们模块化的颗粒度是更细的,可以组合的复杂函数也是更多的。如果非要做一个比喻的话,我觉得就像乐高的基础组件:

图片

这种聚合就是一个泛化的高阶函数和一些特化函数的聚合,这样的高阶函数一旦定义,很多操作都可以很容易地编写出来。

黏合程序:惰性求值

The other new kind of glue that functional languages provide enables whole programs to be glued together.

函数式语言提供的另一种黏合剂就是可以使得程序黏在一起。假设有这么一个函数:

g(f(input))

传统上,需要先计算f,然后再计算g,这是通过将f的输出存储在临时文件中实现的,这种方法的问题是临时文件会占用太大的空间,会让程序之间的黏合变得不太现实。而函数式语言提供的这一种解决方案,程序f和g严格的同步运行,只有当g视图读取输入时,f才启动。这种求值方式尽可能得少运行,因此被称为 "惰性求值"它将程序模块化为一个产生大量可能解的生成器与一个选取恰当解的选择器的方案变得可行。

大家如果有时间还是应该去读读这一篇论文,在论文中,它讲述了三个实例:牛顿-拉夫森求根法,数值微分,数值积分,以及启发性搜索,并使用函数式来实现它们,非常的精彩,这里我就不复述这些实例了。最后我再引用一下该论文的结论:

在本文中,我们指出模块化是成功的程序设计的关键。以提高生产力为目标的程序语言,必须良好地支持模块化程序设计。但是,新的作用域规则和分块编译的技巧是不够的——“模块化”不仅仅意味着“模块”。我们分解程序的能力直接取决于将解决方案粘在一起的能力。为了协助模块化程序设计,程序语言必须提供优良的黏合剂。函数式程序语言提供了两种新的黏合剂——高阶函数与惰性求值。

一颗枣树(例子)

这个例子我参考了Objc.io的《函数式Swift》书籍中关于如何使用函数式的方式来封装滤镜的案例。

Core Image是一很强大的图像处理框架,但是它的API是弱类型的 —— 可以通过键值编码来配置图像滤镜,这样就导致很容易出错,所以可以使用类型来避免这些原因导致的运行时错误,什么意思呢?就是说我们可以封装一些基础的滤镜Filter, 并且还可以实现它们之间的聚合方式。这就是上述论文中介绍的函数式编程提供的黏合剂之一:使简单的函数可以聚合起来形成复杂的函数。

首先确定我们的滤镜类型,该函数应该接受一个图像作为参数并返回一个新的图像:

typalias Filter = (CIImage) -> CIImage

在这里引用一段书中的原话:

我们应该谨慎地选择类型。这比其他任何事情都重要,因为类型将左右开发流程。

然后可以开始定义函数来构件特定的基础滤镜了:

/// sobel提取边缘滤镜
func sobel() -> Filter {
    return { image in
        let sobel: [CGFloat] = [-1, 0, 1, -2, 0, 2, -1, 0, 1]
        let weight = CIVector(values: sobel, count: 9)
        guard let filter = CIFilter(name: "CIConvolution3X3",
                                    parameters: [kCIInputWeightsKey: weight,
                                                 kCIInputBiasKey: 0.5,
                                                 kCIInputImageKey: image]) else { fatalError() }
        
        guard let outImage = filter.outputImage else { fatalError() }
        
        return outImage.cropped(to: image.extent)
    }
}

/// 颜色反转滤镜
func colorInvert() -> Filter {
    return { image in
        guard let filter = CIFilter(name: "CIColorInvert",
                                    parameters: [kCIInputImageKey: image]) else { fatalError() }
        guard let outImage = filter.outputImage else { fatalError() }
        return outImage.cropped(to: image.extent)
    }
}


/// 颜色变色滤镜
func colorControls(h: NSNumber, s: NSNumber, b: NSNumber) -> Filter {
    return { image in
        guard let filter = CIFilter(name: "CIColorControls", parameters: [kCIInputImageKey: image, kCIInputSaturationKey: h, kCIInputContrastKey: s, kCIInputBrightnessKey: b]) else { fatalError() }
        
        guard let outImage = filter.outputImage else { fatalError() }
        
        return outImage.cropped(to: image.extent)
    }
}

直接黏合

基础组件已经有了,接下来就可以堆积木了。如果有一个滤镜需要:先提取边缘 -> 颜色反转 -> 颜色变色,那么我们可以实现如下:

let newFilter: Filter = { image in
    return colorControls(h: 97, s: 8, b: 85)(colorInvert()(sobel()(image)))
}

上述做法有一些问题:

  • 可读性差:无法代码即注释,无法很容易的知道滤镜的执行顺序
  • 不易拓展:API不友好,添加新的滤镜时,需要考虑顺序和括号,很容易出错

自定义函数黏合

首先我们解决可读性差的问题,因为直接使用嵌套调用方法,所以会可读性差。所以我们要避免嵌套调用,直接定义combine方法来组合滤镜:

func compose(filter filter1: @escaping Filter, with filter2: @escaping Filter) -> Filter {
    return { image in
        filter2(filter1(image))
    }
}

// sobel -> invertColor
let newFilter1: Filter = compose(sobel(), colorInvert()) // 左结合的

这是左结合的,所以可读性是OK的,但是如果有三个滤镜组合呢?四个滤镜组合呢?要定义那么多方法吗? 巧了,还真有人是这么干的:

图片

如果大家去看RxSwift的话,就会看见它组合多个Observable的函数: zip , combineLastest ,每一个方法簇都提供了支持多个参数的组合方法,可是这就意味着我们在这个案例也是可以这样做的,但是这显然不是最好的解决方案。

如果使用combine这里三个滤镜组合的方案:

let newFilter2: Filter = compose(compose(sobel(), colorInvert()), colorControls(h:97, s:8, b:85)))

可读性还行,但是还是在添加新的滤镜的时候容易出错,不那么容易拓展。如果要再组合多个滤镜,那么就需要多个combine函数嵌套调用。

自定义操作符黏合

如果对应到数学领域的话,其实这几个滤镜的组合不就是四则运算中的  +  吗?一层一层效果的叠加,当然,确切地说,从效果上和 + 更相似,但是从特性来说更符合减法 -的,都是向左结合,而且都不满足交换律。

所以我们可以自定义操作符来处理滤镜的结合:

infix operator >>>
func >>>(filter1: @escaping Filter, filter2: @escaping Filter) -> Filter {
    return { image in
        filter2(filter1(image))
    }
}

当然还有一个小问题,就是如果有三个滤镜组合的话,会报错,因为我们没有指定它组合的方式(左结合,还是右结合)所以这里我们让它继承加法的优先级,因为它和加法一样都是左结合的:

infix operator >>>: AdditionPrecedence // 让它继承+操作符的优先级, 左结合
func >>>(filter1: @escaping Filter, filter2: @escaping Filter) -> Filter {
    return { image in
        filter2(filter1(image))
    }
}

那接下来我们愉快地使用它吧:

let filter = sobel() >>> colorInvert() >>> colorControls(h: 97, s: 8, b: 85)
let outputImage = filter(inputImage)
imageView.image = UIImage(ciImage: outputImage)

图片

函数式Swift.001.jpeg

那么这里来总结一下这一波过程,假设需求是存在的:

我们定义了很多基础滤镜层(Filter),接下来肯定需要组合基础滤镜为我们实际需求需要的滤镜,有的滤镜可能是有三个基础滤镜组合的,有的需要五个基础滤镜组合,当然极限情况下,可能还有需要十个滤镜组合的。

所以我们需要定义不同滤镜组合的黏合函数, 我们一共经历了三个组合方案的变迁:

  1. 直接组合
  2. 定义compose函数
  3. 自定义操作符

当然,诸君也可以使用更好的组合方案,如果可以希望留个言,共同探讨探讨。

还有一颗也是枣树(例子)

接下来这个例子,是一个我们使用Objective-C编程的时候经常会遇到的问题,需求如下:第二行数据必须等待第一行请求结束之后才可以开始请求。

图片

那么开始吧!

首先我们来看最容易的实现方案:

    @objc func syncData() {
        self.statusLabel.text = "正在同步火影忍者数据"
        
        WebAPI.requestNaruto { (firstResult) in
            if case .success(let result) = firstResult {
                self.sectionOne = result.map { $0 as? String ?? "" }
                DispatchQueue.main.async {
                    self.tableView.reloadSections([0], with: .automatic)
                    
                    self.statusLabel.text = "正在同步海贼王数据"
                    WebAPI.requestOnePiece { (secondResult) in
                        if case Result.success(let result) = secondResult {
                            self.sectionTwo = result.map { $0 as? String ?? "" }
                            DispatchQueue.main.async {
                                self.statusLabel.text = "同步海贼王数据成功"
                                self.tableView.reloadSections([1], with: .automatic)
                            }
                        }
                    }
                }
            }
        }
    }

熟悉吗?当然熟悉,直接在第一个请求的callback中直接进行第二个请求,但是请注意,这和OC写的有区别吗?我们这样和写和简单的人肉翻译机有区别吗?我们写的是Swift这个多范式的编程语言吗?

回到例子,我们就事论事,我觉得这样写会有几个问题:

  1. 数据修改和UI修改耦合在了一起
  2. 多重嵌套
  3. 违背了OCP(Open Closed Principle)法则:应该对修改闭合,对拓展开放
  4. 丑!

解决数据和UI耦合

从重要性的角度,我觉得应该先解决第4个问题,但是出于节奏,我们还是从第一个问题开始解决吧~

    @objc func syncDataThere() {
        // 嵌套函数
        func updateStatus(text: String, reload: (isReload: Bool, section: Int)) {
            DispatchQueue.main.async {
                self.statusLabel.text = text
                if reload.isReload { self.tableView.reloadSections([reload.section], with: .automatic) }
            }
        }
        
        updateStatus(text: "正在同步火影忍者数据", reload: (false, 0))
        
        requestNaruto {
            updateStatus(text: "正在同步海贼王数据", reload: (true, 0))
            self.requestOnePiece {
                updateStatus(text: "同步数据成功", reload: (true, 1))
            }
        }
    }

这里我把网络请求和数据处理都封装到了网络请求中,而且使用了swift的特性:嵌套函数,剥离了一部分重复代码,这样整个请求就变得非常清晰明了了,而且数据和UI就隔离开来了,并没有耦合在一起。

可是嵌套的问题还是存在,如何解决呢?

解决多重嵌套

还记得我介绍的第一棵枣树吗?我使用了自定义操作符来解决了函数调用的嵌套,这里其实也是一样的思路,但是要更复杂些。

这里我还需要重复引用一下《函数式Swift》中的那句话:

我们应该谨慎地选择类型。这比其他任何事情都重要,因为类型将左右开发流程。

第一步抽象

这里有两个类型需要抽象,第一是执行单个语句的函数(这里是更新UI),第二个是对应网络请求的函数

infix operator ->> AdditionPrecedence
typealias Action = () -> Void
typealias Request = (@escaping Action) -> Void

第二步抽象

那么如何将原来的函数拆解为使用类型表示的函数呢?

func syncDataF() {
    ......
 requestNaruto {
     updateStatus(text: "正在同步海贼王数据", reload: (true, 0))
        self.requestOnePiece {
         updateStatus(text: "同步数据成功", reload: (true, 1))
        }
 }
)

我们由上往下,那么抽象的过程应该就是

  • (Request, Action) -> Request

第一个请求 和 回调中的第一个Action,但是第一个请求还没有结束,所以返回的还是Request

  • (Request, Request) -> Request

处理了第一个Action的第一请求 + 第二个请求,  但是请求还是没有结束,所以返回的还是Request

  • (Request, Action) -> Action

第二个请求加上最后需要处理的Action , 完毕!

所以结果如下:

@objc func syncDataFour() {
 func updateStatus(text: String, reload: (isReload: Bool, section: Int)) {
      DispatchQueue.main.async {
         self.statusLabel.text = text
            if reload.isReload { 
                self.tableView.reloadSections([reload.section], with: .automatic) 
            }
        }
    }
    updateStatus(text: "正在同步火影忍者数据", reload: (false, 0))
    // 我们来拆解一下函数:要把函数抽象出来,这一点非常的重要
    // (Request, Action) -> Request
    // (Request, Request) -> Request
    // (Request, Action) -> Action
    // 通过这样的拆解方式就可以开始定义方法了
    let task: Action =
      requestNaruto
            ->> { updateStatus(text: "正在同步海贼王数据", reload: (true, 0)) }
            ->> requestOnePiece
            ->> { updateStatus(text: "同步数据成功", reload: (true, 1)) }
    task()
}

结果呢?我解决了嵌套的问题,很好,很完美,可是也很天真。

解决OCP问题

即使我们使用了自定义操作符,也没有解决OCP问题,因为如果我们要添加请求的话,我们还是需要修改原来的方法,依然违背了OCP法则。

那么怎么解决呢?

嗯嗯,具体的,请各位自己去试验吧!

我在文章尾部添加了相应的引用信息,这个例子是基于2016年的国内的Swift大会中翁阳的分享《Swift, 改善既有代码的设计》,如果有时间,希望大家可以去看看这个分享。

在分享中,他使用了面向协议的思路解决了OCP问题,很抽象,很精彩。

总结

很开心诸位看到了这里,我觉得这篇文章的能量密度应该不会浪费你们的时间。

在这边文章中,我首先是追问了函数式编程,以及编程范式的定义,只是想告诉大家:函数式编程之所以复杂只是因为我们不熟悉,同时它也应该是我们必须的工具。

然后我介绍了《Why Functional Programming Matters》这篇论文,它说明了为什么函数式编程重要,提到函数式范式的两大武器:高阶函数和惰性求值。

最后我使用了两颗枣树来给大家看一看Swift语言结合函数式的思想可以有哪些奇妙的化学反应。

那么这一次Swift的一次函数式之旅就结束了。但是还是想补充几句,每一年的WWDC其实Swift都更新了很多的内容,Swift本身也一直在增加新的特性,一直在稳健的迭代着,如果我们还是使用Objective-C的思维去写Swift的话,其实本身是落后于语言发展的。

最后引用王安石的《游褒禅山记》中的一段话:

而世之奇伟、瑰怪,非常之观,常在于险远,而人之所罕至焉,故非有志者不能至也。

与君共勉!

❌
❌