外观模式(Facade)在开发过程中的运用频率非常高,尤其是在现阶段,各种第三方SDK"充斥”在我们的周边,而这些SDK大多会使用外观模式。通过一个外观类使得整个系统的接口只有一个统一的高层接口,这样能够降低用户的使用成本,也对用户屏蔽了很多实现细节。当然,在我们的开发过程中,外观模式也是我们封装API的常用手段,例如网络模块、ImageLoader模块等。 可能你已经在开发中运用过无数次外观模式,只是没有在理论层面认识它,本章我们就从理论结合实践上学习这个模式。
要求一个子系统的外部与其内部的通信必须通过一个统一的对象进行。门面模式(Facade模式)提供一个高层次的接口,使得子系统更易于使用。
生活中使用外观模式的例子非常多,任何一个类似中央调度结构的组织都类似外观模式。举个简单的例子,手机就是一个外观模式的例子,它集合了电话功能、短信功能、GPS、拍照等于一身,通过手机你就可以完成各种功能。而不是当你打电话时使用一个诺基亚1100,要拍照时非得用一个相机,如果每使用一个功能你就必须操作特定的设备,会使得整个过程很繁琐。而手机给了你一个统 一的入口,集电话、上网、拍照等功能于一身,使用方便,操作简单。结构图如图23-3所示。
下面我们来简单模拟一下手机的外观模式实现,首先我们建立一个MobilePhone代码大致如下。
MobilePhone类中含有两个子系统,也就是拨号系统和拍照系统,MobilePhone将这两个系统封装起来,为用户提供一个统一的操作接口,也就是说用户只需要通过MobilePhone这个类就可以操作打电话和拍照这两个功能。用户不需要知道有Phone这个接口以及它的实现类是Phonelmpl,同样也不需要知道Camera相关的信息,通过MobilePhone就可以包揽一切。而在MobilePhone中也封装了两个子系统的交互,例如视频电话时需要先打开摄像头,然后再开始拨号,如果没有这一步的封装,每次用户实现视频通话功能时都需要手动打开摄像头、进行拨号,这样会增加用户的使用成本,外观模式使得这些操作更加简单、易用。
我们来看看Phone接口和Phonelmpl。
代码很简单,就是单纯的抽象与实现。Camera也是类似的实现,具体代码如下。
运行结果:
从上述代码中可以看到,外观模式就是统一接口封装。将子系统的逻辑、交互隐藏起来,为用户提供一个高层次的接口,使得系统更加易用,同时也对外隐藏了具体的实现,这样即使具体的子系统发生了变化,用户也不会感知到,因为用户使用的是Facade高层接口,内部的变化对于用户来说并不可见。这样一来就将变化隔离开来,使得系统也更为灵活。
在用Android开发过程中,Context是最重要的一个类型,Context意为上下文,也就是程序的运行环境。它封装了很多重要的操作,如startActivity、sendBroadcast()、bindService等,因此,Context对开发者来说是最重要的高层接口。Context只是一个定义了很多接口的抽象类,这些接口的功能实现并不是在Context及其子类中,而是通过其他子系统来完成,例如startActivity的真正实现是通过ActivityManagerService,获取应用包相关信息则是通过PackageManagerService。Context只是做了一个高层次的统一封装,正如上文所示,Context只是一个抽象类,它的真正实现在Contextlmpl类中,Contextlmpl就是今天我们要分析的外观类。
在本书的前面章节中已经提到多次,在应用启动时,首先会fork一个子进程,并且调用ActivityThread.main方法启动该进程。ActivityThread又会构建Application对象,然后和Activity、Contextlmpl关联起来,最后会调用Activity的onCreate、onStart、onResume函数使Activity运行起来,此时应用的用户界面就呈现在我们面前了。main函数会间接地调用ActivityThread中的handleLaunchActivity函数启动默认的Activity, handleLaunchActivity代码如下。
在handleLaunchActivity函数中会调用 perfromLaunchActivity函数执行Applicaton、Contextlmpl、Activity的创建工作,并且通过Activity类的attach函数将这3者关联起来,相关代码在注释4处。 而Activity本身又是Context的子类,因此,Activity就具有了Context定义的所有方法。但Activity并不实现具体的功能,它只是继承了Context的接口,并且将相关的操作转发给Contextlmpl对象。
这个Contextlmpl存储在Activity的上两层父类ContextWrapper中,变量名为mBase,具体代码如下。
在ActivityThread类的perfromLaunchActivity函数中会调用Activity的attach方法将Contextlmpl等对象关联到Activity中,这个Contextlmpl最终会被Contentwrapper类的mBase字段引用。我们先看看attach方法的内部实现。
attach函数主要就是一些赋值操作,这里我们只关心mBase的初始化。在attach函数中,第一句就调用了attachBaseContext函数,该函数定义在ContextWrapper类,它就是简单地将Context参数传递给mBase字段。此时,我们的Activity内部就持有了Contextlmpl的引用。
Activity在开发过程中部分充当了代理的角色,例如,当我们通过Activity对象调用sendBroadcast、getResource等函数时,实际上Activity只是代理了ContextImpl的操作,也就是内部都调用了mBase对象的相应方法来处理,这些操作被封装在Activity的父类的ContentWrapper中。代码如下所示。
既然Contextlmpl那么重要,包含了各个系统服务的调用与操作,那么我们来看看它的相关实现。
从上述程序中可以看到,Contextlmpl内部封装了很多不同子系统的操作,例如,Activity的跳转、发送广播、启动服务、设置壁纸等,这些工作并不是在Contextlmpl中实现,而是转交给了具体的子系统进行处理。通过Context这个抽象了、定义了一组接口,Contextlmpl实现Context定义的接口,这使得用户可以通过Context这个接口统一进行与Android系统的交互,这样用户通常情况下就不需要对每个子系统进行了解,例如启动Activity时用户不需要手动调用mMainThread.getInstrurnentation().execStartActivity函数进行执行,发送广播时也不需要直接操作ActivityManagerNative类。用户与系统服务的交互都通过Context的高层接口,这样对用户屏蔽了具体实现的细节,降低了使用成本。
通过Contextlmpl的封装之后,用户与系统服务之间的交互如图23-4所示。
从上述示例来看,外观模式的实现非常简单, 没有复杂的类型结构,只是通过一组高层接口封装了各个子系统的操作,并且统一提供给用户。试想一下如果没有外观模式的封装,那么用户就必须知道各个子系统的相关细节,甚至它们之间的协作流程,当子系统较多时这些相互之间的关系就会很乱,导致用户在使用相关功能时难度也会变大,易于出错。而使用外观模式的封装就避免了用户需要与多个子系统进行交互,降低了用户的使用成本,对外也屏蔽了具体细节,保证了系统的易用性、稳定性。
在Android开发中,我们为了屏幕适配通常会为一个应用做多套资源,使得在不同分辨率的设备上能够尽量保持一致的UI效果。那么我们不禁要问Android的资源在哪?读取资源的形式又是怎样?下面我们就来简单分析一下Android的资源机制。
其实在Android应用程序资源的编译和打包之后就生成一个资源索引表文件resources.arsc,这个应用程序资源会被打包到APK文件中。Android应用程序在运行的过程中,通过一个称为Resource来获取资源,但实际上Resource内部又是通过一个叫AssetManager的资源管理器来读取打包在APK文件里面的资源文件。那么AssetManager如何与应用关联在一起,它又是如何找到应用的资源,这就是我们本节要关注的重点。
我们在上文中说到,获取资源的操作实际上是由Contextlmpl来完成的,Activity、Service等组件的getResource函数最终都转发给了ContextImpl类型的mBase字段。也就是调用了Contextlmpl的getResource函数,而这个Resource在Contextlmpl关联到Activity之前就会初始化Resource对象,相关代码如下。
在上述performLaunchActivity函数中,首先创建了Activity与Application,然后通过 createBaseContextForActivity创建了一个Contextlmpl对象。而在createBaseContextForActivity函数中又调用了Contextlmpl类的createActivityContext静态函数,我们看看源码。
createActivityContext函数中最终调用了Contextlmpl的构造函数,在该函数中会初始化该进程的各个字段,例如资源、包信息、屏幕配置等。这里只关心与资源相关的代码。在通过mPackagelnfo得到对应的资源之后,会调用ResourcesManager的getTopLevelResources来根据设备配置等相关信息获取到对应的资源,也就是资源的适配。getTopLevelResources代码如下。
首先会以APK路径、屏幕设备id、配置等构建一个资源key,根据这个key到ActivityThread类的mActiveResources(HashMap类型)中查询是否已经加载过该APK的资源,如果含有缓存那么直接使用缓存。这个mActiveResources维护了当前应用程序进程中加载的每一个APK文件及其对应的Resources对象的对应关系。如果没有缓存,那么就会新创建一个,并且保存在mActiveResources中。
在没有资源缓存的情况下,ActivityThread会新创建一个AssetManager对象,并且调用AssetManager对象的addAssetPath函数来将参数resDir作为它的资源目录,这个resDir就是APK文件的绝对路径。创建了一个新的AssetManager对象之后,会将这个AssetManager对象作为Resource构造函数的第一个参数来构建一个新的Resources对象。这个新创建的Resources对象会以前面所创建的ResourcesKey对象为键值缓存在mActiveResources所描述的一个HashMap中,以便重复使用该资源时无需重复创建。
接下来,我们首先分析AssetManager类的构造函数和成员函数addAssetPath的实现,接着再分析Resources类的构造函数的实现,以便可以了解用来访问应用程序资源的AssetManager对象和Resources对象的创建以及初始化过程。
在AssetManager的构造函数中,首先会调用init函数进行初始化然后再调用ensureSystemAssets函数来加载系统资源,这些系统资源存储在mSystem对象中,mSystem也是AssetManager类型。需要注意的是,init函数并不是一个Java函数,而是一个Native层的方法,它的实现在android_util_AssetManager.cpp文件中,具体代码如下。
在init函数中首先创建了一个Native层的AssetManager对象,然后添加了默认的系统资源,最后将这个AssetManager对象转换为整型并且传递给Java层的AssetManager的mObject字段,这样Java层就相当于存有了一个Native层AssetManager的句柄。这里我们关注的是addDefaultAssets函数,具体代码如下。
代码也比较简单,就是拼接系统资源路径,最好将该路径传递给addAssetPath函数中,实际上就是将系统资源路径添加到AssetManager的资源路径列表中。
此时,在ActivityThread的getTopLevelResources函数中的new AssetManager的过程就执行完毕了,然后继续执行AssetManager对象的addAssetPath函数,具体代码如下。
在Java层的AssetManager的addAssetPath函数中实际上调用的是Native层的addAssetPathNative
函数。需要注意的是,这个path参数必须是一个目录或者是一个zip压缩文件(APK本质上就是1个zip文件)路径。这个addAssetPathNative函数在Native层对应的函数为android_content_AssetManager_addAssetPath,该函数定义在android_util_AssetManager.cpp文件中
。
在android_content_AssetManager_addAssetPath函数中会调用assetManagerForJavaObject函数先从Java层的AssetManager对象中获取到mObject字段,该字段存储了由Native层AssetManager指针转换而来的整型值。此时需要通过这个整型值逆向地转换为Native层的AssetManager对象。然后将APK路径添加到资源路径中,这样也就含有了应用本身的资源。
我们再来分析这个过程,首先在Java层的AssetManager构造函数中调用了init函数初始化系统资源,创建与初始化完毕之后又调用了Java层AssetManager对象的addAssetPath函数将应用APK的路径传递给Native层的AssetManager,使得应用的资源也被添加到AssetManager中,这样资源路径就构建完毕了。
最后在ResourcesManager的getTopLevelResources函数中将初始化好的AssetManager、设备配置等作为参数来构造Java层的Resource对象,也就是上文中getTopLevelResources函数中的注释4处。Resource的构造函数如下。
Resources类的构造函数首先将参数assets所指向的一个AssetManager对象保存在成员变量mAssets中,我们获取资源就是通过这个AssetManager对象进行操作。接下来调用updateConfiguration函数来设置设备配置信息,最后调用AssetManager的成员函数ensureStringBlocks来创建字符串资源池。
我们来看看updateConfiguration函数的实现。
Resources类的成员函数updateConfiguration首先是根据参数config和metrics来更新设备的当前配置信息,例如,屏幕大小和密码、国家(地区)和语言、键盘配置情况等,接着再调用成员变量mAssets所指向的一个Java层的AssetManager对象的成员函数setConfiguration来将这些配置信息设置到与之关联的C++层的AssetManager对象中。这样一来,在我们通过Resource获取资源时,Native层就会根据这个配置信息寻找最适合的资源返回,从而达到多屏幕适配的效果。
从上述的分析中我们可以知道,Activity、Contextlmpl、Resource、AssetManager的结构图如图23-5所示。
经过上述的分析,我们也可以得出一个结论:应用的资源存储在APK中,一个AssetManager可以管理多个路径的资源文件,Resource通过AssetManager获取资源。那就是说我们可以通过AssetManager的addAssetPath方法添加APK路径以达到一些资源替换或者换肤的效果,还有类似于动态加载一个未安装的APK时也可以通过这种形式加载插件APK的资源,使得插件Activity等组件可以通过资源类R来访问应用资源。
在上一节中我们学习了Android中的资源机制,本节我们将资源机制作为基本知识点来实现一个简单的动态加载框架。在开始之前我们需要知道,未安装的APK文件是可以通过DexClassLoader进行加载、运行的,具体代码如下。
其中mContext是一个Context类型的对象,apkPath就是这个APK在手机中的绝对路径,dexOutputDir就是APK解压出的dex文件的存放目录。通过这种形式就有了一个可以加载这个APK的ClassLoader。然后通过这个ClassLoader加载这个APK默认启动的Activity,并且通过反射调用这个插件Activity的onCreate、onStart、onResume函数就会加载这个应用。
我们的实现原理当然没有上面所说的那么简单,还需要处理的问题包括加载插件APK的资源、处理它的生命周期函数等。不过在此之前,首先做的还是构建能够加载插件APK的ClassLoader和资源,我们新创建了一个PluginManager类来完成这些工作,具体代码如下。
在PluginManager类中以插件APK的包名为key,插件APK信息为值缓存了插件相关的信息。加载插件APK时会从map中检测是否含有缓存,如果有则使用缓存的APK,否则通过DexClassLoader和资源创建一个PluginApk对象,最后将这个对象缓存到map中。创建PluginApk的代码是createApk函数,具体代码如下:
在createApk函数中首先通过AssetManager对象添加了插件APK所在的绝对路径,使得AssetManager可以获取到该插件APK的资源,然后又通过该AssetManager和设备配置等创建了Resource对象,并且将这个对象存储到PluginApk中,最后构建插件APK的DexClassLoader并存储在PluginApk对象中。最后这个PluginApk对象会缓存到PluginManager的信息表中。
在启动插件之前,用户需要调用PluginManager的loadApk函数加载插件APK的相关信息,也就是上述所说的资源、DexClassLoader等,然后再调用PluginManager的startActivity函数启动插件,具体代码如下。
需要注意的是,启动插件时需要在Intent中传递插件APK的包名和默认加载的Activity类名,否则会抛出异常。因为PluginManager需要知道插件APK默认的启动Activity才能将插件APK运行起来。还有一个重要的地方是,我们在startActivity中会重新构建一个Intent,这个Intent启动的却是ActivityProxy这个类。我们是要启动插件APK的默认Activity,怎么换成了ActivityProxy呢?
实际上我们动态加载的并不是插件APK的Activity,而是一个Activity空壳,也就是这里的ActivityProxy。在这个空壳Activity中包装了插件Activity的声明周期,也就说插件Activity和ActivityProxy是共存的。相关代码如下。
在ActivityProxy中有一个LifbCircleControIler,这个类就负责通过DexClassLoader加载插件APK的Activity,我们看到ActivityProxy的声明周期函数中都调用了LifeCircleController对象对应的方法,而LifeCircleController中实际上又调用了插件Activity的生命周期方法。我们看看LifeCircleController的实现。
在 ActivityProxy 的 onCreate 函数中调用了 LifeCircleController 的 onCreate 函数,在 LifeCircleControIler的onCreate函数中主要做了如下几步操作。
在PluginManager的startActivity函数中我们强调过,跳转到插件Activity的Intent必须包含插件Activity的类路径、包名就是为了在这一步使用。获取到类名之后再得到DexClassLoader,最后动态加载插件Activity,并且将ActivityProxy注入到插件Activity中。最后调用插件Activity的onCreate函数。
对于插件Activity我们也定义了一个基类,叫做PluginActivity,具体代码如下。
从上述程序中可以看到,在PluginActivity中所有的函数几乎都转交给了ActivityProxy处理,比如setContentView,实际上也是设置给了ActivityProxy对象。此时我们可以思考一下,我们在开发插件时会继承自PluginActivity,然后在onCreate函数中设置setContentView为插件Activity设置布局内容,而PluginActivity调用的却是ActivityProxy的setContentView函数,就相当于是为ActivityProxy对象设置内容布局。当我们要启动插件Actiivty时实际上又是启动ActivityProxy,又在ActivityProxy的onCreate函数中通过LifeCircleController动态地用DexClassLoader加载插件Activity,并且调用它的onCreate函数、setContentView方法等,而setContentView实际上调用了ActivityProxy的setContentView,在调用 ActivityProxy的onStart、onResume之后ActivityProxy就显示到我们面前了,它的视图就是通过插件Activity设置的内容
。
一句话概括就是:ActivityProxy加载的是插件Activity的内容与资源。这样一来,插件Activity就会以ActivityProxy的形式展现出来。
例如,我们有一个插件APK在SD卡的plugins目录下,它的包名为com.example.plugin,默认启动的Activity类路径为com.example.plugin.MainActivity。首先需要一个宿主应用来加载插件应用,宿主应用中要加载插件APK时代码调用如下。
我们再创建一个插件工程,这个工程需要引用动态加载框架的代码,然后新创建一个继承自PluginActivity的插件Activity,具体代码如下。
生成APK之后将该插件APK命名为plugin.apk,放到SD卡的plugins目录下,然后运行宿主APK,而在宿主APK中又会加载plugin.apk,此时得到的效果如图23-6和图23-7所示。
在宿主中,我们通过PluginManager启动了插件APK。但是,实际上启动是ActivityProxy,只是我们将插件Activity的contentview设置给了ActivityProxy,这样看起来像是启动了插件Activity。
在这个过程中最终的两点就是contentview的设置和资源的替换。当然这只是一个最基本的实现,通过这个示例只是让大家对于动态加载有一个基本的认识,更完善的动态加载框架请查看https://github.com/singwhatiwanna/dynamic-load-apk,也希望更多的人参与到这个开源项目来。
对于SDK和开源库来说,外观模式通常是使用率最高的模式,这些库通过外观类为用户提供统一的高层接口,使得用户不必了解一些更细节的实现。例如在使用ImageLoader时我们通常只操作一个ImageLoader类就可以完成,而不要了解网络请求类、缓存类以及它们的交互细节。又比如在使用友盟统计SDK时,我们基本上通过MobclickAgent这个类就可以完成我们所需的功能,至于MobclickAgent的内部有其他的什么类型、它们的具体交互是什么我们都不需要关心,这个MobclickAgent类也就是一个外观类。可见,外观模式实际上在我们的开发中无处不在,今天我们还是以小民的ImageLoader(类名为SimplelmageLoader,简称ImageLoader)为例来讲述外观模式的运用。
在本书的前面章节中我们讲到,用户在使用ImageLoader时只需要操作ImageLoader类,其他的类型基本不用关心。ImageLoader封装了内部逻辑,通过ImageLoader的接口用户就可以完成图片加载的操作。核心代码如下。
在ImageLoader类中对外暴露的函数基本只有displayImage,也就是加载图片的函数。最简单的情况下,用户只需要传递ImageView和图片的uri即可实现图片加载。在该函数中,首先会将传递进来的参数转换为一个BitmapRequest对象,然后将该对象传递到请求队列中。请求队列的初始化就在ImageLoader的初始化函数中。该请求队列初始化之后就会启动CPU数量+1个的请求处理线程,在这些线程的run中不断地从请求队列中获取请求、加载请求、最后将图片投递到UI线程更新ImageView。首先我们来看消息队列的初始化。
在RequestQueue中会启动指定数量的RequestDispatcher线程,每个RequestDispatcher本质上是一个线程,在它们的run函数中有一个死循环不断地查询请求队列中是否含有请求,并且处理这些请求。
在RequestDispatcher的run方法中主要分为4步。
从消息队列中获取消息
根据请求的uri地址获取图片的uri schema
根据schema获取对应的图片Loader
加载图片,并且将结果传递给UI线程,更新ImageView
从上述代码看,RequestDispatcher中又封装了LoaderManager系统,而在LoaderManager中又管理了各个Loader类,而Loader类中又管理了图片的加载流程、图片缓存等逻辑。而这些相关的类型、逻辑都被封装在ImageLoader类的外衣之下,对于用户来说它们根本不知道这些类型的存在,几乎所有的操作都是通过SimplelmageLoader这个类完成。它们的结构如图23-8所示。
从图23-8中我们可以看到, ReuqestQueue等类型都被封装在了SimplelmageLoader类之下,用户根本不知道这些类型的存在,而只需要操作SimplelmageLoader的接口就可以完成图片加载的操作。这样就避免了暴露过多的实现细节,也使得ImageLoader的使用更为简单,试想一下,每次都需要手动构建一个BitmapRequest类,然后添加到请求队列中,那么整个过程就增加了用户的使用成本,用户也直接依赖了具体的实现,这对于系统的升级、维护造成了一定的困难。通过SimplelmageLoder类将这些具体细节都隐藏起来,在降低用户使用成本的同时也增加了灵活性,例如,当你需要替换RequestDispatcher为线程池实现时,用户并不能感知到,因为原来的用户并不知道内部实现有RequestDispatcher这个类型,这就能很好地在不对用户产生影响的情况下对产品进行升级、维护。
外观模式是一个使用频率较高的设计模式,它的精髓就在于“封装”二字。通过一个高层次结构为用户提供统一的API入口,使得用户通过一个类型就基本能够操作整个系统,这样减少了用户的使用成本,也能够提升系统的灵活性。
优点
缺点