单例模式的“诱惑”
Posted 寅恩
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了单例模式的“诱惑”相关的知识,希望对你有一定的参考价值。
最近直播SDK要更换拉流工具,所以这段时间在测试工具稳定性的同时重写了一下直播页面。
随着项目和需求的不断迭代,控制器现在有将近 3 千多行代码,还不算小页面和辅助工具类等等,在维护和迭代方面可以说相对困难,有牵一发而动全身的感觉。刚开始的想法就是想将代码归置一下,将同一模块下的代码归类,在整理代码的过程中,发现随着业务的迭代部分代码已经废弃,而且充斥着各种各样的胶水代码,干脆就重构一下,说干就干。在重构了三天后发现还是无法跳脱原来的逻辑结构,代码依然很糟糕。既然这样,干脆重写吧。
当前直播总共分为这几大模块:聊天、视频、白板、广告、websock 服务器等,当然这些模块都已经模块化,当前要做的就是将各个模块组合到一个控制器中实现直播场景下的不同功能。
在重写第一周的时候,完全按照业务线的要求进行代码组装,并没有考虑可扩展性和解耦。
现在直播有 iPhone 和 iPad 两个版本,而直播类型又分为:
大直播、运营直播、小课堂等直播模式。
如果按照这样的方式进行重写随着业务线不断调整,代码肯定又会变得非常冗余,跟现在的代码结构没有差别。
所以停下重写的步伐,进一步思考:
如何能让代码更具有可扩展性?
1.如何在 iPhone 和 iPad 上共用一套代码?
iPhone 和 iPad 的页面布局是不一样的,但是基本功能是相同的。采用继承应该是最好的选择,那应该把哪些功能抽离到基类里面去?最先想到的是:网络监听、屏幕旋转、锁屏、前后台通知、状态栏等这些功能,后来一想,每个直播间都有聊天、白板、画笔、广告等这些功能,干脆就将这些功能也抽离到了基类。
抽离出基类只是为了减少每个直播模式中的代码量,如何针对不同的直播模式使扩展性更强。恰好,最近在学习 Go 语言,在学习接口这一节中,了解到了 Go 语言的编程思想:面向接口编程、多使用组合模式、降低耦合性、更小侵入性等,这样的编程思想与我当前重写的困境不谋而合。那么,我也可以将直播中通用的一些功能抽离为接口,将接口组合起来变成不同的直播模式。虽然 OC 中没有 interface 的功能,但有protocol,将类显式遵守协议。这样,子类也可以继承或重写接口,通过实现不同的接口完成不同的功能,也降低了耦合性。
现在项目中已经将各个部分模块化,直播控制器的一部分功能就是将各个模块按照其功能组装起来,完成不同的直播模式。现有的模块:聊天模块、白板模块、common 模块、广告模块、服务器模块、Core 模块。其中 Core模块是其他模块基础支持。Core 包括:websocket封装、图片下载、音视频播放器、tips、文件下载器、网络请求等等。
通过调用接口的不同,初始化相应的直播间类型,根据不同的直播类型开启相应的服务器,控制器持有一个 serverhander ,serverhander 遵守接口协议,将服务器中的数据通过接口分发出去,控制器响应接口的消息,将消息再分发到各个模块中,控制器根据直播模式的不同,响应各个模块的不同代理。这样通过响应不同的接口即实现了不同的直播模式。
通过这次的代码重写,发现原来项目中有一个严重的问题,那就是单例。
单例的作用是为了保证程序在运行过程中一个类只有一个实例,而且该实例易于供外界访问,可以方便的访问和控制该实例下的资源,从而达到节约系统资源的效果。
因为是直播是以 framework 形式进行开发的,而直播仅仅是 APP 内的一个功能,如果因误点而进入直播间,而里面的模块是以单例形式存在就会造成内存永驻而无法释放,所以在退出直播间的时候要销毁单例。
因为单例的独特性,它
可以在任何时候而被调用,跳脱于控制器生命周期之外。
或许有这样一个需求,点击控制器中某个按钮进行一个三级页面的交互,三页页面交互起码要写两个接口且进行数据传递,在实际开发中由于项目赶进度原因,接口就会写的尽量简单,能不带参数尽量不带,而三级页面的某些数据是控制器持有,这样单例就登场了,它不受时间地点的限制,可以快速拿到数据。功能很快完成了,突然有一天页面不能交互了,你百思不得其解,经过排查是因为单例持有的数据不对,这个数据被 N 个地方持有并修改过。
当你进行网络请求时,当接收到数据后,由于某些需求的原因,这些数据会被延迟展示,而恰巧此时用户退出了控制器,因为延迟处理是一个 block 块,而 block 块中并没有引用控制器的任何东西,所以控制器会正常释放。block 中执行的代码是某个模块的一个接口,而这个调用接口的实例是一个单例。这就导致了退出控制器时销毁了某个单例,而延迟调用的这段代码会再次创建对象,这样会导致下次使用该实例时会有脏数据而导致崩溃,而这样的崩溃信息很难排查。
懒加载:也
成为延时加载
,
即在需要的时候才加载
。
当一个对象为懒加载时,减少 self.obj 的使用,以_obj为代替。在 self 调用对象上,能尽量不用就不用。但是在单例模式下就会有一定的弊端。就拿上面的那个例子来说,控制器被销毁后,block 被延迟调用后单例还是可以正常调用接口,而接口中又调用了其他对象,而这个对象恰好是懒加载对象,这就造成了一连串的问题。你可以在设计单例对象时,单独设计一个初始化对象的接口,而这个接口调用的时机尽量在控制器的生命周期内。这样单例中的实例的声明周期就跟你依附的控制器的生命周期一致,当控制器生命周期销毁时,单例以及其内部对象也会一并释放掉。这样就算在延迟 block 中调用了单例,其内部对象并没有正确初始化,导致调用中断也避免了 bug 的出现。
这次的项目重写,在项目的构思以及代码结构设计方面,收获还是蛮多的。
以上是关于单例模式的“诱惑”的主要内容,如果未能解决你的问题,请参考以下文章
单例模式(单例设计模式)详解
Java模式设计之单例模式(二)
深入理解设计模式-单例模式(饿汉单例模式懒汉单例模式双锁单例模式)
单例模式(饿汉式单例模式与懒汉式单例模式)
单例模式
Java设计模式-单例模式