UML图有很多类型,这里只讨论最重要也最常用的两种 - 类图和时序图。
1. 类图
通过类图,我们可以很容易的了解代码架构,理清模块之间的关系,
包括继承(Inheritance),实现(realization),依赖(dependency),组合(Composition), 聚合(Aggregation), 关联 (Association) 等等。
下面就图中给出的7种关系一一解读。
1.1 Composition
Compostion 是一种 Association 关系,但它更强调两个类之间整体和局部关系,它暗示两个类之间有着相同的生命周期,
比如说图中的三个1.
- W 是 ViewRootImpl的成员变量之一,ViewRootImpl 对象的构造函数里也构造了W,因此,当ViewRootImpl 析构时,W也被析构,他们的生命周期是一致的。
public final class ViewRootImpl implements ViewParent, View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks { ... final W mWindow; ... mWindow = new W(this); //互相引用,所以当一个销毁时,另外一个也无法存在。 }
- 同样类似的关系存在于 WindowManagerService 和 WindwoState 之间。
1.2 Realization
Realization就是实现,在Java中体现为implements 一个接口类interface, 在标准的C++中没有明确的接口概念,但抽象类实际上起着和接口类似的功能,因为C++的Realization可以体现为继承一个抽象类。在Android 的C++代码中,有一个特殊的抽象类IInterface, 定义了PC接口类的一些基本方法。
1.3 Association
有接口就会有引用,在UML中一根最普通的单向箭头即是引用(关联)关系。它的含义是,某个对象用到了一个其他对象的接口或属性。通常,Assocation 通过两种方式获取
- 依赖注入,通过构造函数或SetXXX()接口,比如说 WindowState 通过构造时传入的参数获取了对IWindow对象的引用
WindowState(WindowManagerService service, Session s, IWindow c, WindowToken token, WindowState attachedWindow, int appOp, int seq, WindowManager.LayoutParams a, int viewVisibility, final DisplayContent displayContent)
- 间接获取,通过调用其他对象方法返回。
mActivityManager = ActivityManagerNative.getDefault();
1.4 Android 的 IPC interface.
C++和Java 的接口都只支持进程内的调用。为了支持专门用于跨进程的接口调用,Android 专门做了一些规定。
凡是以IXXXX 定义的接口类均可以支持IPC(当然,进程内也可以调用IXXXX定义的接口)。
当我们在图中看到一个IXXX 接口类,我们便可以认为这是一个进程的边界,他的实现端和调用端对象运行在不同的进程里(至少是不同的线程)。
如上图中,IWindow的两端W对象 和 WindowState 对象就用不同的颜色来标明它们分属于不同的进程,W运行在应用程序的进程里,而WinState存在与System Server 进程里。
1.5 Aggregation
聚合表达了两个类的从属关系,但和Composition不同,他们的生命周期并不一样。
一个经典的例子就是工厂和车子,车子是工厂造出来的,工厂倒闭了,车子可以继续开,反之亦然。
图中PolicyManager 和 PhoneWindow就是类似的关系。
1.6 Inherritance
就是最常见的继承关系了。
复杂的继承关系很难阅读和记忆,通过UML图则方便很多,你可以清楚到看出继承关系,同时能够理解继承的设计思想。
比如说图中右上角,PhoneWindow 继承了Window 类,Activity 引用的是其基类Window的对象,但背后真正干事的是PhoneWindow对象, 因为Acitivy 用的Window类对象是PolicyManager构造出来的。通过这种方式,PhoneWindow的实现细节被PolicyManager 和 Window 基类隐藏起来,从而大大降低了应用程序(Activity) 改变的几率。
这个正是设计模式里有名的工厂模式之一。
1.7 依赖
依赖不同于引用,依赖者和被依赖着之间没有直接的对象引用,通常是常量或静态方法的使用。
比如图中,Activity使用了PolicyManager类的静态方法 makeNewWindow() 创建了PhoneWindow对象,我们说Acitivy 依赖PolicyManager 这个模块,但它并没有引用PoclicyManager的对象。
依赖通常用一根单向虚线箭头表示。
2. 时序图
通过时序图,我们可以了解代码的调用流程, 并可以检查调用过程中可能产成的潜在问题,如死锁等。
时序图可以从两个方向去看,纵向和横向。
纵向描述了一个对象在时间轴上所做的事情,一个方块通过代表一个函数的调用。
横向则描述了各个对象之间的调用关系, 包括同步调用,异步调用,返回等等。
此外,在时序图中,我们可以给执行块赋予不同的颜色,代表了他们分别运行在不同的进程或线程里。
下面就是一张时序图的例子,它描述了Android中一个System server 进程启动的过程, 图中的粉红色注释列举了从图中我们可以获知的一些信息。
这是一个很简单的例子,图中有两个线程,绿色是app线程,它通过调用MediaPlayer 对象的函数来控制播放器,这里它做了两件事,Start() 然后 Stop (). 而粉色部分代表Driver 线程,它通过回调函数告知Mediaplayer 一下底层的事件。也就是说Mediaplayer 是一个被两个线程同时引用的对象,是一个共享的资源。
想当然的,我们用了一把锁来保护它,防止他被同时使用产生冲突。所以,图中,绿色的app在Stop()时候首先拿到了锁。这时问题发生了,Stop()的过程可能会比较长,中途来了一个事件, 图中黄色注释的右方显示了这个情况,两个颜色的长条重叠在一起。这表明有资源冲突发生,也意味着潜在的死锁风险。我们假设Stop()的最终目的就是要析构VideoDecoder 对象,但此时,VideoDecoder调用的eventHandler() 在另外一个线程还没返回,理所当然的我们需要等待它。不幸的是,这个时候死锁发生了,如图中红色注释所示。
通过简单的画这么一个图,可以很轻易的分析出一个死锁的情况。那怎么解决它的,尽可以的避免图中不同颜色的条块重叠在一起。看看下面的解决方案
这回,我们取消了锁的操作,通过添加一个新的线程(变成3个线程,3种颜色)Thread,将同步的调用变成异步,,交由Thread做后台处理。
这下同意了吧,时序图对分析多线程的编程分析有很大的帮助。我们应该在设计阶段尽可能的用类图和时序图来帮助我们避免一些常见的问题,帮助我们得出一个尽可能好的设计。
3. 怎样画Android UML 图?
工具!必须依赖工具,市面上有太多的UML工具,你只需要找一款支持逆向工程的,即将代码转换成UML的数据结构,然后将类图或时序图一步步的绘制出来。