每天梳理1~2个问题,坚持一下(二)

新的一篇,再定个目标吧,每天只玩半小时游戏,留半小时锻炼身体(觉得这个比每天做总结更难哈哈)

19.05.21

NSURLProtocol

想总结这个要从上午的交流说起,一个网页中CSS布局文件或者js文件都是二次或者三次请求来的,如何拿到这些对应的文件,因为平时很少用NSURLProtocol,所以当时没有想到可以用NSURLProtocol进行处理,算是个知识盲区。

NSURLProtocol是一个神奇的类,每个HTTP请求开始时都会产生一个NSURLProtocol对象去处理对应的URL。这个请求包括但不限于你自己创建的一些request,还有webview里面的各种请求,你可以理解他做了抓包的效果,捕获了发出的网络请求。也就是它能拿到你的每个request(wkweb默认不可以)。

那么它能做的事情似乎很多:
拦截广告前缀的URL,
缓存操作,
标准化每个request,
重定向......

只要和大量请求改造有关的,它似乎都能大显身手。

下午只做了一个简单的小测试----捕获发出的请求,没有具体到各个场景或者功能点:

1.需要创建一个继承自NSURLProtocol的类;
2.需要在请求发起前进行注册,像下面这样:

[NSURLProtocol registerClass:[CustomURLProtocol class]];

3.捕获request:

+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
    return NO;
}

上面这个方法表示是否处理这个request,默认返回NO系统处理,如果返回YES,就是自己处理。在这个方法中我们已经可以拿到一个发出的request请求。

NSURLProtocol还有很多对request的请求,准备参考资料+测试完毕之后单独写开篇幅写一个。
另外NSURLProtocol 对WKWebview默认无法捕获其内部的网络请求,似乎是因为WK走的独立的请求方式,需要调用私有API进行操作。

19.05.22

FOUNDATION_EXPORT

在阅读优秀的代码的时候会发现大神们经常用到FOUNDATION_EXPORT这个宏。点进去看一下有下面一段:

#if defined(__cplusplus)
#define FOUNDATION_EXTERN extern "C"
#else
#define FOUNDATION_EXTERN extern
#endif


#if TARGET_OS_WIN32

    #if defined(NSBUILDINGFOUNDATION)
        #define FOUNDATION_EXPORT FOUNDATION_EXTERN __declspec(dllexport)
    #else
        #define FOUNDATION_EXPORT FOUNDATION_EXTERN __declspec(dllimport)
    #endif

    #define FOUNDATION_IMPORT FOUNDATION_EXTERN __declspec(dllimport)

#else
    #define FOUNDATION_EXPORT  FOUNDATION_EXTERN
    #define FOUNDATION_IMPORT FOUNDATION_EXTERN
#endif

由于C++代码中编译器会改变函数名称,编译出来的和定义的会产生差别,所以上面的代码表示FOUNDATION_EXPORT这里兼容了C++中的extern和32位的环境。

extern是C中的一个关键字,告诉编译器遇到extern修饰的变量或函数时去其模块内部查找具体的定义,是一个外部变量的引用。FOUNDATION_EXPORT既然是FOUNDATION框架下对C和C++的extern兼容,起到的效果自然也是和extern是一样的,那么它就可以用来做全局变量。

AFN中有很多处用到了FOUNDATION_EXPORT,我们参考其一处:

AFURLSessionManager.h

FOUNDATION_EXPORT NSString * const AFNetworkingTaskDidResumeNotification;
AFURLSessionManager.m

NSString * const AFNetworkingTaskDidResumeNotification = @"com.alamofire.networking.task.resume";

可以看到AFNetworkingTaskDidResumeNotification这个变量在AFURLSessionManager.h处做了声明,在.m内进行了定义。我们全局搜一下,可以发现在很多个文件中都使用了这个变量,有点宏定义的感觉。事实上大佬们就是使用这个方式来替代宏的效果,FOUNDATION_EXPORT起到了宏的作用。

FOUNDATION_EXPORT的使用和extern其实是一样的,外部声明+模块内部定义。可以参考上面的定义方式。

FOUNDATION_EXPORT和宏的区别:

1.语义更加明确。宏定义更多用在无语义处,比如定义屏幕的宽度,而FOUNDATION_EXPORT更多用在需要明确语义的地方,比如我的一个通知的名称,是由某个模块具体定义的,因为这个名称需要被别的类引用到所以使用FOUNDATION_EXPORT,只需要一些类知晓而不必所有的类都知道,我们可以很方便地追踪到是哪个模块产生的这个定义。

2.在判断字符串相等时,宏是调用了 isEqualToString: ,FOUNDATION_EXPORT则是指针指向地址的比较;

FOUNDATION_EXPORT和宏两者如何选择?我觉得主要是参考上面的1处,即你想表达的语义,如果是全体公用的一些定义,那么宏是很合适的,如果只是某些文件使用的话FOUNDATION_EXPORT无疑更好,FOUNDATION_EXPORT关联到了具体的模块,使得我们很容易找到出处理解其中的含义。

关于extern的用法,有一篇非常不错的文章,先mark一下:https://stackoverflow.com/questions/1433204/how-do-i-use-extern-to-share-variables-between-source-files

19.05.23

NSURLSession和内存泄漏

从内存泄漏方面来说,一直都没有去考虑过NSURLSession引起的内存泄漏,或者说用的AFN大佬们已经对其做了规避没有产生内存泄漏的现象,今天在查阅其它资料的时候发现了这方面的问题记录一下。

问题反馈与解答的链接:https://stackoverflow.com/questions/28223345/memory-leak-when-using-nsurlsession-downloadtaskwithurl/35757989#35757989

我们进入到NSURLSession中去看其delegate:

@property (nullable, readonly, retain) id  delegate;

NSURLSession对其代理是以一个强引用的状态存在,这就意味着当你到处以属性实例化session到处设置代理的时候,会造成循环引用。

另一个重要的原因是每一个NSURLSession对象都会维持一个SSL cache长达十分钟左右,你无法通过现有的API对其进行清理,即使你调用了下面两个方法去清理任务,短时期内也无法令内存下降,所以理论上NSURLSession对象越少越好。

- (void)finishTasksAndInvalidate;
- (void)invalidateAndCancel;

如何去避免这种情况:对于相同用处的request只使用同一个NSURLSession对象,除非你的这个请求非常特殊不得不配置一个新的NSURLSession对象,否则都应该只使用一个单例对象;由于NSURLSession对象的代理是强引用,你可以像AFN使用一个继承自NSObject的子类去桥接NSURLSession的代理方法,而不是每次都直接设置代理。

19.05.25

发烧了,休息两天,很久没生病了,最近要把锻炼身体提上日程了。

19.05.28

记录两个被问到的问题:

OC中向nil发送消息为什么不崩溃

先根据搜集的资料记录一下,并不一定准确,随时修正补充。

向nil发送消息的时候,首先isa指针是空指向0,objc_msgSend(id self, SEL op)函数teq指令会首先判断self(消息对象)是否为空,如果self为空,moveq指令会将SEL置为空,bx指令则直接返回到向nil发送消息的地方什么都不会发生。

teq指令:判断self是否为nil;
moveq指令:如果self为nil,则SEL置空;
bx指令:回到函数调用的地方;

数组下标为什么从0开始

1.物理地址从0开始;
2.减少CPU运算。
当下标从0开始时数组寻址为: arr[i] = base_address + i * type_size(1) ,其中i代表起始地址的偏移量;当下标从1开始时数组寻址为:arr[i] = base_address + (i - 1) * type_size(1),可以看到多了一次-1的运算。虽然简单的加减乘除对于计算机来说并不会产生过多的负担,但是对于这种需要被用在各个地方的底层架构部分,就会产生很多-1的运算,既然这种运算是不必要的且可以避免的,自然也应该去优化掉;
3.万物基于C。C之后的语言基本上都按C的规范来编写,一方面符合习惯另一方面节约学习成本;

当然也有一些数组下标可以不按C来的语言,比如python中的数组下标可以为负,表示从后往前,虽然python也基于C,但号称1行顶10行自然也做了不少的“敏捷”优化。

19.05.29

@synchronized (obj)中的obj

@synchronized以递归锁的形式实现,obj的作用是将obj的地址作为key,通过哈希表维护这个递归锁value。

既然@synchronized以这种方式去保证线程安全,那么obj = nil的时候是不起任何作用的。因此我们也要保证obj这个对象不能为nil,一旦被提前释放或者其它原因置为nil时锁也就失效了。

通常我们会让obj = self,即@synchronized(self),滥用这种写法可能会造成死锁。因为self很可能会被外部加锁访问,以self地址的key生成锁,一旦产生多个公共锁交替很可能死锁。正确的做法是以一个不会被外部捕获的变量作为key,防止产生多个公共锁。总之就是尽可能地不要产生公共锁(以同一地址生成的锁)。

19.05.30

哈希表

哈希表:通过维护key和value的映射关系来进行数据访问的数据结构。

哈希表本质是一个数组,数组中放着下标若干的“桶”,将要映射的数据作为key,通过hash和取余之后得到对应的桶,将这一组数据key和value放入桶中。当需要找到这个数据对应的value时,我们只需对这个数据重新进行一次相同的算法找出对应的桶即可。

理论上哈希表这个数组越大,桶发生冲突的概率就越小,但消耗的内存也就越大,为了解决桶的冲突和内存上的问题就产生了一些方法。

解决关键码哈希地址冲突的方式:

开放寻址法:
*线性探测:当第n个桶产生冲突的时候,哈希表去查看第n+1个桶是否已经记录数据,如果没有则将当前的key-value放入这个桶中,如果有则继续向下寻找n+2,n+3...

*二次探测:和线性探测基本相同,不同的是二次探测以n + 1,n + 4,n + 9...即相隔n的平方跳跃进行空闲位置的查找。这种方式解决了线性探测中聚集的问题,但同时自身也产生了以平方的聚集。

开放寻址法的劣势也非常地明显,当数组中的数据越来越慢时,寻址的效率就会越来越低,当数组完全装满时,需要另开一个新的数组(一般是其2倍容量)来进行新数据的装入,这个过程中对原始数组中的数据操作也是非常耗时的。

拉链法:每个桶中以链表形式保存key-value数据,当桶产生冲突时,只需往当前桶的链表后面拼接一个链表元素即可,在查找的时候先找到桶,再顺着链表逐个查找直到找到对应的key,拿出value。拉链法是目前比较好的解决地址冲突的方式。

再写几句

今天在微博上看到了一段很有感触的话,说的是现在的人在国内总感觉不公平,墙内水深火热,但是墙可以墙掉你肉身和一些信息,墙不掉你学习的能力啊,你在家不是照样可以学习英语?网上有那么多的学习资源你为什么不去学不去改变你自己?

为什么不去改变自己呢?因为懒,少玩一局游戏,多学几段英语,每天有1个问题的答案,一年就是365个问题的答案,这些还只是表面,答案和答案之间往往会产生融会贯通的效果,你的能力在坚持下不仅会得到量的飞越同时会得到质的飞越。

所以,努力改变自己,坚持学习,编程、英语、金融、投资等等,从事的或者感兴趣的,学习使人进步。

19.05.31

Unicode和UTF-8

Unicode:为每种语言的每种字符都规定了统一且唯一的二进制编码,包括字符集和编码方案等。

Unicode缺点:Unicode只规定了字符的二进制代码,并未规定如何对这段二进制代码进行区分和存储。换句话说,不同的字符通过Unicode进行编码之后是一段二进制代码,我们如何让去区分这段二进制代码是一段整体的字符编码还是需要分割的呢?而且英文只占用了1个字节的容量,但是其他符号可能占用2个以上的字节,我们如何去更好地存储这段编码呢?

由于互联网浪潮的到来,UTF-8出现了。

UTF-8:是Unicode能够推广使用的实现方式之一,除此之外还有UTF-16,UTF-32等等。

UTF-8以可变的字节长度来进行编码,它使用1~4个字节表示一个符号,根据符号的不同改变字节大小。

UTF-8编码规则:
对于单字节字符,首位为0,后7位为unicode码,所以对于英文字母来说UTF-8编码和ASCII码是相同的;
对于n字节字符,第一个字节前n位均为1,n+1位为0,剩下字节前两位为10,余下的为放置unicode码。

UTF-8编码

附上一张清晰的编码图。
参考:https://www.cnblogs.com/kingstarspe/p/ASCII.html

19.06.03

抽象类与接口

这两天在看面向协议编程,但感觉信息量很大一次无法完全地梳理通,那就还是采取老模式,点到线到面,先从每个点整理总结。

抽象类:通过抽象出公共的父类以继承的方式不断地增加子类从而扩展功能的方式。(这里先不讨论多重继承的形式,下面会单独整理总结多重继承。)

接口:即面向接口编程的思想,它是一组没有具体主体的方法集合,它也是面向对象编程思想的一个体现。OC中大多以协议和多态的形式去体现。

如果不编写底层或者框架代码,我们可能很少去使用面向接口的编程思想,因为抽象类这一方式已经基本上可以将接口的方案完全替代。虽然说在功能的实现上,两者几乎能互相替代,但其实他们本质是不同的,或者说我们有些地方根本就是用错了方案。

1.在定义上。抽象类是一群主体的抽象,接口是无主体的。这就意味着当你无法抽象出有意义的公共的类时,你可能要考虑使用接口的方案了。例如我们汽车有一个打开启动的方法,电视机也有一个打开启动的方法,但我们似乎并不能很合适地提取一个抽象的父类,我这里用的是“合适”,强行提取也是可以的,比如机器类,但超出高度的抽象对于解决这类问题的实际意义有限,甚至负面作用高于正面作用,这个时候接口就派上了用场,在我的控制类中,我可以定义一个专门用来打开关闭的协议,服从这个协议的类中都需要去实现这个方法。

2.语义不同。抽象类与继承类有isa的关系,接口则是一系列方法功能的集合,表示具备某种能力。例如继承Animals类的Person类,我们可以说人是一个动物,OK没有语义上的问题,但是接口则不行,因为接口没有主体,自然也就没有isa的关系。但两者在具备某种能力上是共同的,Animals都能eat,Person也能eat;汽车能启动,电视也能启动。

3.方式不同。单从OC上来说,抽象是以继承的方式去实现,接口是以协议和多态的形式去体现,这里抽象+多态,是我们一个比较常用的方式。

4.抽象的缺陷。无法多重继承是其很大的一个硬伤,这就使得有些代码可能并不能如愿地复用;抽象程度的好坏直接决定后续工作是否能良好进行;高耦合的模式使得其各个抽象类牵一发动全身。

5.接口的缺陷。接口类只负责定义,没有一个默认的实现(swift可以),是其非常不方便的地方,增加了代码量;接口的理解感远远低于抽象,抽象可以给我们一个主体去理解其方法行为,接口更多地是直白地功能表述。

抽象和接口如何去选择?

1.是否满足定义和语义的要求。能否有意义地抽象,是选用抽象和接口的基本条件。强行抽象滥用继承会使得代码逻辑混乱、结构非常差。
2.考虑接口和继承优缺点。优缺点在上面已经描述。
3.组合化。稍微大一些的体系无法单一地使用某种方式,往往会选用组合的模式,对于一部分有明确父类子类关系的,将其进行抽象继承,另一些没有关系的使用接口的方式。

19.06.04

继承

抽象出父类是为了使其子类具备共性,达到代码的复用,进一步可以扩展到多态的使用。在实际应用中继承的模式用的是非常多的,很多时候考虑到的是代码层面的复用而不是从整体的设计去规划。

继承的一些劣势:
1.由于和父类是高耦合,一旦父类的属性或者方法产生变动,子类就需要大面积的修改,如果继承的子类深度很大,则会是一个不小的工作量,这反而是违背了开放封闭原则;

2.父类的方法和属性对子类是完全可见的(即使写在.m中),这在一定程度上是不安全的;

3.大部分语言都是只支持单继承,不支持多重继承,即使是C++或者python这种支持多重继承的,其实也是通过手动或者规则去指定到某个继承;

4.继承的行为在编译时期确定之后便不可能更改,动态性较差。

插入一个相关的:是否需要基类?

我个人觉得需要,基类出了它本身抽象和继承的好处之外还在一定程度上给予了项目层次的逻辑感。但基类的属性和方法不宜冗杂,且子类继承深度不宜过深。基类起到的作用可能仅仅是简单的设定一些默认值,给出一些既定的接口做出一些默认的实现,真正的功能实现和特异性的实现应该是放到子类去做,或者是配合多态性做一些便捷易于扩展的结构设计。

19.06.05

组合的形式解决继承问题的方案

继承虽然使得我们的代码可以更好复用,但其实往往不会有一个“完美”地继承,一些子类因为继承往往会承担与其无关的父类的职责,这个时候如果使用继承会使得代码结构不清晰,深度过大时会使得整个继承体系显得臃肿。

这个情景取个名字,需要半继承的情景吧,也就是你需要复用这个类的一些变量和方法,但却不应该直接继承这个类,达不到“完美”继承的条件。一个很好的解决这个情景下的方式是使用组合的形式,将需要复用代码的类的实例当做当前类的一个属性或者实例方法,那么当前类就具备了需要复用代码类的能力,同时,因为没有直接继承,所以不必考虑父类对其自身的影响,代码结构也会比较清晰。

19.06.06

setObject:forKey:和setValue:forKey:

面向协议先缓一下,消化消化。。。

群里今天有人提到这两个方法拿来做对比,说了很多地方,之前一直只了解这两个方法对于nil处理的区别,今天总结一下这两个方法的不同。

1.方法的使用者不同。setObject:forKey,是可变字典的方法,setValue:forKey是KVC赋值的方法;
2.对nil的处理不同。这个是大家都熟悉的一个方面,setObject:forKey:放入nil时会crash,setValue:forKey:在放入nil时会自动调用removeObject:forKey:移出nil;
3.setValue:forKey:中的key需要是NSString类型,而setObject:forKey:中的key是id类型,但通常我们使用的依旧是NSString类型;

19.06.10

养成初始化值的好习惯

最近忙的焦头烂额,目前的项目工程庞大且混乱,以稳定为目的,大部分都没办法去做强硬的改动。最近的需求又需要变动之前的一些流程,重新梳理整合这些流程非常令人头疼。在整理之前的流程中发现了很多地方的初始化并未给一个初始值,联想到之前收集到的一些奇奇怪怪的异常觉得问题可能就出现在这个地方。

在ARC环境下,OC对象一般不需要我们初始化为nil,系统会自动帮我们进行这一步。但对于基本类型来说,如果不进行初始化赋值,很可能会出现意外的值。比如常用的BOOL类型,正常情况下如果未初始化我们拿到的是NO,但也有很大可能我们拿到一个随机数。这是由于我们使用的这个地址很可能是之前的一个数据使用的,当我们重用这块地址时未进行数据的清理(初始化),之前的垃圾数据就有可能会被使用到,这就会造成一些很奇怪的异常。所以养成初始化值的习惯是很有必要的。

19.06.11

swift的面向协议编程

面向协议编程这里准备分几部分写,感觉有很多方方面面,一次总结不完。

单独把swift的面向协议编程拎出来一是因为swift把面向协议编程吹的响,二是因为其自身比较特殊。

不得不说的是swift面向协议能自己做出实现,服从这个协议的对象不需要重新再写代码就能拥有默认的实现。如此,swift的面向协议完美地囊括了继承的优势,这也是其他很多语言目前无法做到的,这种形式获得了继承和面向接口的双优势。

像下面这样:

import Foundation

protocol Runable {
}

extension Runable {
    func run() {
        //写入默认实现
    }
}

服从此协议的类都具有此默认实现,无需重复此代码,除非你想做一些不同的实现:

class People: NSObject, Runable {

}

//在其他地方People的实例能直接使用run方法
let person = People()
person.run()

在我看来,swift的面向协议做了两方面的工作:模块化和发挥继承优势。

模块化是指协议的独立性与相关性。协议可以是互不相关互不干扰的,也可以是通过继承上层协议来的。通过将协议模块化,更好地发挥出组合的优势。

发挥继承优势相当于它做了多继承的工作,但相对于多继承来说,具有默认实现的协议显然更能从宏观层面上解耦,遵循开放封闭原则,逻辑也非常的清晰。可以说swift中协议具有默认实现的能力是非常大的一个亮点。

从结构上看,OC更偏向于继承树的结构,是一个由“深度”的结构,而swift由于面向协议的优势,使其结构更加扁平化、组合化。

19.06.12

URL特殊字符处理

中午有个同学在群里提了一个问题寻求解决,大致是下面这样:
http://www.qq.com/wwee|asd
这个字符串如果不进行编码,由于特殊字符 | 的原因,无法生存URL,如果进行编码,把 | 变成%7c,H5那边又无法以 | 作为分割依据。

这个问题很好解决,让H5那边进行一次解码就OK,但是有个不能忽视的现象,这个带特殊字符的str放到safari中能直接打开,这说明用原生处理是可行的只是还没找到方式。

这位同学折腾了一下午,终于在下班的时候把方法找了出来,给我们分享了一下,即使用下面这个方法:

/* Initializes and returns a newly created NSURL using the contents of the given data, relative to a base URL. If the data representation is not a legal URL string as ASCII bytes, the URL object may not behave as expected.
 */
+ (NSURL *)URLWithDataRepresentation:(NSData *)data 
                       relativeToURL:(NSURL *)baseURL;

这个方法应该用的不多,至少对于我来说也是第一次用到。

方法的第一个参数指需要进行编码的字符串部分转成的data,第二个参数指的是URL的domain。

如果用上面的网址的例子,那么URL的domain指的是www.qq.com这部分,第一部分是经过处理的data:

NSData *data = [@"wwee|asd" dataUsingEncoding:NSUTF8StringEncoding];

出来的URL像下面这样:

wwee|asd -- www.qq.com

生成了保留特殊字符的URL。

19.06.13

利用分类“越级”拿到系统属性

iOS没有完全绝对的私有属性和方法,只要你能找到对应的名称,你就可以使用这些属性调用这些方法。通常来说,使用属性会更加安全一些,强行调用系统私有的API会无法通过审核。

获取到某个类的私有属性我们可以做一些“越级”的操作。“越级”指的是跳过一些系统特定的流程直接拿到结果。

这里举一个之前做过的比较典型的例子:iOS屏幕录制的ReplayKit

我们在调用屏幕录制停止的方法时,其代理方法会触发弹出RPPreviewViewController,这个VC包含了你刚才录屏的一系列信息,包括暂存的URL,可供预览的数据,是否进行保存等等。一般来说我们并不希望操作流程如此复杂,只想停止之后保存拿出视频。RPPreviewViewController的实例已经具备了我们想要的信息,只是由于这些信息是私有属性我们无法直接拿到。这个时候分类就派上了用场,由于分类的优先级高于原始类,所以当我们创建分类,给予一模一样的属性变量的时候,我们的属性变量就会生效,可以直接地获取到想要的信息。这个操作很简单,像下面这样:

#import 

@interface RPPreviewViewController (MovieURL)

/**
 获取录屏的URL
 */
@property (nonatomic,strong) NSURL *movieURL;

@end

这种hook属性的方式在一定程度上可以带来很大的便捷,而且在审核上也很安全,工程中其他地方也有一些对私有属性的hook,目前没有遇到审核问题。

19.06.14

关联xib的视图与继承

在创建视图的时候如果关联使用xib,在继承这个视图的时候可以说是一个头疼的问题,因为xib是无法继承的,也就意味着你的视图布局无法继承,只有属性和方法得到了继承。

网上有一些方式去解决这个问题,比如在继承的类初始化方法中用一个视图承接xib的视图,虽然解决了视图的问题,但多少都有一些奇怪。

目前开发的工程中有一部分视图关联了xib,如果非要继承的时候也是使用了上面的方式进行处理。自己在开发的时候使用可视化的模式很少,包括但不限于上面这种情况造成的影响,个人还是比较偏向于纯代码的开发,不过对于几乎不可能重用的视图使用xib也是可以的,比如登录注册页面,毕竟可视化条件下效率还是非常高的。

19.06.17

减少侵入原始代码

最近忙的焦头烂额,因为现在的项目有一些是之前外包做的,新需求又要对这部分进行新的调整,令人头大。

这一部分代码比较臃肿,页面是一个表单的形式,但是十几行的表单其实就是一个cell,上面每一个控件都是一个完完全全alloc init出来的,没有任何复用。光模仿cell的分割线的属性view就有将近30个。。。

考虑了一下时间紧急和重构风险,这个版本放弃了对这部分的重构,推迟到下个月重构。这就意味着我需要“模仿”这部分的代码进行书写。看别人代码已经很难受了,模仿在这个难受程度上要+10086。

虽然要“模仿”之前的代码,但也要有一个比较小的“侵入”模式,如果我和之前的书写一样,就需要每个控件作为一个属性去不停地操作,显然这样非常混乱容易出错,而且这样完全“侵入”了之前的部分。

因为是一个新需求,所以不需要对原来的地方进行较大的改动,所以我觉得采用“分块”的模式去开发,即新需求是一个相对独立的部分。

我用了一个backView去承接我这部分的新需求,使其相对独立开,新的控件完全建立在backView上不和之前的布局一个平级。在视图位置调整上,利用了cell的分割线view,需要新需求时分割线两个分割线view打开拉出高度,不需要时backView隐藏,两个分割线合并。虽然在书写上同样也是在模仿之前的代码,但有了backView做层次区分之后新需求对原来的代码入侵度大大减少,在布局调整上,大调整我可以直接进行backView层面的操作,小调整依旧可以调整backView上的控件,使其成为一个相对独立的模块。

19.06.18

反射模式下的文件引用问题

问题出在合并代码的时候,工程目录文件project.pbxproj被误操作,导致部分文件虽然拉下来在文件夹中但是并未在工程中,没有被引用。

一般来说出现这种问题在编译阶段就会被发现,其他文件引用丢失的文件时便会报错。但是我这个比较特殊,利用model反射出字符串创建对应的cell,丢失的恰好是cell文件,在引用关系上并未有任何一个文件对其引用,这就使得在编译期间无法发现问题,而且非常的隐性。

之前一直没有特别留意到反射模式下这个问题的存在,一方面是由于反射模式并没有大量地被使用到工程中,在编译阶段编译器便会将错误抛出,另一方面是之前没有出过这个问题。

这里先记录下,明天寻求解决方案。

19.06.19

解决反射模式下引用问题

解决的话无外乎两种,一种是编译器编译时抛出错误,另外一种是我们自己检测出错误。

1.想要让编译器抛出错误,那我们就必须对每个类都引用到,不能使其孤立。大多数文件状态下其实是可以的,反射模式下一部分文件会被孤立,这就需要我们手动让其被别的类引用一次。这显然也是不太合理的。

2.手动检测引用问题。
此处根据我目前的工程的状态做出的方案,目前工程里用到反射的情况是利用model反射到对应的cell上,具体见https://www.jianshu.com/p/cbe0530279a0
在Debug环境下我们可以开启检测模式,因为model必定会被引用,所以无需处理model,只需要注重cell,cell被model所映射,所以我们要去model中处理。
重写model的load方法,在load方法中我们去判断对应的cell是否存在,像下面这样,在DEBUG环境下利用断言强行检测一次是否存在要映射的cell:

#ifdef DEBUG

+ (void)load {
    [super load];
    NSString *selfClassName = NSStringFromClass([self class]);
    NSString *cellClassName = [[selfClassName substringToIndex:selfClassName.length - 5] stringByAppendingString:@"Cell"];
    NSString *alertStr = [NSString stringWithFormat:@"%@未被引用",cellClassName];
    NSAssert(NSClassFromString(cellClassName) != nil,alertStr);
}

#endif

当然,这种方式只能对特定的映射进行检测,没办法检测随机的映射。目前能想到的在App完全运行前进行一次有效的检测方式只有这个,有空再思考下更优的方案。

从这次的BUG也能看出来反射模式虽然在解耦合上有很大的优势,但也正因为高度地解耦使得部分文件被孤立到未被引用进工程也没能及时发现。所以并不建议大量使用反射的模式,如果使用也要注意类未被引用,方法未能找到等问题的处理。

19.06.20

快速迭代开发

从过完年开始ERP项目就处在了快速迭代的模式下,每周至少发版1-2次,加急需求另算。在这种模式下虽然效率提升上去了,但是面临的问题也非常之多。在这其中我觉得最主要的问题是任务需求无法穷尽其可能,导致做出来的产品或多或少都有些差异。

因为我们的项目是ERP系统,所以各个模块、节点的关联性非常强,PM在组织需求的时候往往无法穷尽其可能,例如这周做的需求中,只描述了起单页面做出的改动,但是并未考虑起单之后的改动,包括编辑、详情等这些状态。在估时和整体考虑上产生重大偏差,使得效率大大降低。

这种模式已经有一些敏捷开发的味道,弱化了文档,很多只是简单地在禅道分配个任务,但我认为目前的状态弊端非常的大,上面的这种情况已经不止一次地出现。我觉得无论是敏捷还是快速迭代的方式,首先你的需求一定是清晰完整的,你可以不给文档不给原型不给UI之类的,但最基本的需求应该是清晰完整的,包括考虑到可能产生的误解,在信息传递的过程中这些也需要非常的有效和完整,这样出来的产品才会是一个接近理想的产品,而不会是一个四不像。

19.06.21

MVC中加入VM的料

目前的项目中ERP系统大部分基于MVC的设计架构,最近要把CRM子模块进行完善,考虑到后期CRM这部分模块要移植到征信系统中去,所以这部分的完善需要花点心思。

数据层面上,CRM这部分并没有太多的数据改动,改动比较多的是根据状态的显示部分(因为征信和ERP目标群体不一样,征信需要特定人群才能看到,ERP则是每个业务员都可以操作)。

根据上面的情况,M层由于没有变动,所以完全可以进行复用,直接进行移植,V层虽然有大量的状态变化但其实主体模块未做太大变动,所以V其实也可以进行基类的提取或者进行一个扩展复用。那么问题来了,如何处理两个不同的状态呢?M和V都复用了,那就必须有一个桥梁桥接这两个,没错就是VM了。

严格地说,这里的VM和MVVM中的VM其实不太一样,这里的VM比较局限在桥接的作用上。

在实现上,我采用了继承的方式,即VM继承于M,这样一来VM有了M的数据能力,同时,在VM中我处理V的展示逻辑和控制,每个不同的移植对象我有一个不同的VM,这样CRM系统的移植只是做了一次VM的处理。

由于继承的关系,VM的数据由M所控制,M在各个端保持一致统一更改,使得数据层面上完全契合。

虽然这样在MVC中加入了VM的身影多少都会有点打破固有模式的感觉,但我觉得找到适合的方式才是最重要的,包括工程项目中的其他地方,也不会特定地去扣死一个东西,灵活地运用最合适的才会事半功倍。

19.06.24

造了个轮播图轮子(一)

一个上机的内容,准备整理下做成个轮子。

image.png

上面的轮播的效果,之前用的这位老哥的轮子:https://github.com/Zws-China/WSCycleScrollView,图也是他的哈哈

面试上机的时候要求做一个类似的轮播,之前项目中也用到过类似的,直接用了上面老哥的轮子,有考虑过自己写的思路但是没写过,然后面试上机的时候直接要1个小时做出来一个,正好拿之前的思路练手了。

上面老哥的我觉得实现的思路有点复杂,一个小时之内肯定搞不定,代码和计算量很大,所以我还是延续我之前的一个想法:三个视图的重用

但是分析过之后发现,由于静态的时候当前屏幕可容纳的视图已经为3个,这就造成了当我往右或者往左滑动的时候视图的最大可视范围会大于3个,所以这种情况下三种视图显然是不行的。

三个不行那就来五个嘛,反正思路是清晰的,再多两个视图也无所谓,那么最开始的框架就定好了,五个视图进行视图重用轮播。

整体的思路延续了之前的三个视图,分析如下:
1.五张视图进行重用轮播;
2.当前展示的永远是第三个视图,每次向左或者向右滑动之后调整数据源;
3.自身和scroll的clipsTobounds需要关闭,这样才能看到左右两张;
4.滚动的时候放大缩小可以独立于所有的逻辑之外,单独处理;

大致的逻辑像上面这样,明天总结下遇到的问题和处理方式。

19.06.25

造了个轮播图轮子(二)

说下遇到的问题和解决思路:

1.选择scrollerView和每个子视图一样大还是比每个都大一些显示出左右子视图?
考虑到整页滑动的效果这里必须要选择前者。如果你的scroll比每个子视图都大,那么整页滑动会使视图滑动位置错乱。至于显示出左右子视图,我们可以利用clipsTobounds的属性来达到这个效果。

2.在哪个代理方法中去修正下标达到对准数据源?
scrollViewDidEndDecelerating。这个方法中无论是拖拽还是定时器都会触发这个方法。

3.如何更简单地计算坐标问题?
关闭回弹效果bounces=NO。回弹效果会使计算坐标变化变得复杂和不稳定,关闭回弹在体验上也不会降低太多。

4.由于打开了clipsTobounds,可见区域有一部分是无法响应滑动的手势的,如何处理?
重写hitTest方法,传递滑动效果

5.图片固定5张,如果数据小于5张怎么处理?
监听数组变化,对应处理2、3、4三种情况,如果是3张,则数组中为:2、3、1、2、3状态

周末整理一下代码,做成个demo

19.06.26

代码拷贝带来的影响

今天线上发现一个BUG,在我的订单页面中显示的是正确的,但在我的任务中却没有显示,因为是个新需求所以很快就找到了问题所在:我的订单和我的任务两个页面虽然视图几乎一样,但是两个完全不同的控制器,我的任务是从我的订单中完全拷贝一份得来的,由于是紧急任务,当时只修改了其中一个,测试也忽略了另一个入口,导致其中一个页面显示不正确。

目前的项目里这种直接拷贝来的代码还有一些,之前的同事有的懒省事直接拷贝来拷贝去,拷贝过后的代码稍作下修改就把新的需求做完了,相比提取公共类或者设计出一个良好的结构这种方式显然省事多了,但埋下的隐患也非常之大。

如何对两个非常相近的页面进行处理呢?我的思路是枚举和组合的方式。枚举的方式应对简单的修改类型的页面,比如页面上有几个控件组合是否显示;组合的方式应对新增,比如我的任务页面上面要加一个搜索框,由于这个页面和我的订单页面只相差了一个搜索框,我们可以addChildViewController:,把我的订单的VC当做子控制器,处理下视图布局即可,当然,如果调用我的订单VC的私用方法,还需要把这些私有方法声明在.h中,这点是稍微不好的地方。

如何对两个有关系也有差异的页面进行处理呢,很自然地就想到抽象出父类,这是最简单的一种方法,也是比较可行的。但具体到实现其实还有很多问题,比如MVC结构下,M、V、C这三个是否都提出父类呢?

如果使用继承,我的思路是,M保持最原始的数据直接当做父类,子类针对每一种情况对应实例化一种,这里M的子类可以做成VM。之前在移植模块的时候也提到过,这里使用VM过度能使V得以复用,这里的VM不仅具有数据能力还具有指挥V布局的能力。在V的结构上,如果你不能非常确定V的公共视图,那么就尽可能少地提出父类,因为父V需要进行父类视图变更的话影响到各个子V是非常麻烦的,尽可能不去继承或者抽象尽可能地简单,并且不要深度继承,一般来说,V继承三层已经不得了了,不建议超过三层,不然需求变更改动父V是非常痛苦的。这点M和V的差异很大,M的修改效率远远大于V。剩下最后的C了,那么C到底要不要继承呢?我觉得C是可以继承的,但其父类一定是个与视图操作基本无关C,而继承下来的一般是一些方法和公共的配置。因为既然两个页面有明显的差异,那么在C中的逻辑操作就会有很多不同,与其重写这些方法不如各管各的职责分明,这样能降低出错率。

19.06.27

学的多不用就废了

今天写了会儿swift的代码,效率慢的感人。因为是混编,所以写swift也有一年多了,但是断断续续,而且项目一紧为了效率又换成OC了,这样算下来真正写的时间也没几个月,而且用一段放一段,导致现在又不熟悉,不熟悉就效率低,效率低就更不想用了,恶性循环起来了。。。之前的RN也是,刚玩的时候写的嗖嗖嗖,现在估计类都不知道怎么建了。。。
立个flag,以后的新类全部换成swift,三天不写手生。

这篇也很长了,再开一篇,基本上一个月就能开一篇了。。。

你可能感兴趣的:(每天梳理1~2个问题,坚持一下(二))