【Android开发精要笔记】Android组件模型解析

Android组件模型解析

Android中的Mashup

将应用切分成不同类别的组件,通过统一的定位模型和接口标准将他们整合在一起,来共同完成某项任务。
在Android的Mashup模式下,每个组件的功能都可以被充分的复用。来自不同应用的组件可以有机地结合在一起,共同完成任务。

基于Mashup的Android应用模型

三个基本要素:组件、连接、配置

接口就是实现单元。
从代码来看,组件就是派生自特定接口或基类的子类的实现,如界面组件Activity就是指派生自android.app.Activity类的子类实现。

连接指的是组件与组件之间的通信通道,是android为不同类别的组件之间进行调用和通信预设的模式。比如,与界面组件的通信连接,通过Intent对象来建立,与数据源组件的通信,则通过URI地址来定位并搭建连接通路

配置是用来描述组件的功能和实现特征的信息。
Android的组件管理服务,就是通过配置文件中的信息去了解每个组件的特征。

以发送邮件为例子

基于Mashup的应用架构特征

核心是组件,在安卓中,组件执行时的聚合单元是任务(Task),每个任务都由若干个界面组件对象构成,组件可能来自不同的应用,运行在不同的进程中,它们彼此独立,无需关注具体调用者或被调用者的实现细节。
组件间的数据传输,都是通过消息、进程间的通信模型等序列化数据传输的方式来进行,而不是通过对象指针的直接传递,这就使得Android的应用天生具有了良好的跨进程特征。

界面组件Activity解析

android的界面组件并没有沿用MVC架构,它的设计理解更接近于Web页面。毕竟,Android应用的架构思想就源自于Web2.0中的Mashup的概念。

  • 从运行模式来看,Android是个多任务的操作系统,可以同时运行多个任务,每个任务都有一个界面组件栈,栈中的元素是界面组件对象的实例,其中负责与用户进行交互的是前台任务的栈顶组件。
     
  • Android的界面组件是用过类型信息,数据URI信息,数据类型信息等描述信息进行定位的。而界面组件的切换和数据传输,都依赖于Android组件管理服务的统一调度和传递。
  • Android界面组件的功能设计和Web页面类似,都近似于功能黑盒。在Web开发中,会通过Cooik来存储一些状态信息,出于同样的设计考虑,Android的每个应用进程都有一个应用环境对象(Application Context),小数据量的共享数据可以通过它来进行存储。

【处理构造界面】通过R类的帮助,使用setContentView()方法。
【处理交互事件】一类是在当前界面的全局事件,可以通过重载Activity中特定的方法来实现,另一类则是和具体控件相关的交互时间,Android的控件采用了观察者模式,可以通过添加监听者处理相关事件。
【管理界面组件的数据】Android是一个多任务的操作系统,同时运行的任务过多时,就需要自动结束部分应用和组件,以保证系统有充足的内存空间来执行新的任务。Android采用进程托管的策略。对于开发者,需要依照界面组件的生命周期模型,妥善维护好相关的状态,在组件被销毁时序列化保存相关的信息,当应用被重新构造时精准地恢复成销毁前的状态,以保证用户体验的一致性。
【配置界面组件的任务模型】Android界面组件在运行时,会通过任务进行组织。同一个任务中的界面组件,会按照栈模型线性排列。比如,当一个节目组件需要占用大量资源的时候,就不应该有太多的实例同时存在于人物中,而是要可能多地进行复用,以降低系统的开销。为此,android提供了多种组件任务模型,来调整栈中元素的次序,或者是将单个任务拆分成多个任务,甚至将任务放到不同的进程中去,以提升执行效率,通过配置文件中的launchMode、clearTaskOnLaunch、Process等参数进行设置,在使用其他界面组件时,也需要通过Intent的标志位(flags)来控制目标组件的任务模型。
【适应环境配置变化】很多配置信息会随着设备和环境因素的变更而有所改变,比如硬键盘消失,屏幕朝向,语言环境等。当配置信息产生变化时,正在与用户进行交互的界面组件需要根据这些变化及时作出调整,为用户提供最合适的交互方式。在默认情况下,当配置信息发生变化时,Android会简单地销毁当前交互的界面组件对象,并根据新的配置信息重新构建该组件对象。如果不期望组件随着某个配置信息进行销毁重建,可以通过activity配置项configChanges来标明。并在Activity.onConfigurationChanged函数中更精细地处理相关配置的变化事件。

界面组件的数据结构


Activity派生自Context类,Context类提供了应用运行的基本环境,是各组件和系统服务通信的桥梁。
Context类是个抽象类,ContextImpl派生实现了它的抽象接口。
ContextImpl对象会与Android框架层的各个服务建立远程连接,通过Andorid进程间的通信机制(IPC)和这些服务进行通信。
通过Context的抽象和封装,隐藏了应用与系统服务通信的细节,简化了上层应用的开发。ContextImpl是在Android组件管理服务构造各组件对象时被实例化的。
ContextWrapper的设计应用了修饰模式,它派生自Context,其中的具体实现都是通过组合的方式调用ContextImpl的实例来完成的。这样的设计,使得ContextImpl与ContextWrapper子类的实现可以单独变化,彼此独立。
Android的界面组件Activity、服务组件Service以及应用基类Application都派生于ContentWrapper,他们可以通过重载来修改Context接口的实现

服务组件Service解析

Android的服务组件派生自Service类,
从运行模式上来看,Android的服务组件没有运行在独立的进程或线程中。默认情况下,服务组件构造于应用进程中,并且和所有其他的Android组件一样,都在进程的主线程(即UI线程)中运行。这就意味着,如果直接在服务组件中同步执行耗时的操作,就会导致主线程阻塞或界面假死,从而无法响应用户的操作。从使用方式来看,服务组件可以与前端界面组件建立双向连接,提供数据和功能支持,也可以单向接受Intent对象的请求,进行数据的分析处理和功能调度。

服务组件的功能和特征

以闹钟为例:

在这种模式下,服务组件扮演的角色是功能调度者。从事件触发器对象那里收集各类事件信息,进一步分析和处理,然后更新界面、修改数据抑或进行其他相关的存在,调度整个应用使其保持正确的状态。
服务组件还可以扮演另一个很重要的角色,界面组件的功能提供者。在有的场景下,应该需要停留在自己的交互界面与用户交流。此时它不需要复用第三方界面,而只需要获得一些功能和状态数据即可,这样的支持就是通过服务组件来提供的。
输入法框架即是一个基于服务组件进行复用的例子。


输入法界面组件通过调用bindService函数发起连接请求。输入法服务(InputMethodService)的onBind方法会被调用并构造一个IBinder对象返回给输入法界面组件,从而建立一个IPC连接,界面组件可以通过远程方法调用来进行输入法相关操作,结束服务后,应定义unbindService函数终止连接。

服务组件的开发和使用

构造Service的子类,将其注册在配置文件中。
构造一个扮演调度者角色的服务组件需要实现的函数是onStartCommand,需要注意的是,在Android中,所有的组件都是在主线程上构造的,因此,onStartCommand函数的执行会阻塞主线程。如果涉及数据库读写、网络通信、复杂运算等耗时操作,那么就需要将相关操作放入独立的进程或线程中去执行。
将组件放入独立的进程中,可以通过配置文件的process参数来实现。
但这样的方式会增加进程开销,另一种可行的策略是在服务组件中另起一个独立的线程,将那些耗时又费力的操作交给他来打理。
最简单的实现策略是通过派生IntentService来执行服务组件中的处理逻辑。

 private final class ServiceHandler extends Handler {
        public ServiceHandler(Looper looper) {
            super(looper);
        }
        @Override
        public void handleMessage(Message msg) {
            onHandleIntent((Intent)msg.obj);
            stopSelf(msg.arg1);
        }
    }

public void onCreate() {
        // TODO: It would be nice to have an option to hold a partial wakelock
        // during processing, and to have a static startService(Context, Intent)
        // method that would launch the service & hand off a wakelock.
        super.onCreate();
        HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
        thread.start();
        mServiceLooper = thread.getLooper();
        mServiceHandler = new ServiceHandler(mServiceLooper);
    }

IntentService的实现原理:本身是Service的子类,启动时,会在onCreate函数中建立一个后台线程,该线程通过其独立的消息循环在后台等待事件,当onStartCommand函数接到相关的Intent参数时,主线程会将其中的内容打包发送到后台进程中,在onHandleIntent函数中进行处理,从实现来看,只需把本应放在onStartCommand中执行的逻辑内容挪到onHandleIntent中即可。如果用过bindService函数来绑定服务组件建立连接,那么要处理的就是onBind函数,onBind函数会接收到通过bindService函数发出的Intent对象,依照Intent对象的不同,Service.onBind函数会返回对应的IBinder对象,整个绑定过程就是一个异步操作,组件管理服务参与其中进行调度。onBind函数返回的IBinder对象,会通过调用ServiceConnection对象的onServiceConnected方法传递给调用者,调用者拿到这个IBinder对象后,就可以通过它与服务组件进程远程方法调用。
当不需要与服务组件通信时,调用unbindService方法关闭连接。

服务组件的进程间通信模型

在实际应用中,前台界面组件和后台服务组件可能来自不同的应用,这就需要进程间通信。android的进程间通信模型主要包含三个方面:
【Android的进程间通信模型架构】典型的代理模式(Proxy Pattern)



在Proxy对象中,每个函数接口都有一个对应的指令值。当调用者调用这个接口时,Proxy会将数据和指令序列化成一个消息,发送到远端的Stub对象。Stub对象会拆解出对应的指令和数据,并根据指令执行对应的逻辑,将结果返回给接口调用者,整个流程对于调用者而言完全透明。
Proxy数据包的发生和回传工作,是通过Binder来实现的。在Binder对象中,有一个后台消息循环线程,Proxy传来的消息包会扔到消息队列中等待解析和处理,Stub对象都是Binder的子类型,在服务端被实例化,其接口和实现与Proxy一一匹配,负责将Proxy传递过来的消息进行解包,得到其中的指令和数据。在实际应用中。每个Stub都会有一个实现子类Implement,实际上是由它来真正负责在Binder的后台线程中执行所调用的功能。
界面组件可以通过bindService获得IBinder的对象,通过它的静态方法asInterface可以获得Proxy对象实例。界面端使用Proxy对象,就可以实现对服务端功能的远程调用。
对于IPC方法的调用是一个同步的流程,如果执行时间过长,就会阻塞调用方的线程,这时候需要用到异步IPC调用。


构造一个异步的IPC方法调用,需要传入另一个IInterface对象,它的Stub对象(图中的Callback.Stub)在调用组件,而它的Porxy对象(图中的Callback.Proxy)会随着序列化传输到服务组件,供服务端在操作执行完成后回调通知。服务组件完成操作后,会转换角色扮演调用方,通过Callback.Proxy对象中的方法,将执行结果通知给调用组件。

与第一次调用不同,通过Callback.Proxy对象中的方法,这个调用通常是一个非阻塞性质的,即服务组件不等调用组件执行完成后便立刻返回。
【框架代码自动生成】整个进程通信模型中有固定的类型和方法需要实现,包含很多琐碎而雷同的序列化与反序列化操作。通过AIDL(Android Interfacr Deifinition Language)的帮助来自动生成这些框架代码的
AIDL是一种接口描述语言,语法取自Java,在参数上增加了输入输出方向的控制。android SDK中提供了AIDL的解析工具,根据所提供的AIDL文件,自动生成对应的MY_API、MY_API.Proxy、MY_API.Stub等类型的Java文件。开发者只需要基础MY_API.Stub,实现Implement类型,填充真实的执行代码即可,无需关注其他的底层通信细节。
【参数序列化】整个进程间通信的流程中,为了将一个进程中的数据传递到另一个进程中,还有一个重要的步骤,就是数据的序列化和反序列化—-这是所有的进程间通信的基础。
在Android中,负责序列化和反序列化数据的是Parcel类,它提供了一系列的write和read接口,支持多种类型数据的序列化操作。
支持的数据类型主要有三种

  • 基本数据和他们的列表、数组对象。
  • 实现Parcelable接口的子类型对象—-子类型通过派生writeToParcel方法和提供构造函数的途径实现序列化相关的操作。
  • IBinder和IInterface的子类型对象也可以通过Parcel来序列化。

Parcel序列化后的数据是齐位的二进制流。
进程间通信机制是android的重要基础,Parcel类型的实现主要通过C++来实现,上层Parcel对象通过JNI接口进行调用,从而提高了序列化相关操作的执行效率,确保Android系统可以高效运行。

Context.getSystemService接口来获得指定的系统服务,这些系统服务,并不是通过服务组件来实现的,他们都位于系统的核心进程中,有独立的线程空间。
通过Context.getSystemService获得的,其实是这些服务的代理对象,这些对象会和真正的服务线程建立连接,通过IPC调用来实现对应的方法。

触发器组件Broadcast Receiver解析

所谓触发器组件,是派生自BroadcastReceiver的子类型。它的实现集中在onReceive方法中。

触发器组件的功能和特征

在使用触发器组件时,只能够把它当作一个函数来使用,除了一些初始化构造时传入的成员变量,其余都没有用武之地,不会有机会被用到。功能函数onReceive的执行必须是同步且快速的,否则会阻塞与用户交互的当前进程。

常见的触发器组件使用模式

触发器组件的设计,解决了后台事件监听问题。在安卓中,只有当事件真正发生时,组件管理服务才会根据配置信息通知对应的触发器组件对象,构造执行组件的进程。

触发器组件的使用

使用触发器组件进行时间监听有两种方法,分别为冷拔插和热拔插。

  • 冷拔插,就是将触发器组件的相关信息写在应用的配置文件中。
  • 热拔插,通过registerReceiver和unregisterReceiver,动态的将触发器组件与所需要监听的时间进行绑定。实际开发中,一般会在activity的onResume函数中进行触发器组件的注册,而在onPause函数中注销对应的触发器组件。

广播事件的发送

广播事件是通过Intent对象来表示,广播事件需要通过sendBroadcast或sendOrderedBroadcast函数进行发送

通过sendBroadcast的普通广播模式,这种模式,所有注册了该广播事件的触发器组件都会获得事件通知,并发地在各自的应用进程中执行。

通过sendOrderedBroadcast的有序广播模式,所有监听该事件的触发器组件,都会依照设定的优先级进行排序,从高到低依次处理该事件。高优先级的触发器组件可以通过abortBroadcast方法优先终止这个广播的传播,这样,低优先级的触发器组件就不再有机会处理该事件了。在有序广播事件的传递过程中,每个执行中的触发器组件都可以通过setResult等函数在该事件消息中附加额外的数据,而下一个处理该事件的触发器组件则能够使用这些数据。通过这样的方式,事件广播来构成一个消息数据处理链,为了保证该事件一定会被处理,广播事件的发送者还可以指明默认触发器组件,如果事件的传播没有被提前终止,该触发器组件会在最后来响应该事件。

数据源组件Content Provider解析

和其他组件不同,数据源组件并不包含特定的功能逻辑,而是负责为应用提供数据访问的接口。

数据源组件的定位和操作

数据源组件派生自抽象类ContentProvider,需要实现其中的query、update、insert和delete等抽象接口。数据源组件中数据存储的方式没有任何限制,可以通过数据库、文件等任意方式来实现
整个数据源组件的接口设计集合了REST标准和数据库设计的概念,它通过URI(Uniform Resource Identifier)进行定位,像数据库一样,通过SQL语句来描述具体的操作
URI,全局统一定位标志,通过一个结构化的字符串,唯一标志数据源的地址信息。网络地址URL,是其中的一个子类,每个数据源组件都有一个唯一的URI标识

组件开发者需要在配置文件中对应的provider条目下声明URI信息的地址描述。

<provider android:name="SimpleContentProvider"
    android:authorities="com.duguhome.prodiver.sample">
</provider>

数据源组件URI的命名通常要求与所在应用的包名相关联,以保证在同一个Android设备上具有唯一性。如果在安装应用的时候,发现设备中已经存在具有相同URI地址的数据源组件,新的应用会由于数据源组件冲突而安装失败。

数据源组件也可以用REST的方式来定义操作。

# 对列表类型操作
content://com.duguhome.provider.sample/items
# 对id为1的条目进行操作
content://com.duguhome.provider.sample/items/1

REST是基于HTTP协议而设计的。
为了增加对数据的控制力,android同时为数据源组件引入了SQL语句的支持。

 数据源组件的开发

从ContentProvider类派生,并实现抽象方法,同时需要在配置文件中描述其URI等信息。
直接对数据源组件进行数据的读写操作可能会阻塞主线程,从而影响应用于用户的交互。当涉及大量的读写和查询时,调用者可以通过AsyncQueryHandler对象来实现对数据源组件的异步访问。每个AsyncQueryHandler对象都会开启一个后台线程,在线程中执行与数据源组件的数据交互,进行数据增删改查操作。

token帮助调用者确定是那一次请求

数据源组件的实现细节

ContentResolver相当于数据源组件的DNS和本地代理,它负责将各个URI定位到具体的数据源组件,并经由它对数据源进行增删改查等操作。
ContentResolver对数据的操作,其实是分两个步骤完成的,首先是定位,根据URI找到对应的数据源组件,然后,通过对应的数据源组件执行所请求的操作。

有一个数据源组件的缓存对象ProviderMap,它存储各个URI对应到数据源组件对象。

ContentResolver缓存的数据源组件对象,其实是对于数据源组件的代理。当ContentResolver调用其接口进行操作时,相关指令打包成消息,通过Android进程间通信机制传到远端的数据源组件中,而数据源组件执行完成后再将结果序列化传回,整个流程是个同步的操作。

默认情况下,每个数据源组件都只有一个实例,来自不同进程的要求都会通过进程间通信机制与其交互,如果频繁地进行交互则开销较大,可以配置参数mutiprocess为true,此时,数据源组件会在每个调用它的应用进程中构造一个组件对象,避免进程间通信的开销,从而提高操作和数据传输的效率

对数据源进程查询时,数据需要从数据源组件所在的进程中拷贝到调用者所在的进程中。
一次性拷贝浪费时间,浪费内存,每次拷贝一条增加进程间通信的成本。android采用了数据窗口的模式。在数据指针对象Cursor中,包含一个CursorWindow对象,它会在调用者一端缓存部分数据,缓存数据的内容包含当前指针指向位置相关的若干条数据。CursorWindow类的底层实现是基于C++的

应用配置文件解析

权限配置

权限配置包含权限的定义和权限使用声明两部分内容。
应用需要使用的权限,需要通过配置项user-permission来声明。
如果开发者需要定义权限来自第三方应用的访问,则通过permission配置项来进行定义。

    < 
    permission
    android:name=""
    android:label="权限的名字"
    android:description="权限的具体描述"
    android:permissionGroup=
    "android.permission-group.COST_MONEY"
    android:protectionLevel="normal"
    >

定义了的权限还需要部署到对应的组件才能生效,组件管理服务在构造一个组件对象时,会校验请求组件的权限声明是否与该组件的权限配置相匹配(如果请求组件和实现组件位于同一应用,无需进行检查),如果匹配失败,会抛出异常阻止这次调用。android的权限体系没有传递性。

权限也可以不事先部署在组件上,而是在运行时调用checkPermission函数动态校验。

 

 

 

 

 

 

 

posted @ 2015-10-30 17:01  包子糖Sakura  阅读(...)  评论(...编辑  收藏