Android里面的一些设计思想感悟
一切皆文件
一切皆文件不是Android的首创,是Linux/Unix的首创。Linux一个非常好的设计就是一切皆文件的概念,怎么理解呢,可以从两个方面理解。
一个是应用程序使用者,一个是具体程序实现,Linux一切皆文件的概念主要是对驱动来说的,应用程序需要使用设备,设备千差万别,标准也是各不相同,是不是每种设备都需要单独去写,使用它的接口也是不同的呢?当然不能这样,否则,这样的程序无混乱不堪,根本无法移植。
Linux把所有的设备都抽象成文件,不管你是访问打印机,使用键盘,鼠标,耳机,访问网络,创建一个socket连接,进程间的通信等,他们都是一个文件,应用程序需要访问的就是访问一个文件,按照访问文件的标准流程来就可以了,打开文件,读写文件,最后关闭。编写的使用设备文件的逻辑就被简化为访问一个文件,使用者不用写大量的代码即可实现自己的业务,非常简单。
对于设备程序的实现者来说,也很简单,他只需要实现一个文件的具体业务逻辑即可。只需要实现文件的打开,读,写,轮询等这些逻辑。实现者也可以不用写非常多的代码即可实现自己的逻辑。
框架是什么,框架就是方便程序的使用者和具体的实现者,框架就是给开发者,实现者统一的接口,非常简洁的实现,可以做到傻瓜式编程,甚者,有一些框架把开发者变成了配置工程师,只需要配置就实现了自己的业务逻辑。
看看Linux具体是怎样设计实现的。
总线,驱动,设备
Linux外围设备驱动,是通过bus,driver,device来进行管理的,任何程序想要跑起来都需要cpu给它分配时间片,外设都是通过总线来和cpu通信的。Linux已经有一套完整的总线,设备的完整框架,驱动工程师只需要正确地注册自己的驱动,实现自己的设备文件程序即可。我们慢慢看看总线,驱动,设备是如何协同工作的。
总线相当于驱动和设备的总管,具体的一个设备实现就是一个设备,而连接到总线的设备就是一个驱动。
总线在软件层面主要是负责管理设备和驱动。
设备要让系统感知自己的存在,设备需要向总线注册自己;同样地,驱动要让系统感知自己的存在,也需要向总线注册自己。设备和驱动在初始化时必须要明确自己是哪种总线的,I2C设备和驱动不能向USB总线注册吧。
多个设备和多个驱动都注册到同一个总线上,那设备怎么找到最适合自己的驱动呢,或者说驱动怎么找到其所支持的设备呢?
这个也是由总线负责,总线就像是一个红娘,负责在设备和驱动中牵线。
设备会向总线提出自己对驱动的条件(最简单的也是最精确的就是指定对方的名字了),而驱动也会向总线告知自己能够支持的设备的条件(一般是型号ID等,最简单的也可以是设备的名字)。
设备在注册的时候,总线就会遍历注册在它上面的驱动,找到最适合这个设备的驱动,然后填入设备的结构成员中;驱动注册的时候,总线也会遍历注册在其之上的设备,找到其支持的设备(可以是多个,驱动和设备的关系是1:N),并将设备填入驱动的支持列表中。我们称总线这个牵线的行为是match。牵好线之后,设备和驱动之间的交互红娘可不管了。
总线在匹配设备和驱动之后驱动要考虑一个这样的问题,设备对应的软件数据结构代表着静态的信息,真实的物理设备此时是否正常还不一定,因此驱动需要探测这个设备是否正常。我们称这个行为为probe,至于如何探测,那是驱动才知道干的事情,总线只管吩咐得了。
device和driver绑定
当增加新device的时候,bus 会轮循它的驱动列表来找到一个匹配的驱动,它们是通过device id和 driver的id_table来进行 ”匹配”的,主要是在 driver_match_device()[drivers/base/base.h]通过 bus->match() 这个callback来让驱动判断是否支持该设备,一旦匹配成功,device的driver字段会被设置成相应的driver指针:
really_probe()
{
dev->driver = drv;
if(dev->bus->probe) {
ret =dev->bus->probe(dev);
...
} else if(drv->probe) {
ret =drv->probe(dev);
...
}
}
然后 callback 该 driver 的 probe 或者 connect 函数,进行一些初始化操作。
同理,当增加新的driver时,bus也会执行相同的动作,为驱动查找设备。因此,绑定发生在两个阶段:
1: 驱动找设备,发生在driver向bus系统注册自己时候,函数调用链是:
driver_register --> bus_add_driver -->
driver_attach() [dd.c] -- 将轮循device链表,查找匹配的device。
2: 设备查找驱动,发生在设备增加到总线的的时候,函数调用链是:
device_add --> bus_probe_device -->
device_initial_probe --> device_attach --将轮循driver链表,查找匹配的driver。
匹配成功后,系统继续调用 driver_probe_device() 来 callback 'drv->probe(dev)' 或者 'bus->probe(dev)
-->drv->connect(),在probe或者connect函数里面,驱动开始实际的初始化操作。因此,probe() 或者 connect() 是真正的驱动'入口'。
对驱动开发者而言,最基本是两个步骤:
定义device id table.
probe()或connect()开始具体的初始化工作。
驱动实现者需要做的事情
驱动只需要注册了驱动,实现file_operations里面相应的操作即可
struct file_operations
{
int (*open)(struct inode *, struct file *);
int (*ioctl)(struct inode *, struct file *, ...);
ssize_t(*read) (struct file *, char __user *,...);
ssize_t(*write) (struct file *, const char __user *, ...);
unsignedint(* poll )(struct file *, structpoll_table_struct *);
…
}
而驱动probe成功之后,通过sysfs文件系统、uevent事件通知机制、后台应用服务mdev程序,三者的配合,在/dev目录创建对应的设备文件。
而应用层只需要知道相应的这个节点,直接打开这个文件,进行相应的读写即可。
所有的事情就简化了,驱动实现者就需要向总线注册驱动,注册设备,实现设备file_operations里面的需要的操作,生成对应的文件节点,而应用层来访问,只需要打开这个文件,读写文件。使用者的代码全都简化为对文件的访问,实现者就只是需要实现文件的open, write, read, ioctl等。
文件大多数的数据是同步的,打开文件,直接读写,马上得到结果,但是有一些并不是数据马上准备好的,那么就需要等待,poll函数是提供给上层进行轮询,数据是否已经准备好了,它的应用主要是配合上层的I/O多路复用机制。
简单粗暴的说Linux将整个在Linux环境上的编程简化为了文件的读写,将复杂的代码编写简化为文件的读写,文件的读写也会有复杂的情况,如果有异步,如果有并发,如教科书上说的饥饿,死锁这些并发问题,也就是传说中的读写者问题,如何避免掉这些问题,Linux有一套完整的方案来解决并发读写的问题。
Linux上的一个读写者程序 I/O多路复用
select和poll机制的原理非常相近,主要是一些数据结构的不同,最终到驱动层都会执行f_op->poll(),执行__pollwait()把自己挂入等待队列。 一旦有事件发生时便会唤醒等待队列上的进程。比如监控的是可写事件,则会在write()方法中调用wakeup方法唤醒相对应的等待队列上的进程。这一切都是基于底层文件系统作为基石来完成I/O多路复用的事件监控功能。
他们的套路都是监听相应的文件上的事件f_op->poll(),当事件发生唤醒对应事件里面的等待队列,唤醒等待队列上相应的进程。唤醒后得进程就可以去读写。这个主要是对那些异步的读写操作。
这套机制在Android上又被复用了,我们看看Android的读写者程序Handler机制
一切皆Context
Android为了最大简化应用程序的编写,对Android的组件,重要接口做了一次封装,做成了Context,只要有Context就可以调用组件。
Android框架是一个C/S结构,也就是设计模式里面的代理模式,Service端就是Android服务,而Manager端是客户端,提供给应用层各种接口,Manager端和Service端通过进程间通信。Android有很多服务,对应各自的硬件,但是四大组件就一个Context提供给应用程序,可以调用框架的所有功能。
Android的Context设计跟Linux的驱动文件有异曲同工之妙,写Android应用程序,只要拿到Context就可以操作四大组件,访问磁盘,访问资源文件,进行权限校验等,几乎所有Android功能都有。这个和Linux的一切皆文件很相似,Linux对外提供的驱动都是文件,不管是管道,socket,设备节点,进程间的通信,全都是文件,所有的操作就是对文件的操作。
Context的设计在设计模式上来说是装饰者模式,主要是为了防止子类数量爆炸式增长。看看它的类图。
一个典型的Decorator模式,基类Context是一个借口定义了各种接口,主要是四大组件使用的各种接口(获取,启动四大组件,访问资源文件,访问磁盘等),ContextImpl负责实现接口的具体功能。对外提供使用时,ContextImpl需要被包装(Wrapper)一下,这就有了ContextWrapper这个修饰器。修饰器一般只是一个传递者,修饰器所有的方法实现都是调用具体的实现类ContextImpl,所以修饰器ContextWrapper需要持有一个ContextImpl的引用。
Application启动之后会绑定一个相应的Context,
看如下几处关键代码
// ActivityThread.handleBindApplication()
private voidhandleBindApplication(AppBindData data) {
...
Application app = data.info.makeApplication(data.restrictedBackupMode,null);
mInitialApplication = app;
...
try {
mInstrumentation.callApplicationOnCreate(app);
}catch (Exception e) {...}
}
// LoadedApk.makeApplication()
public Application makeApplication(booleanforceDefaultAppClass, Instrumentation instrumentation) {
...
if (forceDefaultAppClass || (appClass == null)) {
appClass = "android.app.Application";
}
try {
java.lang.ClassLoader cl = getClassLoader();
ContextImpl appContext = ContextImpl.createAppContext(mActivityThread,this);
app = mActivityThread.mInstrumentation.newApplication(
cl, appClass, appContext);
appContext.setOuterContext(app);
}catch (Exception e) {...}
...
}
// ContextImpl.createAppContext()
static ContextImplcreateAppContext(ActivityThread mainThread, LoadedApk packageInfo) {
if (packageInfo == null) throw newIllegalArgumentException("packageInfo");
return new ContextImpl(null, mainThread,
packageInfo, null, null, 0, null, null, Display.INVALID_DISPLAY);
}
// ContextImpl.constructor()
private ContextImpl(ContextImpl container,ActivityThread mainThread,
LoadedApk packageInfo, IBinder activityToken, UserHandle user, intflags,
Display display, Configuration overrideConfiguration, int createDisplayWithId){
mOuterContext = this; //外围包装器,暂时用
...
mMainThread = mainThread; //主线程
mActivityToken = activityToken; //关联到系统进程的ActivityRecord
mFlags = flags;
...
mPackageInfo = packageInfo; // LoadedApk对象
mResourcesManager = ResourcesManager.getInstance();
...
Resources resources = packageInfo.getResources(mainThread);
...
mResources = resources; //通过各种计算得到的资源
...
mContentResolver = new ApplicationContentResolver(this, mainThread,user); //访问ContentProvider的接口
}
构建Application对象完成后,便会调用其attach()函数绑定一个Context,这一绑定就相当于在ContextWrapper中关联了一个ContextImpl,这一层Decorator的修饰包装关系这就么套上了。回顾一下时序图,ActivityThread中发起Application对象的创建操作,然后创建一个真实的ContextImpl对象(AppContext),最后将AppContext包装进Application对象中,才完成整个的修饰动作,在这之后,Application便可作为一个真正的Context使用,可以回调其生命周期的onCreate()方法了。应用程序有了Context之后,就可以执行Android的几乎所有操作了,startActivity启动Activity,bindService,sendBroadcast,getContentResolver,获取andorid目录,资源文件等。
Activity,Service的创建过程也类似。
Android的应用层接口没有设计成一堆组件,一堆类,一堆接口,分成一大堆包,调用时需要引入一大堆包,调用一大堆类,非常的不利于代码管理。 Android Context设计的意义,只要有Android环境Context,就可以使用Android的任何东西,极大地方便Android应用程序的开发。
PMS,四大组件的管家
PMS的设计主要是作为四大组件的管家,专门用于解析应用的四大组件及权限,解析后每个组件都有一个缓存。当应用程序有了Context,配合着Intent,几乎可以执行Android层的操作。当应用发起了调用,Android会到PMS的组件缓存中进行查询,如果有对应组件切权限满足,则允许调用,否则失败。由于这些组件的信息都在内存中,所以响应很快。而PMS在开机完成之前已经对各个组件的唯一性,是否合法等都做了检查,不会导致组件的冲突,不符合规范等。
Context主要是提供给外部应用程序使用,只要应用程序拿到一个Context,配合着Intent,就可以使用Android的组件。而PMS主要是内部自己查询组件使用,当有应用程序发起调用,PMS负责查询与之匹配的组件,满足条件就返回,给予启动。
Android服务
分离良好的代码结构
良好的代码结构,一定是各个功能分离完全的代码,每部分只是自己模块的功能,不会受到其他部分的影响,会非常方便调试,便于移植。我们接下来看看分离良好的代码是怎样的。
接口,转接,实现。满足这种结构的代码可以称得上是代码结构良好的代码,怎么说呢,接口是提供给其他模块调用的,所以,这部分一定要抽出来,作为独立的一部分,有完整的接口说明,包括返回值,参数。这样调用者不用关心内部具体的实现,只需要按照接口说明直接使用即可。
转接,转接部分代码也要作为一个独立的部分独立出来,转接的代码是连接接口和实现的部分,并没有实际的功能,但是有了这部分之后代码结构会更清晰,特别是代码在修改和移植的时候,转接部分的代码就相当于插桩直接移植,不需要任何修改。而修改部分只要接口不修改,转接的代码也不需要修改,只需要修改具体的实现。
具体实现,具体的实现可能是千差万别,但是最终都是对具体接口的实现,一定要满足接口的定义。一般程序出了bug,大多数都是具体实现部分的问题,只需要修改出问题的实现部分即可,代码就不会大量的改动。移植的时候也只需要适配具体的实现,而接口和转接部分不会变化,易于移植。
大家看看Android的框架代码,Manager接口和Service实现的分离就使整个代码结构非常清晰。
如何看代码?按照如上的写法,首先应该看代码处有没有特别定义自己的实体,类,接口说明这些,按照这些说明,再去看它的调用关系,最后看它的实现。这样基本上就可以理清代码了。
怎么区分一个程序它的客户端和服务端呢?不只是网络程序这种,服务器机器上的程序是服务端,客户端机器上的是客户端,其实大多数程序都有客户端和服务端。客户端就是用户调用的地方,服务端就是代码的具体实现的地方,就如同main函数里面的调用子函数,而被调用的子函数就是服务端。
如果一个程序再加强,在服务端对客户端的调用做一些限制呢,不让客户端轻易地去调用服务端的内容,必须是指定的接口,得到的也是指定的数据。甚至连服务端除了接口之外其他一无所知,那么久必须做成是一个C/S结构。
Android Binder就是一个很好的例子,Binder把具体的实现和调用接口进行分离,调用端必须通过特定的接口才可以访问服务端内容,而且必须要有相应的权限才可以,客户端是没法随意引用服务端的任何代码的。客户端就不能随意的调用服务端内容,隐藏自己的细节,保证程序的安全性。试想一下,客户端和服务端不做严格的区分,混在一起,所有的程序可以随意调用,代码到处交叉,任何一个调用的地方都有最大的权限,整个程序肯定很快就乱套,稳定性,安全性会是一个极大的问题。
服务是怎么组织起来的呢?打个比方,就是你要先服务中心进行登记注册,你可以来进行查询是否有这个服务,然后可以通过特定的方式获取这个服务。服务里面的数据就是一个提供服务的一个队列,要想获得这个服务,就得先在这个队列里面找到这个具体的服务,然后拿到它。
一般的服务就是一个资源管理程序。通过接口对外提供数据,服务里面的数据是服务至关重要的资源,服务对外提供的接口有哪几种方式呢?
同步,轮询,异步
接口最多的方式就是同步接口,调用直接得到结果。这是简单高效的方式,是绝大多数的实现方式。如果数据在调用的时候没有准备好,那么直接调用可能就不行了,需要去定期查询,如果数据准备好,则通过接口获取数据; 另外一种方式是通过一个监听,当数据准备好了直接回调,也就是异步的方式。
轮询的方式需要定期地去调用接口,会频繁地消耗cpu资源,导致性能下降,这种方案是一种下等方案。
Handler机制。
Handler机制算是一个典型的生产者-消费者模型
异步方式
最直接的方式就是观察者模式
观察者模式定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,并自动更新。观察者模式属于行为型模式,行为型模式关注的是对象之间的通讯,观察者模式就是观察者和被观察者之间的通讯。
但是,这种方式的代码结构太过于简单,观察者和被观察者之间是直接绑定,直接通信的,耦合非常紧密。当其中任何一个地方需要改动代码,必然会导致绑定的多个部分都要进行相应的修改。没有完全达到简洁代码的要求。
订阅-发布模式
订阅-发布模式可以算是观察者模式的一个去掉了代码耦合的观察者模式。订阅者和发布者不直接通信,他们是通过通信通道间接通信的,通信通道对订阅和发布的消息进行调度和转发,订阅者只需要去订阅自己感兴趣的事件即可,而发布者把事件发布到通信通道,事件通道将事件转发给订阅者,从而达到代码的去耦合。对比下观察者模式和订阅-发布模式的区别。
Android的广播算是一个标准的订阅-发布模式。广播的接收者不和具体的广播发送者进行通信,它只需要注册自己感兴趣的广播就好,做好广播接收处理逻辑即可。而广播发送者也不用关心有哪些接收了自己的广播,只需要将广播发出去,交给广播系统进行调度。广播系统把广播的类型分为有序广播,并行广播,并按相应的类型进行调度。当广播触发时,直接将广播转发给注册的接收者。
简略的看看广播机制
每一个应用都持有一个LoadedApk实例,LoadedApk实例中包含多个Context实例(一个进程对应多个Activity和Service以及一个Application),每个Context实例可能创建了多个BroadcastReceiver实例,每个BroadcastReceiver实例在动态注册的时候都会生成一个对应的ReceiverDispatcher实例,每个ReceiverDispatcher实例内部又会由InnerReceiver类生成一个IIntentReceiver实例。这个IIntentReceiver实例在动态注册BroadcastReceiver的时候会被传递给AMS,AMS会为每个IIntentReceiver实例创建一个ReceiverList实例,每个ReceiverList实例中保存了多个BroadcastFilter实例,而这个BroadcastFilter实例里面包含了具体的IntentFilter和ReceiverList等相关信息。Android Broadcast之间的数据就是这样建立起了联系。
广播的发送接收
广播的发送接收,其实就是上面关系图的一个反向查找的过程。应用端调用系统服务(AMS)发送广播,AMS会去广播解析器IntentResolver中查询哪些BroadcastFilter跟这个广播有关联,然后把相关信息封装成 BroadcastRecord类的实例添加到广播发送序列BroadcastQueue中逐个广播。在BroadcastQueue中广播的时候会从BroadcastRecord中获得BroadcastFilter进而获得对应的ReceiverList,ReceiverList中包含了对应的IIntentReceiver实例,通过这个IIntentReceiver实例就可以找到对应的BroadcastReceiver,调用其BroadcastReceiver.OnReceive方法把广播传递给对应的BroadcastReceiver。
广播的接收者,发送者,根本就不知道相互之间的存在,也不直接进行调用通信等,接收者直接接收想要的广播,而发送者直接发送相应的广播,广播系统将相应的广播转发给接收者
服务的管控
应用程序需要通过Android的接口访问Android服务的资源,那么Android的所有资源都是有限的,服务是需要对应用程序的资源访问进行限制的,除了常规的权限限制,Android还提供了速度响应,内存方面的限制。其实对应用程序对服务的资源访问应该是有限制的,特别是写应用的童鞋,访问资源时长应该有限制(比如不要一直持有wake lock),资源大小有限制(比如不能占用太大的内存),访问频度有限制(比如不能频繁的唤醒系统),否则Android就要出杀手锏来杀掉这些服务使用过度的程序,最好的写应用方式是写一个资源池,指定它的大小,超出则等待,否则过多或者过频的使用系统资源必然会导致资源紧张而耗电,卡顿,甚至不稳定。
Low Memory Killer
Low memory killer是Android内存清理机制,因移动端设备的内存、性能、电量等因素Android内核维护一套内存清理机制,就是LMK机制,会定期检查应用内存使用情况、杀死一些进程来释放内存,Low memory killer 主要通过进程oom_adj来判定进程重要度,这个值越小程序越重要,被杀死的可能性越低。
原理和概念
Low memory killer根据两个原则,进程的重要性和释放这个进程可获取的空闲内存数量,来决定释放的进程。
(1) 进程分类,后面的数字为oom_adj值,每个进程都有oom_adj值,越小越重要,被杀的可能性越低,在相同oom_adj下内存占用大的优先被回收。
名称 oom_adj值 解释
FOREGROUD_APP 0 前台程序,可以理解为你正在使用的程序
VISIBLE_APP 1 用户可见的程序
SECONDARY_SERVER 2 后台服务,比如微信会在后台运行服务
HOME_APP 4 HOME,就是主界面
HIDDEN_APP 7 被隐藏的程序
CONTENT_PROVIDER 14 内容提供者
EMPTY_APP 15 空程序,既不提供服务,也不提供内容
(2) Android有两个数组,lowmem_adj和lowmen_minfree,lowmem_adj存放着oom_adj的阈值,lowmen_minfree存放minfree的警戒值,单位为页(一页4K),通过这两个数组计算需要回收的进程。
oom_adj 警戒值
0 1536
1 2048
2 4096
7 5120
14 5632
15 6144
(3) LMK检查的时候基于多个标准来给每个进程评分,对adj高于多少(min_adj)的进程进行分析是否释放,评分最高的被选中并Kill。
2.进程oom_adj配置
进程的oom_adj是可以配置的,进程的类型在ActivityManagerService中可以看到。
进程类型:
static final int EMPTY_APP_ADJ;
static final int HIDDEN_APP_MAX_ADJ;
static final int HIDDEN_APP_MIN_ADJ;
static final int HOME_APP_ADJ;
static final int BACKUP_APP_ADJ;
static final int SECONDARY_SERVER_ADJ;
static final int HEAVY_WEIGHT_APP_ADJ;
static final int PERCEPTIBLE_APP_ADJ;
static final int VISIBLE_APP_ADJ;
static final int FOREGROUND_APP_ADJ;
static final int CORE_SERVER_ADJ = -12;
static final int SYSTEM_ADJ = -16;
其中SYSTEM_ADJ代表着系统进程,CORE_SERVER_ADJ为系统核心服务,这类进程永远不会被杀死,EMPTY_APP、CONTENT_PROVIDER 只类的最容易被杀死,FOREGROUND的进程很难被杀死
Application Not Response
在应用程序响应方面,Android也有一套自己的控制机制,比如一个应用写得太烂,总是在界面上执行一大堆很重的操作,经常卡住,体验非常差,那么Android就会报应用程序无响应(ANR),弹出一个弹框,要么等待,要么杀掉重来。
缓存
如果要随时调试服务状态的话,最好所有的值都可以立即dump出来,便于实时知道服务数据里面的每个状态,例如AMS,WMS里面的每种状态。如果要长久保存,那么久需要写入文件了,写入文件不能一有改变就写入文件,应该有一个定时时长,提高效率,例如BatteryStats耗电日志。
Android硬件抽象层
Linux上的Unix环境编程
Android是构建在Linux之上的,在Linux上面就是Unix环境编程,当然写的应用程序不能直接访问Linux,需要一个中间层来过渡。
在Android诞生之初,有很多人都希望不通过java层直接访问so,或者直接访问到Linux的文件,他们这样做就是想绕开Android所做的各种权限限制,能够直接有效地使用和掌控手机的各项资源。但是这样会扰乱系统的资源分配,可能会导致卡死,崩溃,有一些程序得不到有效地资源分配,安全问题等。
Android最初的Legacy策略
linux共享库思路,把硬件接口都打包到libhardware_legacy.so,Android的JNI在要调用到这个库的硬件接口函数时,只要将Android.mk中的LOCAL_SHARED_LIBRARIES增加libhardware_legacy就行,这样就会到共享库中获取接口。so库并不会自动运行,但是当多个进程使用时 ,如果其他进程加载了它,那么so里面的数据就会映射到对应进程空间中去,这样会造成内存使用增大。另外一个so库本应该完成一类功能,就应该只有一个进程去加载放问它,因为没有限制,任何进程都有可能去加载,造成整个程序的混乱,滋生出安全问题。
Stub策略
一个好的策略应该是一个进程对应一个hal,满足最低权限原则,即当前进程只能访问当下所必须的资源,如下图所示。
我们看下Android是如何设计实现HAL的
HAL使用hw_module_t结构体描述一类硬件抽象模块。每个硬件抽象模块都对应一个动态链接库,一般是由厂商提供的,这个动态链接库必须尊重HAL的命名规范才能被HAL加载到,我们后面会看到。
每一类硬件抽象模块又包含多个独立的硬件设备,HAL使用hw_device_t结构体描述硬件模块中的独立硬件设备。
因此,hw_module_t和hw_device_t是HAL中的核心数据结构,这2个结构体代表了HAL对硬件设备的抽象逻辑。
typedef structhw_module_t {
/** tag must beinitialized to HARDWARE_DEVICE_TAG */
uint32_t tag;
uint16_t module_api_version;
#defineversion_major module_api_version
uint16_t hal_api_version;
#defineversion_minor hal_api_version
/** Identifier of module */
const char *id;
/** Name of this module */
const char *name;
/** Author/owner/implementor of the module*/
const char *author;
/** Modules methods */
struct hw_module_methods_t* methods;
/** module's dso */
void* dso;
#ifdef __LP64__
uint64_t reserved[32-7];
#else
/** padding to 128 bytes, reserved forfuture use */
uint32_t reserved[32-7];
#endif
} hw_module_t;
typedef structhw_device_t {
/** tag must be initialized toHARDWARE_DEVICE_TAG */
uint32_t tag;
uint32_t version;
/** reference to the module this devicebelongs to */
struct hw_module_t* module;
/** padding reserved for future use */
#ifdef __LP64__
uint64_t reserved[12];
#else
uint32_t reserved[12];
#endif
/** Close this device */
int (*close)(struct hw_device_t* device);
} hw_device_t;
C语言中并没有继承的概念,那它是如何用hw_device_t,hw_module_t去实现不同模块的具体功能的呢?
C语言通过组合,指针强制转换实现类似C++的继承和多态行为,我们接下来看看,以vibrator为例,梳理一下vibrator的加载过程。
Vibrator的数据结构定义vibrator.h文件中
/**
* The id of this module
*/
#defineVIBRATOR_HARDWARE_MODULE_ID "vibrator"
/**
* The id of the main vibrator device
*/
#defineVIBRATOR_DEVICE_ID_MAIN "main_vibrator"
structvibrator_device;
typedef structvibrator_device {
/**
* Common methods of the vibratordevice. This *must* be the first memberof
* vibrator_device as users of thisstructure will cast a hw_device_t to
* vibrator_device pointer in contextswhere it's known the hw_device_t references a
* vibrator_device.
*/
struct hw_device_t common;
/** Turn on vibrator
*
* This function must only be called afterthe previous timeout has expired or
* was canceled (through vibrator_off()).
*
* @param timeout_ms number of millisecondsto vibrate
*
* @return 0 in case of success, negativeerrno code else
*/
int (*vibrator_on)(struct vibrator_device*vibradev, unsigned int timeout_ms);
/** Turn off vibrator
*
* Cancel a previously-started vibration,if any.
*
* @return 0 in case of success, negativeerrno code else
*/
int (*vibrator_off)(struct vibrator_device*vibradev);
}vibrator_device_t;
static inlineint vibrator_open(const struct hw_module_t* module, vibrator_device_t** device)
{
return module->methods->open(module,VIBRATOR_DEVICE_ID_MAIN, TO_HW_DEVICE_T_OPEN(device));
}
Vibrator.c文件中vibrator的具体实现。
static constchar THE_DEVICE[] = "/sys/class/timed_output/vibrator/enable";
static booldevice_exists(const char *file) {
int fd;
fd = TEMP_FAILURE_RETRY(open(file,O_RDWR));
if(fd < 0) {
return false;
}
close(fd);
return true;
}
static boolvibra_exists() {
return device_exists(THE_DEVICE);
}
static intwrite_value(const char *file, const char *value)
{
int to_write, written, ret, fd;
fd = TEMP_FAILURE_RETRY(open(file,O_WRONLY));
if (fd < 0) {
return -errno;
}
to_write = strlen(value) + 1;
written = TEMP_FAILURE_RETRY(write(fd,value, to_write));
if (written == -1) {
ret = -errno;
} else if (written != to_write) {
/* even though EAGAIN is an errno valuethat could be set
by write() in some cases, none ofthem apply here. So, this return
value can be clearly identified whendebugging and suggests the
caller that it may try to callvibrator_on() again */
ret = -EAGAIN;
}else {
ret = 0;
}
errno = 0;
close(fd);
return ret;
}
static intsendit(unsigned int timeout_ms)
{
char value[TIMEOUT_STR_LEN]; /* largeenough for millions of years */
snprintf(value, sizeof(value),"%u", timeout_ms);
return write_value(THE_DEVICE, value);
}
static intvibra_on(vibrator_device_t* vibradev __unused, unsigned int timeout_ms)
{
/* constant on, up to maximum allowed time*/
return sendit(timeout_ms);
}
static intvibra_off(vibrator_device_t* vibradev __unused)
{
return sendit(0);
}
static constchar LED_DEVICE[] = "/sys/class/leds/vibrator";
static intwrite_led_file(const char *file, const char *value)
{
char file_str[50];
snprintf(file_str, sizeof(file_str),"%s/%s", LED_DEVICE, file);
return write_value(file_str, value);
}
static boolvibra_led_exists()
{
char file_str[50];
snprintf(file_str, sizeof(file_str),"%s/%s", LED_DEVICE, "activate");
return device_exists(file_str);
}
static intvibra_led_on(vibrator_device_t* vibradev __unused, unsigned int timeout_ms)
{
int ret;
char value[TIMEOUT_STR_LEN]; /* largeenough for millions of years */
ret = write_led_file("state","1");
if (ret)
return ret;
snprintf(value, sizeof(value),"%u\n", timeout_ms);
ret = write_led_file("duration",value);
if (ret)
return ret;
return write_led_file("activate","1");
}
static intvibra_led_off(vibrator_device_t* vibradev __unused)
{
return write_led_file("activate","0");
}
static intvibra_close(hw_device_t *device)
{
free(device);
return 0;
}
static intvibra_open(const hw_module_t* module, const char* id __unused,
hw_device_t** device__unused) {
bool use_led;
if (vibra_exists()) {
ALOGD("Vibrator usingtimed_output");
use_led = false;
} else if (vibra_led_exists()) {
ALOGD("Vibrator using LEDtrigger");
use_led = true;
} else {
ALOGE("Vibrator device does notexist. Cannot start vibrator");
return -ENODEV;
}
vibrator_device_t *vibradev = calloc(1,sizeof(vibrator_device_t));
if (!vibradev) {
ALOGE("Can not allocate memory forthe vibrator device");
return -ENOMEM;
}
vibradev->common.tag =HARDWARE_DEVICE_TAG;
vibradev->common.module = (hw_module_t*) module;
vibradev->common.version =HARDWARE_DEVICE_API_VERSION(1,0);
vibradev->common.close = vibra_close;
if (use_led) {
vibradev->vibrator_on =vibra_led_on;
vibradev->vibrator_off =vibra_led_off;
} else {
vibradev->vibrator_on = vibra_on;
vibradev->vibrator_off = vibra_off;
}
*device = (hw_device_t *) vibradev;
return 0;
}
/*===========================================================================*/
/* Defaultvibrator HW module interface definition */
/*===========================================================================*/
static structhw_module_methods_t vibrator_module_methods = {
.open = vibra_open,
};
structhw_module_t HAL_MODULE_INFO_SYM = {
.tag = HARDWARE_MODULE_TAG,
.module_api_version = VIBRATOR_API_VERSION,
.hal_api_version =HARDWARE_HAL_API_VERSION,
.id = VIBRATOR_HARDWARE_MODULE_ID,
.name = "Default vibrator HAL",
.author = "The Android Open SourceProject",
.methods = &vibrator_module_methods,
};
Vibrator在frameworks/base/services/core/jni/com_android_server_VibratorService.cpp文件中加载,看看它的初始化
staticvoid vibratorInit(JNIEnv /* env */, jobject /* clazz */)
{
if (gVibraModule != NULL) {
return;
}
int err =hw_get_module(VIBRATOR_HARDWARE_MODULE_ID, (hw_module_tconst**)&gVibraModule);
if (err) {
ALOGE("Couldn't load %s module(%s)", VIBRATOR_HARDWARE_MODULE_ID, strerror(-err));
} else {
if (gVibraModule) {
vibrator_open(gVibraModule,&gVibraDevice);
}
}
}
hw_get_module(VIBRATOR_HARDWARE_MODULE_ID,
(hw_module_t const**)&gVibraModule)具体是怎样加载到vibrator对应的so库呢?
通过hw_get_module()函数以VIBRATOR_HARDWARE_MODULE_ID 参数获得camera_module_t 指针来初始化和调用VIBRATOR 我们再看hw_get_module()的实现
inthw_get_module(const char *id, const struct hw_module_t **module)
{
return hw_get_module_by_class(id, NULL, module);
}
而hw_get_module()又是通过hw_get_module_by_class():
inthw_get_module_by_class(const char *class_id, const char *inst,
const struct hw_module_t **module)
{
.................核心看......................
returnload(class_id, path, module);
}
staticint load(const char *id,const char *path,const struct hw_module_t **pHmi)
{
..........................................................................................
handle = dlopen(path, RTLD_NOW);
if (handle == NULL) {
char const *err_str = dlerror();
ALOGE("load: module=%s\n%s", path,err_str?err_str:"unknown");
status = -EINVAL;
goto done;
}
/* Get the address of the struct hal_module_info. */
const char *sym = HAL_MODULE_INFO_SYM_AS_STR;
hmi = (struct hw_module_t *)dlsym(handle, sym);
if (hmi == NULL) {
ALOGE("load: couldn't find symbol %s", sym);
status = -EINVAL;
goto done;
}
/* Check that the id matches */
if (strcmp(id, hmi->id) != 0) {
ALOGE("load: id=%s != hmi->id=%s", id,hmi->id);
status = -EINVAL;
goto done;
}
.................................................................................
}
有load函数可发现,它是通过dlopen()加载vibrator.so库,通过dlsym()查询HAL_MODULE_INFO_SYM_AS_STR全局变量的地址,通过强制指针转换可以获得HAL_MODULE_INFO_SYM_AS_STR变量,并检查传进来的id和获得id是否一致。HAL_MODULE_INFO_SYM_AS_STR是个宏定义如下:
#define HAL_MODULE_INFO_SYM_AS_STR"HMI"
而HAL_MODULE_INFO_SYM在vibrator.c中定义如下:
structhw_module_t HAL_MODULE_INFO_SYM = {
.tag = HARDWARE_MODULE_TAG,
.module_api_version = VIBRATOR_API_VERSION,
.hal_api_version =HARDWARE_HAL_API_VERSION,
.id = VIBRATOR_HARDWARE_MODULE_ID,
.name = "Default vibrator HAL",
.author = "The Android Open SourceProject",
.methods = &vibrator_module_methods,
};
其关键在于load函数中的下面两行代码:
const char *sym =HAL_MODULE_INFO_SYM_AS_STR;
hmi = (struct hw_module_t *)dlsym(handle,sym);
在打开的.so中查找HMI符号的地址,并保存在hmi中。至此,.so中的hw_module_t已经被成功获取,从而可以根据它获取别的相关接口。
HAL通过hw_get_module函数获取hw_module_t
HAL通过hw_module_t->methods->open获取hw_device_t指针,并在此open函数中初始化hw_device_t的包装结构中的函数及hw_device_t中的close函数,如vibrator_device_open。
三个重要的数据结构:
struct hw_device_t:表示硬件设备,存储了各种硬件设备的公共属性和方法
struct
hw_module_t: 可用hw_get_module进行加载的module
struct
hw_module_methods_t: 用于定义操作设备的方法,其中只定义了一个打开设备的方法open.
Vibrator HAL的代码看起来确实是有点绕。好了我们不太细究这个细节,领会其思想才是最关键的,就是定义自己的vibrator_device_t,定义自己的HAL_MODULE_INFO_SYM,定义自己的vibrator id VIBRATOR_HARDWARE_MODULE_ID,实现vibrator的具体功能,vibrator
on,vibrator off,这样vibrator就可以运转起来,中间,hal的加载,需找特定的so库这些HAL框架都已经做好了。而且只有Vibrator JNI才可以加载自己,只能是Vibrator的Service才可以使用,避免了上层多个进程可以随意的加载Vibrator so库的混乱,保证程序的安全,资源浪费。
为何不把Android HAL做到内核中去?
Linux是由自己的协议的,使用Linux,修改Linux相关的代码都需要遵守协议,要公开代码,所以Android就在Linux之上搞了一个HAL层,各个厂商可以自由实现自己的逻辑,不用开源,保护自己的技术成果。
Android UI上的一些异步设计
异步机制Handler
写UI程序的时候常常会碰到异步处理,非常棘手,需要一套非常好的异步机制来完成,看看Handler
Message:消息,由MessageQueue统一队列,然后交由Handler处理。
MessageQueue:消息队列,用来存放Handler发送过来的Message,并且按照先入先出的规则执行。
Handler:处理者,负责发送和处理Message。
Looper:消息轮询器,不断的从MessageQqueue中抽取Message并执行。
如下图:
Handler的大致结构是这样,Handler,Message,MessageQueue这些都比较直接明了,我们看看消息轮询器Looper是如何工作的。它的具体工作是在native层完成的。
Looper的native层核心是一个管道,当获取MessageQueue下一个消息时会执行epoll_wait的方式等待事件发生(管道中有数据写入),当执行sendMessage之后消息队列有了数据之后会触发唤醒nativeWake,就向管道中写入1,这样epoll_wait等待的事件发生返回,MessageQueue就可以拿到下一个消息,从而完成了消息的循环。(详细过程可以追踪一下Android的源码),这就是教科书上经典的读写者模式。
Android主要用Handler来完成异步模式,将一些耗时过程移入到子线程中完成。
多线程异步AsyncTask
看看这个抽象类里面有哪些接口
public abstractclass AsyncTask {
onPreExecute()
//此方法会在后台任务执行前被调用,用于进行一些准备工作
doInBackground(Params...
params) //此方法中定义要执行的后台任务,在这个方法中可以调用publishProgress来更新任务进度(publishProgress内部会调用onProgressUpdate方法)
onProgressUpdate(Progress...
values) //由publishProgress内部调用,表示任务进度更新
onPostExecute(Result
result) //后台任务执行完毕后,此方法会被调用,参数即为后台任务的返回结果
onCancelled() //此方法会在后台任务被取消时被调用
调用AsyncTask,启动执行是用execute()接口
@MainThread
public final AsyncTask execute(Params... params) {
returnexecuteOnExecutor(sDefaultExecutor, params);
}
我们再来看看executeOnExecutor具体怎么执行的
@MainThread
public final AsyncTask executeOnExecutor(Executor exec,
Params... params) {
if (mStatus != Status.PENDING) {
switch (mStatus) {
case RUNNING:
throw newIllegalStateException("Cannot execute task:"
+ " the taskis already running.");
case FINISHED:
throw newIllegalStateException("Cannot execute task:"
+ " the taskhas already been executed "
+ "(a task canbe executed only once)");
}
}
mStatus = Status.RUNNING;
onPreExecute();
mWorker.mParams = params;
exec.execute(mFuture);
return this;
}
executeOnExecutor先是执行onPreExecute() 已就是上面提到的接口中说的在执行前的一些准备工作,接着将参数传给 mWorker. Mparams,mWorker是一个WorkerRunnable,用于和Future,Executor来完成异步返回的。
看看他们具体怎样执行
public AsyncTask(@Nullable LoopercallbackLooper) {
mHandler = callbackLooper == null ||callbackLooper == Looper.getMainLooper()
? getMainHandler()
: new Handler(callbackLooper);
mWorker = new WorkerRunnable() {
public Result call() throws Exception{
mTaskInvoked.set(true);
Result result = null;
try {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
//noinspection unchecked
result =doInBackground(mParams);
Binder.flushPendingCommands();
} catch (Throwable tr) {
mCancelled.set(true);
throw tr;
} finally {
postResult(result);
}
return result;
}
};
mFuture = newFutureTask(mWorker) {
@Override
protected void done() {
try {
postResultIfNotInvoked(get());
} catch (InterruptedExceptione) {
android.util.Log.w(LOG_TAG,e);
} catch (ExecutionException e){
throw newRuntimeException("An error occurred while executing doInBackground()",
e.getCause());
} catch (CancellationExceptione) {
postResultIfNotInvoked(null);
}
}
};
}
在WorkerRunnable里面就doInBackground(), 就是接口里面的执行后台任务。执行完之后,需要将结果Result投递到需要它的地方。
private Result postResult(Result result) {
@SuppressWarnings("unchecked")
Message message =getHandler().obtainMessage(MESSAGE_POST_RESULT,
newAsyncTaskResult(this, result));
message.sendToTarget();
return result;
}
其实就是发一个消息到对应线程的Handler去
private void finish(Result result) {
if (isCancelled()) {
onCancelled(result);
} else {
onPostExecute(result);
}
mStatus = Status.FINISHED;
}
最后执行到onPostExecute里面去,也就是接口中的结果更新。
其实AsyncTask是Android中的一个多线程异步,Android有Handler和Thread,为何还要这呢?其实AsyncTask帮我们实现了一个线程池,是一个串行执行的。开始就初始化了一部分县城,免去线程的经常开启,销毁的开销。它同时提供了异步返回的能力,将耗时任务放入后台子线程,一个线程执行完毕后再去取下一个线程,当后台任务结束后将结果返回来更新。开发者只需要按照接口说明,耗时任务放到doInBackground,更新结果放入onPostExecute,不用管线程,不用管并发导致的问题。
Android对Java集合类改进
ArrayMap
SpraseArray
先不管这两个类,看看原生的Android集合类,数组,链表,树,哈希表。这些有和差异,有何优劣。
数组,存储效率高,基本没有附加的空间,没有浪费,查询的时候效率高,只要知道下标就可以拿到数据。但是要删除添加的时候,为了保证数据的有序,需要挪动数据位置,保证数据的有序,会导致效率的下降。
链表,由于链表需要存储上一份数据,下一份数据的地址,所以会有空间浪费,在查询的时候要依据当前指针位置一个一个地移动,效率偏低。但是在增加删除时,只需要改掉当前数据的前后指针位置就可以,删除,添加效率是很高的。
树,用得比较多的就是红黑树和完全二叉树了。这两种数据结构用得最多的是红黑树,它是一种近似的完全二叉树。
树的查询效率是很高的,完全是对数级别,也就是常数级别,非常快。但是在有数据添加和删除时,需要旋转,保证树是平衡的,会有一定的性能开销。所以它的增加删除效率不是很好。
哈希表,其实Java的哈希表是用数组加链表的方式实现的,数组可以实现数据的高效存储,当对应数组位置的数据存在冲突时,就用链表来存储,一般链表上的数据不会太多,保证存储效率。之前说了链表在查找的时候效率偏低。而且存储效率不高,因为存有下一份数据的地址。哈希表的数组可以避免因为数据的删除,增加而改动位置导致的性能开销,因为它是根据哈希算法来存取的,它是常数级别的,直接存,直接取。而链表是用来处理哈希冲突,只是查询效率一般,删除,增加效率很好。做两个极端假设,哈希如果没有冲突,就完全是一个数组,当然,这个申请的内存空间会很大。如果冲突太多,这时候近似是一个链表了,查询的效率就会很差了。
Android SpraseArray ArrayMap
我在看了哈希表的具体实现之后就提出过,用数组+二叉树的形式来做哈希表,数组可以提高存储效率,而在查找的时候,在二叉树上,它的查找效率是对数级别,效率也很高。SpraseArray,ArrayMap就是这样思路实现的,它的key是个有序数组,当要查询时,通过折半查找找到(类似二叉树),而value是通过哈希算法存取的,效率也是常数级别,同时没有空间浪费。这样可以做到查询,添加,删除效率的同时提高。
代码控制和减少耦合
我们看看Spring里面的控制反转和依赖注入
通过实例理解控制反转的概念
贺岁大片在中国已经形成了一个传统,每到年底总有多部贺岁大片纷至沓来让人应接不暇。在所有贺岁大片中,张之亮的《墨攻》算是比较出彩的一部。该片讲述了战国时期墨家人革离帮助梁国反抗赵国侵略的个人英雄主义故事,恢宏壮阔、浑雄凝重的历史场面相当震撼。其中有一个场景:当刘德华所饰演的墨者革离到达梁国都城下,城上梁国守军问到:“来者何人?”刘德华回答:“墨者革离!”我们不妨通过一个Java类为这个“城门叩问”的场景进行编剧,并借此理解IoC的概念:
代码清单3-1 MoAttack:通过演员安排剧本
public class MoAttack {
public void cityGateAsk(){
//①演员直接侵入剧本
LiuDeHua ldh = new LiuDeHua();
ldh.responseAsk("墨者革离!");
}
}
我们会发现以上剧本在①处,作为具体角色饰演者的刘德华直接侵入到剧本中,使剧本和演员直接耦合在一起(图3-1)。
一个明智的编剧在剧情创作时应围绕故事的角色进行,而不应考虑角色的具体饰演者,这样才可能在剧本投拍时自由地遴选任何适合的演员,而非绑定在刘德华一人身上。通过以上的分析,我们知道需要为该剧本主人公革离定义一个接口:
代码清单3-2 MoAttack:引入剧本角色
public class MoAttack {
public void cityGateAsk()
{
//①引入革离角色接口
GeLi geli = new LiuDeHua();
//②通过接口开展剧情
geli.responseAsk("墨者革离!");
}
}
在①处引入了剧本的角色——革离,剧本的情节通过角色展开,在拍摄时角色由演员饰演,如②处所示。因此墨攻、革离、刘德华三者的类图关系如图 3 2所示:
可是,从图3 2中,我们可以看出MoAttack同时依赖于GeLi接口和LiuDeHua类,并没有达到我们所期望的剧本仅依赖于角色的目的。但是角色最终必须通过具体的演员才能完成拍摄,如何让LiuDeHua和剧本无关而又能完成GeLi的具体动作呢?当然是在影片投拍时,导演将LiuDeHua安排在GeLi的角色上,导演将剧本、角色、饰演者装配起来(图3-3)。
通过引入导演,使剧本和具体饰演者解耦了。对应到软件中,导演像是一个装配器,安排演员表演具体的角色。
现在我们可以反过来讲解IoC的概念了。IoC(Inverse of Control)的字面意思是控制反转,它包括两个内容:
其一是控制
其二是反转
那到底是什么东西的“控制”被“反转”了呢?对应到前面的例子,“控制”是指选择GeLi角色扮演者的控制权;“反转”是指这种控制权从《墨攻》剧本中移除,转交到导演的手中。对于软件来说,即是某一接口具体实现类的选择控制权从调用类中移除,转交给第三方决定。
因为IoC确实不够开门见山,因此业界曾进行了广泛的讨论,最终软件界的泰斗级人物Martin
Fowler提出了DI(依赖注入:Dependency Injection)的概念用以代替IoC,即让调用类对某一接口实现类的依赖关系由第三方(容器或协作类)注入,以移除调用类对某一接口实现类的依赖。“依赖注入”这个名词显然比“控制反转”直接明了、易于理解。
IoC的类型
从注入方法上看,主要可以划分为三种类型:构造函数注入、属性注入和接口注入。Spring支持构造函数注入和属性注入。下面我们继续使用以上的例子说明这三种注入方法的区别。
构造函数注入
在构造函数注入中,我们通过调用类的构造函数,将接口实现类通过构造函数变量传入,如代码清单3-3所示:
代码清单3-3 MoAttack:通过构造函数注入革离扮演者
public class MoAttack {
private GeLi geli;
//①注入革离的具体扮演者
public MoAttack(GeLi geli){
this.geli = geli;
}
public void cityGateAsk(){
geli.responseAsk("墨者革离!");
}
}
MoAttack的构造函数不关心具体是谁扮演革离这个角色,只要在①处传入的扮演者按剧本要求完成相应的表演即可。角色的具体扮演者由导演来安排,如代码清单3-4所示:
代码清单3-4 Director:通过构造函数注入革离扮演者
public class Director {
public void direct(){
//①指定角色的扮演者
GeLi geli = new LiuDeHua();
//②注入具体扮演者到剧本中
MoAttack moAttack = newMoAttack(geli);
moAttack.cityGateAsk();
}
}
在①处,导演安排刘德华饰演革离的角色,并在②处,将刘德华“注入”到墨攻的剧本中,然后开始“城门叩问”剧情的演出工作。
属性注入
有时,导演会发现,虽然革离是影片《墨攻》的第一主角,但并非每个场景都需要革离的出现,在这种情况下通过构造函数注入相当于每时每刻都在革离的饰演者在场,可见并不妥当,这时可以考虑使用属性注入。属性注入可以有选择地通过Setter方法完成调用类所需依赖的注入,更加灵活方便:
代码清单3-5 MoAttack:通过Setter方法注入革离扮演者
public class MoAttack {
private GeLi geli;
//①属性注入方法
public void setGeli(GeLi geli) {
this.geli = geli;
}
public void cityGateAsk() {
geli.responseAsk("墨者革离");
}
}
MoAttack在①处为geli属性提供一个Setter方法,以便让导演在需要时注入geli的具体扮演者。
代码清单3-6 Director:通过Setter方法注入革离扮演者
public class Director {
public void direct(){
GeLi geli = new LiuDeHua();
MoAttack moAttack = new MoAttack();
//①调用属性Setter方法注入
moAttack.setGeli(geli);
moAttack.cityGateAsk();
}
}
和通过构造函数注入革离扮演者不同,在实例化MoAttack剧本时,并未指定任何扮演者,而是在实例化MoAttack后,在需要革离出场时,才调用其setGeli()方法注入扮演者。按照类似的方式,我们还可以分别为剧本中其他诸如梁王、巷淹中等角色提供注入的Setter方法,这样,导演就可以根据所拍剧段的不同,注入相应的角色了。
接口注入
将调用类所有依赖注入的方法抽取到一个接口中,调用类通过实现该接口提供相应的注入方法。为了采取接口注入的方式,必须先声明一个ActorArrangable接口:
public interface ActorArrangable {
voidinjectGeli(GeLi geli);
}
然后,MoAttack实现ActorArrangable接口提供具体的实现:
代码清单3-7 MoAttack:通过接口方法注入革离扮演者
public class MoAttack implementsActorArrangable {
private GeLi geli;
//①实现接口方法
public void injectGeli (GeLi geli) {
this.geli = geli;
}
public void cityGateAsk() {
geli.responseAsk("墨者革离");
}
}
Director通过ActorArrangable的injectGeli()方法完成扮演者的注入工作。
代码清单3-8 Director:通过接口方法注入革离扮演者
public class Director {
public void direct(){
GeLi geli = new LiuDeHua();
MoAttack moAttack = new MoAttack();
moAttack. injectGeli (geli);
moAttack.cityGateAsk();
}
}
由于通过接口注入需要额外声明一个接口,增加了类的数目,而且它的效果和属性注入并无本质区别,因此我们不提倡采用这种方式。
一份好的代码,首先是分离良好的,客户端服务端分离,代码要有控制部分,就如同拍电影一样,导演,演员和剧本是解藕的,只要合适谁都可以来演,谁都可以做导演,加入控制部分导演拍电影,理解角色,选择演员,控制剧情。不会因为换导演,换演员导致剧本剧情跟着改动。directMovie()算是整个代码的控制中心,可以称之为导演部,它能够选定导演,演员,控制剧情,拍摄节奏。这一切的一切是因为,剧本,角色,导演,角色分离的很好才可以做到。
很多时候我们谈到继承,组合,组合优先于继承。其实,我们也可以多用用依赖注入,减少代码的耦合,这些逻辑是在代码运行时进行控制。一般,控制反转和依赖注入一一起使用的,解决掉代码的耦合,更好的控制代码逻辑。
服务端与客户端
我们写代码的时候,一定要随时有服务端与客户端的思想。客户端不仅仅是网络程序访问端,在写代码的时候main函数的地方,或者发起调用的地方我们都称之为客户端,而具体实现的地方,被调用的地方我们都称之为服务端。客户端与服务端要尽量分离,抽离出服务端的接口作为调用之用,因为客户端可能会因为需求经常变动,常常改代码,而服务端的代码一旦固定就不轻易改动,所以要抽离出接口,接口是不变的,服务端也不用改动。避免客户端的改动而导致整个服务端也跟着改动。客户端是具体需求的地方,经常改动,所以尽量要抽离出一个控制部分出来,控制部分可以控制整个的功能逻辑,修改需求,甚至删掉需求,有了控制部分我们就可以做到对需求的整体把控,改动也可以做到最小。
写在最后
作为一名工程师,我们按照别人设计好的框架去写代码,而不去想想,或者看看别人是怎样设计的,久而久之会对这些框架形成依赖,变得懒惰,甚至不会去思考了。框架就是让开发工程师做傻瓜式编程,真的会让人变傻。
所以,我们还是要突破这种限制,跳出这个包围,看看框架是怎样设计的,有什么考量。这样一个工程师才会进阶。