阿里云对象的生命周期是从上传成功到被删除为止吗?

在讨论这些方面时,除了从使用的角度介绍以外,还结合具体的驱动实现,分析这些功能对应的内部原理,以加深读者的理解。

为了在阅读文章时有一个共同的认识,本文首先简单介绍了OSS的一些基本内容。 关于OSS编程更详细的介绍,可以参考IBM DeveloperWork以前的 。

声卡中主要有两个基本装置:Mixer和CODEC(ADC/DAC)。Mixer用来控制输入音量的大小,对应的设备文件为/dev/mixer;CODEC用来实现录音(模拟信号转变为数字信号)和播放声音(数字信号转变为模拟信号)的功能,对应的设备文件为/dev/dsp。

开发OSS应用程序的一般流程是:

2.缓冲区设置的性能分析

在设置驱动内部的缓冲区时,存在一个矛盾:在声卡驱动程序中,为了防止抖动的出现,保证播放的性能,设置了内部缓冲区-DMA buffer。在播放时,应用程序通过驱动程序首先将音频数据从应用程序缓冲区-APP buffer,写入到DMA buffer。接着,由DMA控制器把DMA buffer中的音频数据发送到DAC(Digital-Analog Converter)。某些时刻CPU非常的繁忙,比如正在从磁盘读入数据,或者正在重画屏幕,没有时间向DMA buffer放入新的音频数据。DAC由于没有输入新的音频数据,导致声音播放的间断,这就出现了声音的抖动现象。此时,需要将DMA buffer设置的足够大,使得DAC始终有数据播放。但是,DMA buffer的增大使得每次从APP buffer拷贝的时间也变长,导致了更大的播放延迟。这对于那些延迟敏感的应用场合,如与用户有交互的音频应用程序,就会出现问题。

对于这个矛盾,可以从两个不同的方面分别着手解决。驱动程序采用多缓冲(Multi-buffering)的方式,即将大的DMA buffer分割成多个小的缓冲区,称之为fragment,它们的大小相同。驱动程序开始时只需等待两个fragment满了就开始播放。这样可以通过增加fragment的个数来增加缓冲区的大小,但同时每个fragment被限制在合适的大小,也不影响时延。音频驱动程序中的多缓冲机制一般会利用底层DMA控制器的scatter-gather功能。

另一方面,应用程序也可指导驱动程序选择合适大小的缓冲区,使得在没有抖动的情况下,时延尽可能的小。特别的,应用程序将驱动程序中的缓冲通过mmap映射到自己地址空间后,会以自己的方式来处理这些缓冲区(与驱动程序的不一定一致),这时应用程序往往会先根据自己的需要设置驱动程序中内部缓冲区的大小。

在OSS的ioctl接口中,SNDCTL_DSP_SETFRAGMENT就是用来设置驱动程序内部缓冲区大小。具体的用法如下:

为了给音频程序的开发者展示缓冲区配置对播放效果的影响,我们将对缓冲区配置与播放性能的关系进行测试。下面首先介绍测试的环境,包括测试方法的原理和测试结果的含义;接着针对两种情况进行测试,并解释测试的结果。

测试是在PC机上进行的,具体的测试环境参见下表。

主板集成(工作在44.1KHz,立体声,16bit的模式)

测试软件(latencytest)由两部分组成:音频播放测试程序、系统运行负载模拟程序。(注:latencytest软件主要目的是测试内核的时延,但这里作为对不同缓冲配置进行比较的工具。)

音频播放测试程序的工作流程见下面的代码。为了保证音频播放在调度上的优先性,音频播放测试程序使用SCHED_FIFO调度策略(通过sched_setscheduler())。

my_gettime返回当前的时刻,在每个操作的开始和结束分别记录下时间,就可以得到操作所花费的时间。audio_fd为打开音频设备的文件描述符,playbuffer是应用程序中存放音频数据的缓冲区,也就是APP buffer,fragmentsize为一个fragment的大小,write操作控制向驱动写入一个fragment。空循环用来模拟在播放音频时的CPU运算负载,典型的例子是合成器(synthesizer)实时产生波形后,再进行播放(write)。空循环消耗的时间长度设置为一个fragment播放时延的80%。

相关指标的计算方法如下:

为了模拟真实的系统运行情况,在测试程序播放音频数据的同时,还运行了一个系统负载。一共设置5种负载场景,按顺序分别是:

  • 1) 高强度的图形输出(使用x11perf来模拟大量的BitBlt操作)
  • 2) 高强度对/proc文件系统的访问(使用top,更新频率为0.01秒)
  • 3) 高强度的磁盘写(向硬盘写一个大文件)
  • 4) 高强度的磁盘拷贝(将一个文件拷贝到另一个地方)
  • 5) 高强度的磁盘读(从硬盘读一个大文件)

针对不同的系统负载场景,测试分别给出了各自的结果。测试结果以图形的形式表示,测试结果中图形的含义留待性能分析时再行解释。

下面,我们分别对两种缓冲区的配置进行性能比较,

为了看懂测试结果,需要了解测试结果图形中各种标识的含义:

  • 1) 红线:全部缓冲区的播放时延。全部缓冲区播放时延 = 一个fragment时延 x fragment的个数。对于测试的第一种情况,全部缓冲区时延 = 2.90ms x 2 = 5.8ms。
  • 2) 白线:实际的调度时延,即一次循环的时间(time3-time1)。如果白线越过了红线,则说明所有的缓冲区中音频数据播放结束后,应用程序仍然没有来得及将新的数据放入到缓冲区中,此时会出现声音的丢失,同时overruns相应的增加1。
  • 3) 绿线:CPU执行空循环的时间(即前面的time2-time1)。绿线的标称值为fragm.latency x 80%。由于播放进程使用SCHED_FIFO调度策略,所以如果绿线所代表的时间变大,则说明出现了总线竞争,或者是系统长时间的处于内核中。
  • 4) 黄线:一个fragment播放时延。白线应该接近于黄线。

第一种情况的缓冲区很小,每个fragment只有512字节,总共的缓冲区大小为2 x 512 = 1024字节。1024字节只能播放5.8ms。根据OSS的说明,由于Unix是一个多任务的操作系统,有多个进程共享CPU,播放程序必须要保证选择的缓冲区配置要提供足够的大小,使得当CPU被其它进程使用时(此时不能继续向声卡传送新的音频数据),不至于出现欠载的现象。欠载是指应用程序提供音频数据的速度跟不上声卡播放的速度,这时播放就会出现暂停或滴答声。因此,不推荐使用fragment大小小于256字节的设置。从测试结果中看到,不管使用那种系统负载,都会出现欠载的现象,特别是在写硬盘的情况下,一共发生了14次欠载(overruns

当然,对于那些实时性要求高的音频播放程序,希望使用较小的缓冲区,因为只有这样才能保证较小的时延。在上面的测试结果我们看到了欠载的现象,但是,这并不完全是缓冲区过小所导致的。实际上,由于Linux内核是不可抢占的,所以无法确知Linux在内核中停留的时间,因此也就无法保证以确定的速度调度某个进程,即使现在播放程序使用了SCHED_FIFO调度策略。从这个角度来说,多媒体应用(如音频播放)对操作系统内核提出了更高的要求。在目前Linux内核的情况下,较小的调度时延可以通过一些专门的内核补丁(low-latency patch)达到。不过我们相信Linux2.6新内核会有更好的表现。

第二种情况的缓冲区要大得多,总共的缓冲区大小为4 x 2048 = 8192字节。8192字节可以播放0.046秒。从测试的图形来看,结果比较理想,即使在系统负载较重的情况,仍然能够基本保证播放时延的要求,而且没有出现一次欠载的现象。

当然,并不是说缓冲区越大越好,如果继续选择更大的缓冲区,将会产生比较大的时延,对于实时性要求比较高的音频流来说,是不能接受的。从测试结果中可以看到,第二种配置的时延抖动比第一种配置要大得多。不过,在一般情况下,驱动程序会根据硬件的情况,选择一个缺省的缓冲区配置,播放程序通常不需要修改驱动程序的缓冲区配置,而可以获得较好的播放效果。

如果播放程序写入的速度超过了DAC的播放速度,DMA buffer就会充满了音频数据。应用程序调用write时就会因为没有空闲的DMA buffer而被阻塞,直到DMA buffer出现空闲为止。此时,从某种程度来说,应用程序的推进速度依赖于播放的速度,不同的播放速度就会产生不同的推进速度。因此,有时我们不希望write被阻塞,这就需要我们能够知道DMA buffer的使用情况。

以上的代码不停的查询驱动程序中是否有空的fragment(SNDCTL_DSP_GETOSPACE),如果没有,则进入睡眠(usleep(100)),此时应用程序做其它的事情,比如更新画面,网络传输等。如果有空闲的fragment(info.fragments > 0),则退出循环,接着就可以进行非阻塞的write了。

除了依赖于操作系统内核提供更好的调度性能,音频播放应用程序也可以采用一些技术以提高音频播放的实时性。绕过APP buffer,直接访问DMA buffer的mmap方法就是其中之一。

我们知道,将音频数据输出到音频设备通常使用系统调用write,但是这会带来性能上的损失,因为要进行一次从用户空间到内核空间的缓冲区拷贝。这时,可以考虑利用mmap系统调用,获得直接访问DMA buffer的能力。DMA控制器不停的扫描DMA buffer,将数据发送到DAC。这有点类似于显卡对显存的操作,大家都知道,GUI可以通过mmap将framebuffer(显存)映射到自己的地址空间,然后直接操纵显存。这里的DMA buffer就是声卡的framebuffer。

理解mmap方法的最好方法是通过实际的例子, 。

代码中有详细的注释,这里只给出一些说明。

PlayerDMA函数的参数samples指向存放音频数据的缓冲,rate/bits/channels分别说明音频数据的采样速率、每次采样的位数、声道数。

在打开/dev/dsp以后,根据/rate/bits/channels参数的要求配置驱动程序。需要注意的是,这些要求并一定能得到满足,驱动程序要根据自己的情况选择,因此在配置后,需要再次查询,获取驱动程序真正使用的参数值。

音频驱动程序针对播放和录音分别有各自的缓冲区,mmap不能同时映射这两组缓冲,具体选择映射哪个缓冲取决于mmap的prot参数。PROT_READ选择输入(录音)缓冲,PROT_WRITE选择输出(播放)缓冲,代码中使用了PROT_WRITE|PROT_READ,也是选择输出缓冲。(这是BSD系统的要求,如果只有PROT_WRITE,那么每次对缓冲的访问都会出现segmentation/bus

DMA一旦启动后,就会周而复始的扫描DMA buffer。当然我们总是希望提前为DMA准备好新的数据,使得DMA的播放始终连续。因此,PlayerDMA函数将mmap后的DMA buffer分割成前后两块,中间设置一个界限。当DMA扫描前面一块时,就填充后面一块。一旦DMA越过了界限,就去填充前面一块。

使用mmap的问题是,不是所有的声卡驱动程序都支持mmap方式。因此,在出现不兼容的情况下,应用程序要能够转而去使用传统的方式。

最后,为了能深入的理解mmap的实现原理,我们以某种声卡驱动程序为例,介绍了其内部mmap函数时具体实现。 

当然,除了上面所讨论的问题以外,音频应用的开发还有很多实际的问题需要去面对,比如多路音频流的合并,各种音频文件格式的打开等等。

OSS音频接口存在于Linux内核中许多年了,由于在体系结构上有许多的局限性,在Linux 2.6内核中引入了一种全新的音频体系和接口——ALSA(Advanced Linux Sound Architecture),它提供了很多比OSS更好的特性,包括完全的thread-safe和SMP-safe,模块化的设计,支持多个声卡等等。为了保持和OSS接口的兼容性,ALSA还提供了OSS的仿真接口,使得那些为OSS接口开发的大量应用程序仍然能够在新的ALSA体系下正常的工作。

    1. 定义两个Activity类,并为两个Activity类编写布局文件:

    2. 电池电量充足(即从电量低变化到饱满时会发出广播
      系统启动完成后(仅广播一次)
      按下照相时的拍照按键(硬件按键)时
      设备当前设置被改变时(界面语言、设备方向等)
      未正确移除SD卡但已取出来时(正确移除方法:设置–SD卡和设备内存–卸载SD卡)
      插入外部储存装置(如SD卡)

      注:当使用系统广播时,只需要在注册广播接收者时定义相关的action即可,并不需要手动发送广播,当系统有相关操作时会自动进行系统广播

        发送出去的广播被广播接收者按照先后顺序接收

有序是针对广播接收者而言的

  • 广播接受者接收广播的顺序规则(同时面向静态和动态注册的广播接受者)

    1. 按照Priority属性值从大-小排序;
    2. Priority属性相同者,动态注册的广播优先;
  • 先接收的广播接收者可以对广播进行截断,即后接收的广播接收者不再接收到此广播;
  • 先接收的广播接收者可以对广播进行修改,那么后接收的广播接收者将接收到被修改后的广播
  • 有序广播的使用过程与普通广播非常类似,差异仅在于广播的发送方式:

    • 其他App针对性发出与当前App intent-filter相匹配的广播,由此导致当前App不断接收广播并处理;
    • 其他App注册与当前App一致的intent-filter用于接收广播,获取广播具体信息;
      即会出现安全性 & 效率性的问题。
  1. App应用内广播可理解为一种局部广播,广播的发送者和接收者都同属于一个App。
  2. 相比于全局广播(普通广播),App应用内广播优势体现在:安全性高 & 效率高
  • 具体使用1 - 将全局广播设置成局部广播

    1. 注册广播时将exported属性设置为false,使得非本App内部发出的此广播不被接收;
    2. 在广播发送和接收时,增设相应权限permission,用于权限验证;
    3. 发送广播时指定该广播接收器所在的包名,此广播将只会发送到此包中的App内与之相匹配的有效广播接收器中。
  • 使用方式上与全局广播几乎相同,只是注册/取消注册广播接收器和发送广播时将参数的context变成了LocalBroadcastManager的单一实例


5.3.3、普通广播和有序广播的区别
  • 普通广播:异步执行 --> 广播接收器接收广播没有先后顺序;效率较高,无法被拦截
  • 有序广播:同步执行 --> 同一时刻只会有一个广播接收器能够接收到信息;根据优先级有先后顺序;可以被拦截

通常service用来执行一些耗时操作,或者后台执行不提供用户交互界面的操作。其他的应用组件可以启动Service,即便用户切换

了其他应用,启动的Service仍可在后台运行。一个组件可以与Service绑定并与之交互,甚至是跨进程通信(IPC)。例如,一个

Service可以在后台执行网络请求、播放音乐、执行文件读写操作或者与 content provider交互等。

为了创建Service,需要继承Service类。并重写它的回调方法,这些回调方法反应了Service的生命周期,并提供了绑定Service的

机制。最重要的Service的生命周期回调方法如下所示:

  • 当其他组件调用bindService()方法请求绑定Service时,该方法被回调。该方法返回一个IBinder接口,该接口是Service与绑定的组件进行交互的桥梁。若Service未绑定其他组件,该方法应返回null。

  • 当Service第一次创建时,回调该方法。该方法只被回调一次,并在onStartCommand() 或 onBind()方法被回调之前执行。若Service处于运行状态,该方法不会回调。

  • 当Service被销毁时回调,在该方法中应清除一些占用的资源,如停止线程、结束绑定注册的监听器或broadcast receiver 等。该方法是Service中的最后一个回调。

上面两条路径并不是毫不相干的:当调用startService()后,您仍可以bind该Service。比如,当播放音乐时,需调用startService()启

动指定播放的音乐,当需要获取该音乐的播放进度时,则需要调用bindService(),在这种情况下,直到Service被unbind ,调用

当系统内存低时,系统将强制停止Service的运行;若Service绑定了正在与用户交互的activity,那么该Service将不大可能被系统

kill。如果创建的是前台Service,那么该Service几乎不会被kill。否则,当创建了一个长时间在后台运行的Service后,系统会降低

该Service在后台任务栈中的级别——这意味着它容易被kill,所以在开发Service时,需要使Service变得容易被restart,因为一旦

onStartCommand()方法必须返回一个整数,这个整数是一个描述了在系统的kill事件中,系统应该如何继续这个服务的值。

  • intents传递,否则Intent将为null。该模式适合做一些类似播放音乐的操作。

  • “非粘性的”。若执行完onStartCommand()方法后,系统就kill了service,不要再重新创建service,除非系统回传了一个pending intent。这避免了在不必要的时候运行service,您的应用也可以restart任何未完成的操作。

  • START_STICKY的兼容版本,但不保证服务被kill后一定能重启。

除此之外,在标签中还可以配置其他属性

  • android:label —>服务的名字,如果此项不设置,那么默认显示的服务名则为类名
  • android:permission —>申明此服务的权限,这意味着只有提供了该权限的应用才能控制或连接此服务
  • android:process —>表示该服务是否运行在另外一个进程,如果设置了此项,那么将会在包名后面加上这段字符串表示另一进程的名字
  • android:exported —>表示该服务是否能够被其他应用程序所控制或连接,不设置默认此项为 false

有了 Service 类我们如何启动他呢,有两种方法:

当然,service也可以同时在上述两种方式下运行。这涉及到Service中两个回调方法的执行:onStartCommand()(通过start方式启

动一个service时回调的方法)、onBind()(通过bind方式启动一个service回调的方法)。

传递参数。当然,您也可以将Service在manifest文件中配置成私有的,不允许其他应用访问。

常,一个被start的Service会在后台执行单独的操作,也并不给启动它的组件返回结果。比如说,一个start的Service执行在后台下

载或上传一个文件的操作,完成之后,Service应自己停止。

一般使用如下两种方式创建一个start Service

  • 请务必在Service中开启线程来执行耗时操作,因为Service运行在主线程中。

  • onHandleIntent()方法,该方法接收一个回传的Intent参数,您可以在方法内进行耗时操作,因为它默认开启了一个子线程,操

如果你需要在Service中执行多线程而不是处理一个请求队列,那么需要继承Service类,分别处理每个Intent。在Service中执行操

作时,处理每个请求都需要开启一个新线程(new Thread()),并且同一时刻一个线程只能处理一个请求

添加Button点击事件:

这样的话,一个简单的带有Service功能的程序就写好了,现在我们将程序运行起来,并点击一下Start Service按钮,可以看到

LogCat的打印日志如下:

那么如果我连续两次点击Start Service按钮呢?这个时候的打印日志如下:

上面如果我们的耗时任务时间够长,在MyService停止之前点击”返回”,Activity被干掉了,但是我们的服务仍然在运行,可以查看

设置–>应用–>正在运行,截图如下:

在大多数情况下,start Service并不会同时处理多个请求,因为处理多线程较为危险,所以继承IntentService类带创建Service是个

  • 当所有请求处理完成后,自动停止service,无需手动调用stopSelf()方法;

综上所述,您只需重写onHandleIntent()方法即可,当然,还需要创建一个构造方法,示例如下:

用各自的父类方法以保证子线程能够正常启动。

比如,要实现onStartCommand()方法,需返回其父类方法:

动了。”然后Service就去忙自己的事情了。那么有没有什么办法能让它们俩的关联更多一些呢?比如说在Activity中可以指定让

Service去执行什么任务,当然可以,只需要让Activity和Service建立关联就好了。

类,在客户端操作这个类就能和这个服务通信了,比如得到 Service 运行的状态或其他操作。如果 Service 还没有运行,使用这个

观察MyService中的代码,你会发现有一个onBind()方法我们都没有使用到,这个方法其实就是用于和Activity建立关联的,重新写

务,而且在Service中还写了一个doSomethingInService()方法,同样可以执行后台任务,其实这里只是打印了一行日志。

方法中,我们又通过向下转型得到了MyBinder的实例,有了这个实例,Activity和Service之间的关系就变得非常紧密了。现在我们

当然,现在Activity和Service其实还没关联起来了呢,这个功能是在Bind Service按钮的点击事件里完成的。可以看到,这里我们

个参数就是刚刚构建出的Intent对象,第二个参数是前面创建出的ServiceConnection的实例,第三个参数是一个标志位,有两个

现在让我们重新运行一下程序吧,在MainActivity中点击一下Bind Service按钮,LogCat里的打印日志如下图所示:

另外需要注意,任何一个Service在整个应用程序范围内都是通用的,即MyService不仅可以和MainActivity建立关联,还可以和任

何一个Activity建立关联,而且在建立关联时它们都可以获取到相同的MyBinder实例。

在Service的启动这一部分,我们已经简单介绍了销毁Service的方法。

以上这两种销毁的方式都很好理解。

但有几点需要注意一下:

finish 的时候绑定会自动解除,并且Service会自动停止);

(4)当在旋转手机屏幕的时候,当手机屏幕在“横”“竖”变换时,此时如果你的 Activity 如果会自动旋转的话,旋转其实是 Activity

的重新创建,因此旋转之前的使用 bindService 建立的连接便会断开(Context 不存在了),对应服务的生命周期与上述相同。

连接会抛出异常,因此我创建了一个 boolean 变量来判断是否 unbindService 已经被调用过。

若系统正在处理多个调用onStartCommand()请求,那么在启动一个请求时,你不应当在此时停止该Service。为了避免这个问题,

您可以调用stopSelf(int)方法,以确保请求停止的Service是最新的启动请求。这就是说,当调用stopSelf(int)方法时,传入的ID代表

的Start请求,ID将无法匹配,Service并不会停止。具体的例子参见上面Service启动一节。

Thread我们大家都知道,是用于开启一个子线程,在这里去执行一些耗时操作就不会阻塞主线程的运行。而Service我们最初理解

的时候,总会觉得它是用来处理一些后台任务的,一些比较耗时的操作也可以放在这里运行,这就会让人产生混淆了。但是,如果

我告诉你Service其实是运行在主线程里的,你还会觉得它和Thread有什么关系吗?

现在重新运行一下程序,并点击Start Service按钮,会看到如下Log信息:

可以看到,它们的线程id完全是一样的,由此证实了Service确实是运行在主线程里的,也就是说如果你在Service里编写了非常耗

时的代码,程序也会出现ANR的。

下面我详细的来解释一下:

  • Thread:Thread 是程序执行的最小单元,它是分配CPU的基本单位。可以用 Thread 来执行一些异步的操作。
  • main 线程上。 因此请不要把 Service 理解成线程,它跟线程半毛钱的关系都没有!

Android的后台就是指,它的运行是完全不依赖UI的。即使Activity被销毁,或者程序被关闭,只要进程还在,Service就可以继续运行。比如说一些应用程序,始终需要与服务器之间始终保持着心跳连接,就可以使用Service来实现。你可能又会问,前面不是刚刚验证过Service是运行在主线程里的么?在这里一直执行着心跳连接,难道就不会阻塞主线程的运行吗?当然会,但是我们可以在Service中再创建一个子线程,然后在这里去处理耗时逻辑就没问题了。

既然在Service里也要创建一个子线程,那为什么不直接在Activity里创建呢?这是因为Activity很难对Thread进行控制,当Activity被销毁之后,就没有任何其它的办法可以再重新获取到之前创建的子线程的实例。而且在一个Activity中创建的子线程,另一个Activity无法对其进行操作。但是Service就不同了,所有的Activity都可以与Service进行关联,然后可以很方便地操作其中的方法,即使Activity被销毁了,之后只要重新与Service建立关联,就又能够获取到原有的Service中Binder的实例。因此,使用Service来处理后台任务,Activity就可以放心地finish,完全不需要担心无法对后台任务进行控制的情况。你也可以在

一个比较标准的Service就可以写成:

Service几乎都是在后台运行的,一直以来它都是默默地做着辛苦的工作。但是Service的系统优先级还是比较低的,当系统出现内存不足情况时,就有可能会回收掉正在后台运行的Service。如果你希望Service可以一直保持运行状态,而不会由于系统内存不足的原因导致被回收,就可以考虑使用前台Service。前台Service和普通Service最大的区别就在于,它会一直有一个正在运行的图标在系统的状态栏显示,下拉状态栏后可以看到更加详细的信息,非常类似于通知的效果,只有前台Service被destroy后,状态栏显示才能消失。当然有时候你也可能不仅仅是为了防止Service被回收才使用前台Service,有些项目由于特殊的需求会要求必须使用前台Service,比如说墨迹天气,它的Service在后台更新天气数据的同时,还会在系统状态栏一直显示当前天气的信息,如下图所示:

来看一下如何才能创建一个前台Service吧,其实并不复杂,修改MyService中的代码,如下所示:

这里只是修改了MyService中onCreate()方法的代码。可以看到,我们创建了一个Notification对象,然后设置了它的布局和数据,并在这里设置了点击通知后就打开MainActivity。然后调用startForeground()方法就可以让MyService变成一个前台Service,并会将通知的图片显示出来。

现在重新运行一下程序,并点击Start Service或Bind Service按钮,MyService就会以前台Service的模式启动了,并且在系统状态栏会弹出一个通栏图标,下拉状态栏后可以看到通知的详细内容,如下图所示

最后我们看一下进程的分类:

    • 当前用户操作的Activity所在进程

从上面可知,Service其实是运行在主线程里的,如果直接在Service中处理一些耗时的逻辑,就会导致程序ANR。让我们来验证一下吧,修改MyService代码,在onCreate()方法中让线程睡眠60秒,如下所示:

点击一下Start Service按钮或Bind Service按钮,程序就会阻塞住无法进行任何其它操作,过一段时间后就会弹出ANR的提示框,如下图所示:

现在来看看远程Service的用法,如果将MyService转换成一个远程Service,还会不会有ANR的情况呢?让我们来动手尝试一下吧。将一个普通的Service转换成远程Service其实非常简单,只需要在注册Service的时候将它的android:process属性指定成:remote就可以了,代码如下所示:

重新运行程序,并点击一下Start Service按钮,你会看到控制台立刻打印了onCreate()的信息,而且主界面并没有阻塞住,也不会出现ANR。大概过了一分钟后,又会看到onStartCommand()打印了出来。
为什么将MyService转换成远程Service后就不会导致程序ANR了呢?这是由于,使用了远程Service后,MyService已经在另外一个进程当中运行了,所以只会阻塞该进程中的主线程,并不会影响到当前的应用程序。

那既然远程Service这么好用,干脆以后我们把所有的Service都转换成远程Service吧,还省得再开启线程了。其实不然,远程Service非但不好用,甚至可以称得上是较为难用。一般情况下如果可以不使用远程Service,就尽量不要使用它。
下面就来看一下它的弊端吧,首先将MyService的onCreate()方法中让线程睡眠的代码去除掉,然后重新运行程序,并点击一下Bind Service按钮,你会发现程序崩溃了!为什么点击Start Service按钮程序就不会崩溃,而点击Bind Service按钮就会崩溃呢?这是由于在Bind Service按钮的点击事件里面我们会让MainActivity和MyService建立关联,但是目前MyService已经是一个远程Service了,Activity和Service运行在两个不同的进程当中,这时就不能再使用传统的建立关联的方式,程序也就崩溃了。

那么如何才能让Activity与一个远程Service建立关联呢?这就要使用AIDL来进行跨进程通信了(IPC)。
下面我们就来一步步地看一下AIDL的用法到底是怎样的。首先需要新建一个AIDL文件,在这个文件中定义好Activity需要与Service进行通信的方法。新建MyAIDLService.aidl文件,代码如下所示:

点击保存之后,在gen目录下通过aapt就会生成一个对应的Java文件,如下图所示:

然后我们写一个MyRemoteService,在里面实现我们刚刚定义好的MyAIDLService接口,如下所示:

这里先是对MyAIDLService.Stub进行了实现,重写里了toUpperCase()和plus()这两个方法。这两个方法的作用分别是将一个字符串全部转换成大写格式,以及将两个传入的整数进行相加。然后在onBind()方法中将MyAIDLService.Stub的实现返回。这里为什么可以这样写呢?因为Stub其实就是Binder的子类,所以在onBind()方法中可以直接返回Stub的实现。

接下来修改MainActivity中的代码,如下所示:

world字符串作为参数,最后将调用方法的返回结果打印出来。
现在重新运行程序,并点击一下Bind Remote Service按钮,可以看到打印日志如下所示:

由此可见,我们确实已经成功实现跨进程通信了,在一个进程中访问到了另外一个进程中的方法。
不过你也可以看出,目前的跨进程通信其实并没有什么实质上的作用,因为这只是在一个Activity里调用了同一个应用程序的Service里的方法。而跨进程通信的真正意义是为了让一个应用程序去访问另一个应用程序中的Service,以实现共享Service的功能。那么下面我们自然要学习一下,如何才能在其它的应用程序中调用到MyRemoteService里的方法。

我要回帖

更多关于 阿里对象存储 的文章

 

随机推荐