请选择 进入手机版 | 继续访问电脑版

第15章 抓住问题核心一一模板方法模式

[复制链接]
钟启航 发表于 2021-1-1 18:31:51 | 显示全部楼层 |阅读模式 打印 上一主题 下一主题
第15章 抓住问题焦点一一模板方法模式



15.1 模板方法模式先容

在面向对象开发过程中,通常会遇到这样的一个问题,我们知道一个算法所需的关键步调,并确定了这些步调的执行顺序,但是,某些步调的具体实现是未知的,大概说某些步调的实现是会随着情况的厘革而改变的,比方,执行程序的流程大抵如下:

  • 查抄代码的正确性;
  • 链接相关的类库;
  • 编译相关代码;
  • 执行程序。
对于差别的程序设计语言,上述4个步调都是不一样的,但是,它们的执行流程是固定的,这类问题的解决方案就是我们本章要讲的模板方法模式。
15.2 模板方法模式的界说

界说一个操纵中的算法的框架,而将一些步调延迟到子类中,使得子类可以不改变一个算法的布局即可重界说该算法的某些特定步调。
15.3 模板方法模式的使用场景


  • 多个子类有公有的方法,而且逻辑根本相同时
  • 重要、复杂的算法,可以把焦点算法设计为模板方法,周边的相关细节功能则由各个子类实现
  • 重构时,模板方法模式是一个经常使用的模式,把相同的代码抽取到父类中,然后通过钩子函数约束其行为
15.4 模板方法模式的UML类图

UML类图如图15-1所示。

角色先容


  • AbsTemplate:抽象类,界说了一套算法框架。
  • ConcretelmplA:具体实现类A。
  • ConcretelmplB:具体实现类B。
15.5 模板方法模式的简单示例

模板方法实际上是封装一个固定流程,就像是一套执行模板一样,第一步该做什么,第二步该做什么都已经在抽象类中界说好。而子类可以有差别的算法实现,在框架不被修改的情况下实现某些步调的算法替换,下面以打开计算机这个动作来简单演示一下模板方法。打开计算机的整个过程都是相对固定的,首先启动计算机电源,计算机检测自身状态没有问题时将进入操纵系统,对用户举行验证之后即可登录计算机,下面我们使用模板方法来模仿一下这个过程。


输出效果如下。

通过上面的例子可以看到,在startup方法中有一些固定的步调, 依次为开启电源、查抄硬件、加载系统、用户登录4个步调,如图15-2所示。

但是,差别用户的这几个步调的实现大概各不相同,因此,子类需要覆写相应的方法来举行自界说处理,这里需要注意的是startup为final方法,这样就包管了逻辑流程不能被子类修改,子类只可以或许改变某一步调中的具体实现,这样就包管了这个逻辑流程的稳定性。startup中的这几个算法步调我们可以称为是一个套路,也称为模板方法,这也是该模式的由来。
15.6 Android源码中的模板方法模式

在Android中,AsyncTask是比较常用的一个范例,这个类就使用了模板方法模式。关于AsyncTask更详细的分析,请参考《Android中 AsyncTask的使用与源码分析》这篇文章(网址为http://blog.csdn.net/bboyfeiyu/article/details/8973058),我们这里只分析在该类中使用的模板方法模式。
在使用AsyncTask时,我们都知道把耗时的方法放在doInBackground(Params…params)中,在doInBackground之前,如果还想做一些雷同初始化的操纵,可以把实现写在onPreExecute方法中,当doInBackground方法执行完成后,会执行onPostExecute方法,而我们只需要构建 AsyncTask对象,然后执行execute方法即可。我们可以看到,它整个执行过程实在是一个框架,具体的实现都需要子类来完成,而且它执行的算法框架是固定的,调用execute后会依次执行 onPreExecute、doInBackground、onPostExecute,当然也可以通过onProgressUpdate来更新进度,我们可以简单地理解为如图15-3所示。

下面我们看源码,首先我们看执行异步任务的入口,即execute方法。


可以看到execute方法是一个final方法,它调用了 executeOnExecutor方法,在该方法中会判断该任务的状态,如果不是PENDING状态则抛出异常,这也解释了为什么AsyncTask只能被执行一次,因为AsyncTask的RUNNING和FINISHED状态都会抛出异常,因此,每次使用AsyncTask时都需要重新创建一个对象
继续往下看,在executeOnExecutor方法中首先执行了onPreExecute方法,因为AsyncTask的要求是需要在UI线程中调用execute方法,因此,onPreExecute方法也执行在UI线程,然后将params参数通报给了mWorker对象的mParams字段,而且执行了exec.execute(mFuture)方法
mWorker和mFuture又是什么呢?实在mWorker只是实现了Callable接口,并添加了一个参数数组字段,我们分别来分析,通过跟踪代码可以看到,这两个字段都是在构造函数中初始化

简单地说就是,mFuture包装了这个mWorker对象,在这个mFuture对象的run函数中又会调用mWorker对象的call方法,在call方法中又调用了 doInBackgroud函数。因为mFuture提交给了线程池来执行,所以,使得doInBackgroud执行在非UI线程。得到doInBackgroud的效果后,通过postResult通报效果给UI线程,我们再看看postResult的实现。


从程序中可以看到,postResult就是把一个消息(msg.what = MESSAGE_POST_RESULT)发送给sHandler,sHandler为IntemalHandler范例,当IntemalHandler接到MESSAGE_POST_RESULT范例的消息时就会调用result.mTask.finish(result.mData[0])方法。我们可以看到result为AsyncTaskResult范例,具体源码如下。

从上述程序可以看到mTask就是AsyncTask对象,调用AsyncTask对象的finish方法时又调用了 onPostExecute,这个时候整个执行过程就完成了
总之,execute方法内部封装了onPreExecute、dolnBackground、onPostExecute这个逻辑流程, 用户可以根据自己的需求再覆写这几个方法,使得用户可以很方便地使用异步任务来完成耗时的操 作及更新UI,这实在就是通过线程池来执行耗时的任务,得到效果之后,通过Handler将效果通报到UI线程来执行
15.7 深度扩展

另一个比较好的模板方法示例就是Activity的生命周期函数,比方,Activity从启动到显示到窗口中会履历如下过程:onCreate、onStart、onResume,这就是一个典范的Activity启动流程,也是一个模板方法的运用。
我们知道,在Android系统启动时,第一个启动起来的历程就是zygote历程,然后由zygote启动SystemScrver,再后就是启动ActivityManagerService、WindowManagerService等系统焦点服务,这些服务承载着整个Android系统与客户端程序交互的重担。zygote除了启动系统服务与历程之外,平凡的用户历程也由zygote历程fork而来,当一个应用历程启动起来后,就会加载用户在AndroidManifest.xml中设置的默认加载的Activity,此时加载的入口是ActivityThread.main(String[] args)方法,这个方法就是雷同于C语言中的main方法,是整个应用程序的入口
在ActivityThread.main(String[] args)这个方法中,主要的功能就是创建Application和Activity,而且调用Activity的一些生命周期函数,如onCreate、onResume等。下面就从ActivityThread.main(String[] args)这个入口开始学习。

从上述程序可以看到,在ActivityThread.main中主要的功能就是创建了UI线程消息循环,而且启动了消息循环。最重要的是创建了ActivityThread,并调用了attach方法。在attach方法中又调用了ActivityManagerService中的attachApplication(mAppThread)方法,这个才是我们的重点。mAppThread是ApplicationThread范例,它也不是一个Thread,而是一个Binder,负责与远程的ActivityManagerService举行交互。在了解attachApplication之前,我们先来了解一下ApplicationThread与ActivityThread的关联,具体代码如下。


ApplicationThread与ActivityManagerService举行交互,通过H类的对象mH来发送消息,而且在H类的handleMessage函数中举行处理,然后handleMessage根据消息的范例调用ActivityThread中对应的方法,比如handleResumeActivity来使Activity变得可见等。因为H的消息队列就是主线程的消息队列,因此,这些过程都在UI线程中处理
下面继续关注ActivityManagerService中的 attachApplication(mAppThread)。


在省略了一些代码之后,我们抓住了需要关心的点,即注释3处,这里才是真正启动Activity的地方mStackSupervisor的范例为ActivityStackSupervisor,我们看看注释3处的ActivityStackSupervisor类中的attachApplicationLocked。

在attachApplicationLocked函数中会遍历整个Activity栈,而且找出栈顶ActivityStack,然后调用realStartActivityLocked函数来真正启动 Activity。我们看看realStartActivityLocked函数。


程序实现起来较复杂,最终在注释1处调用了ActivityThread中的内部类的ApplicationThread中的scheduleLaunchActivity函数该函数中会发送一个H.LAUNCH_ACTIVITY消息给UI线程, 具体代码如下。

ActivityThread的内部类H的handleMessage函数中会处理该消息,然后会调ActivityThread的 handleLaunchActivity(ActivityClientRecord r, Intent customIntent)函数,在这个函数中会创建将要启动的Activity,而且调用其生命周期函数onCreate、onResume。ApplicationThread中的scheduleLaunchActivity函数的代码在上文已经给出,下面我们看看ActivityThread的handleLaunchActivity(ActivityClientRecord r, Intent customIntent)函数。


如上述代码所示,在调用ActivityThread的handleLaunchActivity(ActivityClientRecord r, Intent customlntent)后会调用 performLaunchActivity函数来创建Activity,而且将Activity与Application关联上,然后调用Activity的onCreate、onStart函数。再之后是调用handleResumeActivity函数, 该函数的实现如下。

handleResumeActivity函数又调用 performResumeActivity函数往返调Activity的onResume函数,而且DecorView添加到WindowManager中,最后将Activity的DecorView设置为可见,而且通知ActivityManagerService渲染视图,因此,在onResume函数之后,Activity就显示在屏幕上了。 时序图如图15-4所示。

也就是说,在调用ActivityThread的main方法之后,会依次执行如图15-5所示的流程。

这就是一个典范的模板方法,ActivityThread的main函数被调用之后,依次执行Activity的 onCreate、onStart、onResume函数,用户通常在Activity的子类中覆写onCreate方法,而且在该方法中调用setContentView来设置布局。从上文我们知道,在onResume之后,Activity的布局内容就显示到窗口上了,那么用户的布局视图是如何添加到Activity的呢?我们照旧从源码中寻找答案。
首先看看Activity中的setContentView到底做了什么。

我们可以看到,实际上调用了mWindow的setContentView方法,本书中我们己经多次提到Window的实现类为PhoneWindow,我们就看看PhoneWindow的setConentView,其焦点源码如下。

我们可以看到,setContentView的根本流程简单概括就是如下几步
(1) 构建mDecor对象。mDecor就是整个窗口的顶层视图,它主要包罗了Title和ContentView两个区域,Title区域就是我们的标题栏,ContentView区域就是显示xml布局内容中的区域
(2) 设置一些关于窗口的属性,初始化标题栏区域和内容显示区域
这里比较复杂的就是generateLayout(mDecor)这个函数,我们一起继续深入分析。

其步调如下所示。

  • 获取用户设置的一些属性与Flag
  • 根据一些属性选择差别的顶层视图布局,如FEATURE_NO_TITLE则选择没有title的布局文件等。
    这里我们选择layoutResource =com.android.internal.R.layout.screen_title 的情形来分析,看screen_title.xml的布局代码。


    我们可以看到有两个区域,即title区域和content区域,generateLayout函数获取content区域的代码如下。

    获取的就是xml中id为content的FrameLayout,这个content就是内容显示区域,整个布局对应的效果如图15-6所示。

    这两个区域就组成了mDecor视图,我们的main_activity.xml就是放在内容视图这个区域的。
  • 加载顶层布局文件,转换为View,将其添加到mDecor中
  • 获取内容容器Content Parent,即用于显示内容的区域
  • 设置一些配景图和title等
颠末这几步,我们就得到了mContentParent,这就是用来装载视图的ViewGroup,再回过头来看setContentView函数。

实际上就是将layoutResId这个布局的视图附加到mContentParent中,这个layoutResId就是开发人员在onCreate函数中设置给Activity的谁人布局资源id,也这是说Google预先设置了一些布局资源,这些布局资源内里有一个留给开发者们设置窗口内容的区域,也就是content区域,我们通过setContentView设置的布局会被添加到content布局中,这个content布局是一个FrameLayout,因此,它是一个ViewGroup,如图15-7所示。 

ViewGroup从语义上来说就是视图组,它也继续自View类,它实在就是视图的容器。Android中通过ViewGroup来组织、管理子视图,比方常见的FrameLayout、LinearLayout、ListView等都是ViewGroup范例,总之,只要能包罗其他View大概ViewGroup的都是ViewGroup范例。而View就是UI界面上的可见的组件范例,任安在UI上可见的都为View的子类,TextView、Button、ImageView、FrameLayout、LinearLayout、ListView等都是View的子类。这样一来,Android 就可以使用ViewGroup和View来构建视图树,如图15-8所示。

这样,ViewGroup范例的视图管理和嵌套在内里的ViewGroup以及View控件组成了丰富多彩的用户界面。
当Activity启动时,通过onCreate函数让用户设置自己的界面,系统将这个布局界面添加到一个内置的布局界面的content区域中,此时,DecorView就创建起来了。然后调用onStart函数,而且在调用onResume函数之前将DecorView添加到WindowManager中,而且设置Activity为可见,然后通知ActivityManagerService,该Activity已变为resume状态,使得系统可以或许渲染Activity的视图,至此,Activity的视图就会显示在手机上了
15.8 模板方法实战

做什么事都得有个顺序,软件开发也是一个道理,比方,要举行一个网络请求,那必须先创建请求,然后执行请求、分析返回的数据、将效果回调给UI,不管如何厘革,这4个整体步调根本稳定,这就是一个流程。尚有上述所说的Activity的显示流程、AsyncTask的执行流程等,都是顺序化执行的体现。
从上文的讲授中,各人对于模板方法也有了一定的了解,下面我们就以实战的角度来看看模板方法在开发中的运用。
小民的ImageLoader颠末一步一步的重构变得越来越“成熟”起来,但总体来说,图片加载流程照旧相对固定的,厘革的只是某些具体的细节实现, 加载流程大抵如图15-9所示。

图片加载请求创建之后,首先检测是否有缓存,如果没有则从网络上下载该图片,而且将图片添加到缓存中,最后将图片分发给UI线程举行更新,这些流程被封装在Loader的内部,外部只需要调用对应的接口即可。
首先用户通过ImageLoader的displayimage接口来请求加载图片到某个ImageView中,相关代码如下。

在displayimage函数中会将ImageView、图片uri、listener以及请求设置封装到一个BitmapRequest中,而且将这个请求添加到请求队列,等候加载线程的执行。在初始化ImageLoader时会默认启动4个图片加载线程,它们会在配景不绝地从请求队列中获取请求,而且交给对应的Loader加载,这些图片加载线程我们定名为RequestDispatcher,相关代码如下。


在RequestDispatcher的run函数中会不绝地从请求队列中获取请求,然后通过请求图片的uri获取到对应的schema。这个schema就是一个字符串,比方,网络图片是以http大概https开头的,那么schema就为http大概https。对于当舆图片来说,它的uri格式是"file://+绝对路径"。因此,当舆图片的schema就是file://。这些schema与Loader的对应关系我们通过LoaderManager举行管理,相关代码如下。

LoaderManager是一个单例,在构造LoaderManager实例时会默认注册3种形式的图片uri与Loader到mLoaderMap中,这样在外部需要Loader时只需通过schema就可以获取到,这个Loader就是负责加载图片的角色,比方从网络上加载、从当地加载等,AbsLoader实现了Loader接口,而且将loadlmage设置为final,使得子类不可覆写该方法。更为重要的是,在loadlmage中指定了图片加载的流程,也就是如图15-8所示的先判断是否有缓存,没有缓存再加载,加载乐成添加到缓存,最后分发到调用端程序中,相关代码如下。

AbsLoader中的loadlmage就是一个模板方法,它通过loadlmage规定了加载图片的执行流程,而且将该方法设置为final,这样子类就不可以或许粉碎这个固定的执行流程,子类可以修改的就是具体的加载过程,这个工作由onLoadlmage函数实现,比方,从网络上加载图片。由于loadlmage函数是在RequestDispatcher的run函数中调用的,因此,onLoadlmage也是执行在子线程RequestDispatcher中,所以,onLoadlmage在这里是一个同步函数。
我们看看加载网络图片和加载当舆图片两个类的简单实现,具体代码如下。


这两个Loader继续自AbsLoader,而且只实现了onLoadlmage函数,URLoader从网络上加载图片,因此,它需要创建http请求,读取网络上的输入流,通过这个输入流来构建图片。而LocalLoader则是加载当舆图片,获取到图片的当地路径之后,通过BitmapFactorydecodeFile函数即可创建图片。当然,在上述代码中为了更简便,我们省略了对图片举行缩放操纵,图片缩放是很重要的一步,因此,如果没有这一步很大概会造成OOM,而且如果图片许多,要显示的ImageView很小,此时将图片完整地加载进内存也是很消耗资源的。
在onLoadlmage从具体的泉源加载了图片之后,最终会调用AbsLoader中的deliveryToUIThread(request, resultBitmap)函数,该函数是将加载得到的图片分发给调用端,因此,我们在构建BitmapRequest时就有一个Listener参数,这个参数就是调用端通报进来的,此时获取到Bitmap之后,就可以将该listener的回调函数执行到UI线程中,这样就使得调用端可以在回调函数中直接更 新UI了,至此,整个加载过程结束。
本章只关注AbsLoader的加载流程,也就是loadimage中的执行过程,大抵分为如下几步:

  • 判断是否有缓存;
  • 如果没有缓存则加载图片,而且再缓存起来;
  • 将图片分发给调用端,这个过程执行在UI线程。
Loadlmage函数是final的,也就是子类不允许修改执行流程,而只能修改具体的某一步。在该示例中,子类可以或许定制化的就是onLoadlmage函数,也就是具体的图片加载函数,比方,UrlLoader从网络上加载、LocalLoader从当地文件加载,这样通过封装流程,使得固定的执行顺序不能被子类随意修改,而子类只能替换掉某些具体的算法计谋,不光可以或许复用父类的代码,也能制止功能遭到粉碎。对外部客户端程序来说,模板方法模式隐藏了具体的执行流程,对外暴露一个统一的接口,使得客户端程序的使用资本更小,接口更简便。
15.9 小结

模板方法模式用4个字概括就是:流程封装。也就是把某个固定的流程封装到一个final函数中,而且让子类可以或许定制这个流程中的某些大概所有步调,这就要求父类提取共用的代码,提升代码的复用率,同时也带来了更好的可扩展性。
优点


  • 封装稳定部分,扩展可变部分
  • 提取公共部分代码,便于维护
缺点
模板方法会带来代码阅读的难度,会让用户觉得难以理解。

来源:https://blog.csdn.net/aha_jasper/article/details/111873890
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

发布主题

专注素材教程免费分享
全国免费热线电话

18768367769

周一至周日9:00-23:00

反馈建议

27428564@qq.com 在线QQ咨询

扫描二维码关注我们

Powered by Discuz! X3.4© 2001-2013 Comsenz Inc.( 蜀ICP备2021001884号-1 )