如何让你的app在后台被干掉后优雅的启动

作为一名Android开发师,肯定在处理用户的体验上下一定的功夫。有这么一个场景,在用户用着你开发的app的时候,突然某个聊天工具来消息了,切换到聊天工具后长时间停留,并且可能做了一些你不知道的操作,比如看视频阿,刷刷消息圈什么的。一般这种情况下都很容易出现手机内存不足的情况,内存不足就会可能被干掉。这种时候用户切换到app准备继续操作时,如果开发师处理不好,就会引起崩溃的情况,肯定会出现返回的时候一瞬间的白屏,对于用户体验的来说,非常不好。

开始

首先要介绍下Android中activity的四种启动模式(就当作复习一下旧知识吧,资料来源于网络总结):

Standard:是默认的也是标准的Task模式,在没有其他因素的影响下,使用此模式的Activity,会构造一个Activity的实例,加入到调用者的Task栈中去,对于使用频度一般开销一般什么都一般的Activity而言,standard模式无疑是最合适的,因为它逻辑简单条理清晰,所以是默认的选择。
singleTop:基本上于standard一致,仅在请求的Activity正好位于栈顶时,有所区别。此时,配置成singleTop的Activity,不再会构造新的实例加入到Task栈中,而是将新来的Intent发送到栈顶Activity中,栈顶的Activity可以通过重载onNewIntent来处理新的Intent(当然,也可以无视…)。这个模式,降低了位于栈顶时的一些重复开销,更避免了一些奇异的行为(想象一下,如果在栈顶连续几个都是同样的Activity,再一级级退出的时候,这是怎么样的用户体验…),很适合一些会有更新的列表Activity展示。一个活生生的实例是,在Android默认提供的应用中,浏览器(Browser)的书签Activity(BrowserBookmarkPage),就用的是singleTop。
singleTask:配置了这个属性的activity,最多仅有一个实例存在,而且,它在根的task中,在之后的被杀死重启的过程中我们会利用到这个配置,也就是我们的主界面MainActivity。
singleInstance:跟上面的singleTask基本上是一样的,但是,singleInstance的Activity,是它所在栈中仅有的一个Activity,如果涉及到的其他Activity,都移交到其他Task中进行,在实际开发中这个是用得比较少的。
这个是activity的生命周期:

就不多介绍这个生命周期了,相信都熟悉不过了,有想了解的自行Google或者百度吧。

重点

接下来是我们的重点:程序如果在后台被杀死之后,我们怎么去处理?是立刻恢复还是重新启动?哪个方法更适合我们?

首先,我们得知道,为什么程序会在后台被干掉的?我们又没有手动关闭程序。

app在后台被强杀,是在内存不足的情况下被强制释放了,也有一些恶心的rom会强制杀掉那些后台进程以释放缓存以提高所谓的用户体验。(注:当你的代码写得混乱、冗余,而且非常消耗内存的时候,那你的app在后台运行时将会比较容易被系统给干掉的,所以从现在开始要约束自己要养成良好的编码习惯和注意内存泄漏的问题)

我们都觉得android rom很恶心,但同时还是用些更恶心的手法去绕开这些瓶颈。乱,是因为在最上层没有一个很好的约束,这也是开源的弊端。anyway。我们还是得想破脑袋来解决这些问题,否则饭碗就没了。

我们现在来重现这个熟悉的一幕:

假设:App A -> B -> C

在C activity中点Home键后台运行,打开ddms,选中该App进程,强杀。

然后从“最近打开的应用”中选中该App,回到的界面是C activity,假设App中没有静态变量,这个时候是不会crash的,点击返回到B,这个时候也只是短暂白屏后显示B界面。但如果B中有引用静态变量,并想要获取静态变量中的某个值时,就NullPointer了。

以上复现的流程就几个点,我们展开说下:

当应用被强杀,整个App进程都是被杀掉了,所有变量全都被清空了。包括Application实例。更别提那些静态变量了。

虽然变量被清空了,但Android给了一些补救措施。activity栈没有被清空,也就是说A -> B -> C这个栈还保存了,只是ABC这几个activity实例没有了。所以回到App时,显示的还是C页面。另外当activity被强杀时,系统会调用onSaveInstance去让你保存一些变量,但我个人觉得面对海量的静态变量,这个根本不够用。返回到B会白屏,是因为B要重绘,重走onCreate流程,渲染上需要点时间,所以会白屏了。

大概是以上这些点。如果App中没有静态变量的引用,那就不用出现NullPointer这个crash,也就不需要解决。一旦你有静态变量,或者有些Application的全局变量,那就很危险了。比如登录状态,user profile等等。这些值都是空了。

肯定会有人说,这没关系啊,所有的静态变量都改到单例去不就好了吗?然后附加上一些持久化cache,空了再取缓存就ok了嘛。嗯,这肯定也是一个办法,但是这样的束手束脚对开发来说也是痛苦,至少需要多50%的编码时间才能全部cover。另外,还有那么多帮你挖坑的队友,难省心啊。

既然App都被强杀了,干嘛不重新走第一次启动的流程呢,别让App回到D而是启动A,这样所有的变量都是按正常的流程去初始化,也就不会空指针了,对吧?有人说这方案用户体验一点都不好呀。但哪有十全十美的事呢,是重走流程好,还是一点一个NullPointer好?好好去沟通,相信产品也不会为难你的。当然你也可以拿来举例,iOS在最近打开的应用里杀了某个App,重新点击那个App,还是会重走流程的啊。

如果你说用户已经打开了C界面,所以重新打开的是是恢复到C界面,这样的用户体验会更好啊,如果你是这样认为的,那你很多时间都是在防止恢复的时候不让你的app crash了,与其这样,还不如让整个app重新走整个流程呢,这样更省时间,而且这样也不用担心随时都会崩溃的情况,难道这样的用户体验不会更好吗?

那且想想如何让它不回到C而是重走流程呢?也就是说中断C的初始化而回到A,并且按back键,不会回到C,B。考虑一下。

我们先实例化这个场景吧。
A为App的启动页
B为首页
C为二级页面

把首页launchMode设置为singleTask,具体为什么上面介绍activity的启动模式的时候已经介绍了singleTask的作用了。

在BaseActivity中onCreate中判断App是否被强杀,强杀就不往下走,直接重走App流程。

首页起一个承接或者中转的作用,所有跨级跳转都需要通过首页来完成。

再给个提示,以上场景的解决方案也可以用于解决其它相关问题:
在任意页面退出App
在任意页面返回到首页
其实最重要的知识点就是launchMode

具体实现

AppStatusConstant

1
2
3
4
5
public static final int STATUS_FORCE_KILLED = -1;//应用放在后台被强杀了
public static final int STATUS_NORMAL = 2; //APP正常态//intent到MainActivity区分跳转目的
public static final String KEY_HOME_ACTION = "key_home_action";//返回到主页面
public static final int ACTION_BACK_TO_HOME = 0;//默认值
public static final int ACTION_RESTART_APP = 1;//被强杀

AppStatusManager

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public int appStatus= AppStatusConstant.STATUS_FORCE_KILLED; //APP状态初始值为没启动不在前台状态
public static AppStatus ManagerappStatusManager;
privateAppStatusManager() {
}
public static AppStatus ManagergetInstance() {
if(appStatusManager==null) {
appStatusManager=newAppStatusManager();
}
return appStatusManager;
}
public int getAppStatus() {
return appStatus;
}
public void setAppStatus(int appStatus) {
this.appStatus= appStatus;
}

BaseActivity(大致内容)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
switch(AppStatusManager.getInstance().getAppStatus()) {
case AppStatusConstant.STATUS_FORCE_KILLED:
restartApp();
break;
case AppStatusConstant.STATUS_NORMAL:
setUpViewAndData();
break;
}
protected abstract void setUpViewAndData();
protected void restartApp() {
Intent intent =newIntent(this,MainActivity.class);
intent.putExtra(AppStatusConstant.KEY_HOME_ACTION,AppStatusConstant.ACTION_RESTART_APP);
startActivity(intent);
}

每一个继承于父activity的都不要在oncreat中实现界面初始化和数据的初始化,因为如果被杀死之后,回来会走一次正常的生命流程的。

StartPageActivity配置(在oncreate()方法配置,并且在super()前):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
AppStatusManager.getInstance().setAppStatus(AppStatusConstant.STATUS_NORMAL);//进入应用初始化设置成未登录状态
MainActivity(配置了singleTask的主界面)

@Override
protected void restartApp() {
Toast.makeText(getApplicationContext(),"应用被回收重启",Toast.LENGTH_LONG).show();
startActivity(newIntent(this,StartPageActivity.class));
finish();
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
intaction = intent.getIntExtra(AppStatusConstant.KEY_HOME_ACTION,AppStatusConstant.ACTION_BACK_TO_HOME);
switch(action) {
case AppStatusConstant.ACTION_RESTART_APP:
restartApp();
break;
}
}

当应用打开的时候,启动StartPageActivity,然后设置app的status为normal状态,记住,一定要设置,因为默认的是被杀死的状态的。

当应用被杀死之后,所有数据都会被回收,所以之前设置的app status也会置于默认状态,即杀死状态,所以再次打开app的时候,status为杀死状态,就会走重启的流程,这里为什么要先跳转到MainActivity呢?就是因为MainActivity配置为了Sing了Task,当跳转到这个界面时,MainActivity就会置于Activity Task的最上层,其他的Activity将会被默认销毁掉,利用这种技巧去销毁其他的Activity,最后才是重新启动StartPageActivity。整个流程就是这样了。

大致的实现就如上所述了,我所倡导的宗旨就是花最少的时间,写最好的代码,实现最好的体验!之前也参考过很多网上大神们的实现方式,但是我觉得以上实现的应该是比较完整的一种了。

SFTP传输,验证文件完整性

第一次接触与合作方的文件传输方面的需求,这里稍微整理一下整个流程纪要,以免遗忘~
如果有不恰当的地方,还请指教~

传输方法

  1. 生成N个业务文件,并对每个文件的内容采用AES进行加密;
  2. 生成OkFile.txt文件,记录N个文件对应的行数以及大小,AES加密;(仅用于对每个文件进行粗略的校验)
  3. 将N个业务文件和OKFile文件进行压缩打包为tar.gz文件;
  4. 对tar.gz文件求md5码,生成md5文件;(防止第三方篡改某个文件的内容,供合作方验证文件完整性)
  5. 将tar.gz文件和md5文件传送给合作方;(合作方在收到文件后,会采用相同算法对tar.gz文件求md5码,并比对md5值是否相同,如果相同,说明文件完整性没问题)
  6. 如何传输?== 复用前人的方法,将需要传输的文件放在SFTP服务器上,对方定时拉取SFTP服务器上的文件;(注意账号的读写权限)
    这里为什么采用了sftp而不是用https传输呢?下面对这两部分协议进行调研,但是感觉还是没有get到心中的点上,后续有时间继续研究~

https还是sftp?

对于文件的传输,我们更加关心的是传输文件的大小以及安全性问题;对于性能方面的考虑,可能没有那么重要。

安全性上来说,https和sftp协议都是安全的传输协议,https = http + ssl,sftp是基于ssh的安全文件传输协议;

性能上来说,https协议的传输速度可能比sftp速度更快;

参考HTTPS or SFTP – which is best?这篇博文,最后有说到,对于一些普通的用户,如果仅仅是要下载文件的功能,那么https是更好的选择(这里是从用户体验考虑);然而,如果涉及传输的文件比较复杂,比较大,那么请采用sftp协议。

SFTP安全性?

sftp的安全性是由ssh协议进行保证,因此只需要弄懂ssh协议的安全性原理就ok了。

ssh协议目的是实现安全的远程登录或其他网络安全服务,根源还是采用了非对称加密和对称加密的算法实现,但是却无法解决中间人攻击的问题。https是通过ca证书解决,那么ssh是如何解决的呢?

基于口令的认证

由于ssh的publish key和private key都是自己生成的,没办法进行公证,只能通过client自己对公钥进行确认。因此,在第一次登录某台server时,系统中会出现如下提示信息:

这里采用主机密钥指纹(MD5)而不是直接采用主机密钥,是因为主机密钥的key过长(采用rsa算法生成的公钥有024位),不易比较,因此对公钥进行hash生成一个128位的指纹。

点击 接受并保存 后,就表示该server已经被确认,并且会追加到client的known_hosts文件中。后续会用该公钥对密钥进行加密传输给server,此时server就能够安全的拿到对称加密的密钥了,后面对数据的加密也是采用对称加密的密钥进行。

基于公钥的认证

公钥不同于上面的口令认证,不需要输入密码,而是需要用户手动将client的公钥添加到server端的authorized_key中。这样用户发送请求给server端时,server首先生成随机数R,并用client的公钥对其加密,得到pubKey®,返回给客户端;客户端拿到数据后,用私钥解密,得到R,并用MD5对R和sessionKey生成摘要信息,将摘要信息传输给server,server同样用MD5对R和sessionKey生成摘要,对比两个摘要信息,完整认证过程。

由上面的两种方法认证过程可知,ssh协议还是非常安全的,也能在一定程度上防止中间人攻击。为什么说是一定程度上呢?因为ssh协议的安全认证过程的前提是第一次连接认证的时候,不会被中间人攻击,那么client就会把指纹存放在本地的known_hosts文件中,以后的通讯就会通过known_hosts文件中的内容进行验证。但是,如何中间人攻击出现在第一次连接认证的时候,那么安全性就无法得到保证了。

Linux中,终端连接ftp服务乱码

问题:
服务器是centos,用Mac的iterm2 ssh连上去,终端显示中文乱码,也不能输入中文,然而本地终端可以显示和输入。

解决方法:
这种情况一般是终端和服务器的字符集不匹配,MacOSX下默认的是utf8字符集。
输入locale可以查看字符编码设置情况,而我的对应值是空的。
因为我在本地和服务器都用zsh替代了bash,而且使用了oh-my-zsh,而默认的.zshrc没有设置为utf-8编码,所以本地和服务器端都要在.zshrc设置,步骤如下,bash对应.bash_profile或.bashrc文件。

1.在终端下输入

1
vim ~/.zshrc

或者使用其他你喜欢的编辑器编辑~/.zshrc文件

2.在文件内容末端添加:

1
2
export LC_ALL=en_US.UTF-8  
export LANG=en_US.UTF-8

接着重启一下终端,或者输入source ~/.zshrc使设置生效。

设置成功的话,在本地和登录到服务器输入locale回车会显示下面内容。

1
2
3
4
5
6
7
8
LANG="en_US.UTF-8"
LC_COLLATE="en_US.UTF-8"
LC_CTYPE="en_US.UTF-8"
LC_MESSAGES="en_US.UTF-8"
LC_MONETARY="en_US.UTF-8"
LC_NUMERIC="en_US.UTF-8"
LC_TIME="en_US.UTF-8"
LC_ALL="en_US.UTF-8"

这时,中文输入和显示都正常了。

Android四种启动模式的生命周期

Android四种启动模式的进入时的生命周期:

  1. standard:
    第一次进入:onCreate => onStart
    在栈顶再次进入: onCreate => onStart
    不在栈顶再次进入:onCreate => onStart
    从其他Activity返回:onRestart => onStart
    切换应该后再返回:onRestart => onStart

  2. singleTop:
    第一次进入:onCreate => onStart
    在栈顶再次进入:onNewIntent
    不在栈顶再次进入:onCreate => onStart
    从其他Activity返回:onRestart => onStart
    切换应该后再返回:onRestart => onStart

  3. singleTask:
    第一次进入:onCreate => onStart
    在栈顶再次进入:onNewIntent
    不在栈顶再次进入:onNewIntent => onRestart => onStart
    从其他Activity返回:onRestart => onStart
    切换应该后再返回:onRestart => onStart

  4. singleInstance:
    第一次进入:onCreate => onStart
    在栈顶再次进入: onNewIntent
    不在栈顶再次进入:onNewIntent => onRestart => onStart
    从其他Activity返回:onRestart => onStart
    切换应该后再返回:onRestart => onStart

Activity四种启动模式的onNewIntent调用时机:

  1. standard
    默认启动模式,每次激活Activity时都会创建Activity,并放入任务栈中,永远不会调用onNewIntent()。

  2. singleTop
    如果在任务的栈顶正好存在该Activity的实例, 就重用该实例,并调用其onNewIntent(),否者就会创建新的实例并放入栈顶(即使栈中已经存在该Activity实例,只要不在栈顶,都会创建实例,而不会调用onNewIntent(),此时就跟standard模式一样)。

  3. singleTask
    如果在任务栈中已经有该Activity的实例,就重用该实例(会调用实例的onNewIntent())。重用时,会让该实例回到栈顶,因此在它上面的实例将会被从栈中移除。如果栈中不存在该实例,将会创建新的实例放入栈中(此时不会调用onNewIntent())。

  4. singleInstance
    在一个新栈中创建该Activity实例,并让多个应用共享该栈中的该Activity实例。一旦该模式的Activity的实例存在于某个栈中,任何应用再激活该Activity时都会重用该栈中的实例,其效果相当于多个应用程序共享一个应用,不管谁激活该Activity都会进入同一个应用中。

注意事项:
当调用到onNewIntent(intent)的时候,需要在onNewIntent() 中使用setIntent(intent)修改getIntent()的返回值。否则,后续的getIntent()都是得到老的Intent。

Android中service介绍

Service全部内容基本会在本篇涉及到,我们将围绕以下主要知识点进行分析:

  • Service简单概述
  • Service在清单文件中的声明
  • Service启动服务实现方式及其详解
  • Service绑定服务的三种实现方式
  • 关于启动服务与绑定服务间的转换问题
  • 前台服务以及通知发送
  • 服务Service与线程Thread的区别
  • 管理服务生命周期的要点
  • Android 5.0以上的隐式启动问题及其解决方案
  • 保证服务不被杀死的实现思路

Service简单概述

  Service(服务)是一个一种可以在后台执行长时间运行操作而没有用户界面的应用组件。服务可由其他应用组件启动(如Activity),服务一旦被启动将在后台一直运行,即使启动服务的组件(Activity)已销毁也不受影响。 此外,组件可以绑定到服务,以与之进行交互,甚至是执行进程间通信 (IPC)。 例如,服务可以处理网络事务、播放音乐,执行文件 I/O 或与内容提供程序交互,而所有这一切均可在后台进行,Service基本上分为两种形式:

启动状态
  当应用组件(如 Activity)通过调用 startService() 启动服务时,服务即处于“启动”状态。一旦启动,服务即可在后台无限期运行,即使启动服务的组件已被销毁也不受影响,除非手动调用才能停止服务, 已启动的服务通常是执行单一操作,而且不会将结果返回给调用方。

绑定状态
  当应用组件通过调用 bindService() 绑定到服务时,服务即处于“绑定”状态。绑定服务提供了一个客户端-服务器接口,允许组件与服务进行交互、发送请求、获取结果,甚至是利用进程间通信 (IPC) 跨进程执行这些操作。 仅当与另一个应用组件绑定时,绑定服务才会运行。 多个组件可以同时绑定到该服务,但全部取消绑定后,该服务即会被销毁。

Service在清单文件中的声明

  前面说过Service分为启动状态和绑定状态两种,但无论哪种具体的Service启动类型,都是通过继承Service基类自定义而来,也都需要在AndroidManifest.xml中声明,那么在分析这两种状态之前,我们先来了解一下Service在AndroidManifest.xml中的声明语法,其格式如下:

1
2
3
4
5
6
7
8
9
10
<service android:enabled=["true" | "false"]
android:exported=["true" | "false"]
android:icon="drawable resource"
android:isolatedProcess=["true" | "false"]
android:label="string resource"
android:name="string"
android:permission="string"
android:process="string" >
. . .
</service>

android:exported:代表是否能被其他应用隐式调用,其默认值是由service中有无intent-filter决定的,如果有intent-filter,默认值为true,否则为false。为false的情况下,即使有intent-filter匹配,也无法打开,即无法被其他应用隐式调用。

android:name:对应Service类名

android:permission:是权限声明

android:process:是否需要在单独的进程中运行,当设置为android:process=”:remote”时,代表Service在单独的进程中运行。注意“:”很重要,它的意思是指要在当前进程名称前面附加上当前的包名,所以“remote”和”:remote”不是同一个意思,前者的进程名称为:remote,而后者的进程名称为:App-packageName:remote。

android:isolatedProcess :设置 true 意味着,服务会在一个特殊的进程下运行,这个进程与系统其他进程分开且没有自己的权限。与其通信的唯一途径是通过服务的API(bind and start)。

android:enabled:是否可以被系统实例化,默认为 true因为父标签 也有 enable 属性,所以必须两个都为默认值 true 的情况下服务才会被激活,否则不会激活。
  ok~,关于Service在清单文件的声明我们先了解这些就行,接下来分别针对Service启动服务和绑定服务进行详细分析

3.Service启动服务
  首先要创建服务,必须创建 Service 的子类(或使用它的一个现有子类如IntentService)。在实现中,我们需要重写一些回调方法,以处理服务生命周期的某些关键过程,下面我们通过简单案例来分析需要重写的回调方法有哪些?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package com.zejian.ipctest.service;

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.support.annotation.Nullable;

/**
* Created by zejian
* Time 2016/9/29.
* Description:service simple demo
*/
public class SimpleService extends Service {

/**
* 绑定服务时才会调用
* 必须要实现的方法
* @param intent
* @return
*/
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}

/**
* 首次创建服务时,系统将调用此方法来执行一次性设置程序(在调用 onStartCommand() 或 onBind() 之前)。
* 如果服务已在运行,则不会调用此方法。该方法只被调用一次
*/
@Override
public void onCreate() {
System.out.println("onCreate invoke");
super.onCreate();
}

/**
* 每次通过startService()方法启动Service时都会被回调。
* @param intent
* @param flags
* @param startId
* @return
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
System.out.println("onStartCommand invoke");
return super.onStartCommand(intent, flags, startId);
}

/**
* 服务销毁时的回调
*/
@Override
public void onDestroy() {
System.out.println("onDestroy invoke");
super.onDestroy();
}
}

  从上面的代码我们可以看出SimpleService继承了Service类,并重写了onBind方法,该方法是必须重写的,但是由于此时是启动状态的服务,则该方法无须实现,返回null即可,只有在绑定状态的情况下才需要实现该方法并返回一个IBinder的实现类(这个后面会详细说),接着重写了onCreate、onStartCommand、onDestroy三个主要的生命周期方法,关于这几个方法说明如下:

onBind()
  当另一个组件想通过调用 bindService() 与服务绑定(例如执行 RPC)时,系统将调用此方法。在此方法的实现中,必须返回 一个IBinder 接口的实现类,供客户端用来与服务进行通信。无论是启动状态还是绑定状态,此方法必须重写,但在启动状态的情况下直接返回 null。

onCreate()
  首次创建服务时,系统将调用此方法来执行一次性设置程序(在调用 onStartCommand() 或onBind() 之前)。如果服务已在运行,则不会调用此方法,该方法只调用一次

onStartCommand()
  当另一个组件(如 Activity)通过调用 startService() 请求启动服务时,系统将调用此方法。一旦执行此方法,服务即会启动并可在后台无限期运行。 如果自己实现此方法,则需要在服务工作完成后,通过调用 stopSelf() 或 stopService() 来停止服务。(在绑定状态下,无需实现此方法。)

onDestroy()
  当服务不再使用且将被销毁时,系统将调用此方法。服务应该实现此方法来清理所有资源,如线程、注册的侦听器、接收器等,这是服务接收的最后一个调用。

  我们通过Demo测试一下Service启动状态方法的调用顺序,MainActivity代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.zejian.ipctest;

import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Button;

import com.zejian.ipctest.service.SimpleService;

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

private Button startBtn;
private Button stopBtn;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
startBtn= (Button) findViewById(R.id.startService);
stopBtn= (Button) findViewById(R.id.stopService);
startBtn.setOnClickListener(this);
assert stopBtn != null;
stopBtn.setOnClickListener(this);
}

@Override
public void onClick(View v) {
Intent it=new Intent(this, SimpleService.class);
switch (v.getId()){
case R.id.startService:
startService(it);
break;
case R.id.stopService:
stopService(it);
break;
}
}
}

记得在清单配置文件中声明Service(声明方式跟Activity相似):

1
2
3
4
5
6
7
<manifest ... >
...
<application ... >
<service android:name=".service.SimpleService" />
...
</application>
</manifest>

  从代码看出,启动服务使用startService(Intent intent)方法,仅需要传递一个Intent对象即可,在Intent对象中指定需要启动的服务。而使用startService()方法启动的服务,在服务的外部,必须使用stopService()方法停止,在服务的内部可以调用stopSelf()方法停止当前服务。如果使用startService()或者stopSelf()方法请求停止服务,系统会就会尽快销毁这个服务。值得注意的是对于启动服务,一旦启动将与访问它的组件无任何关联,即使访问它的组件被销毁了,这个服务也一直运行下去,直到手动调用停止服务才被销毁,至于onBind方法,只有在绑定服务时才会起作用,在启动状态下,无需关注此方法,ok~,我们运行程序并多次调用startService方法,最后调用stopService方法。Log截图如下:

  从Log可以看出,第一次调用startService方法时,onCreate方法、onStartCommand方法将依次被调用,而多次调用startService时,只有onStartCommand方法被调用,最后我们调用stopService方法停止服务时onDestory方法被回调,这就是启动状态下Service的执行周期。接着我们重新回过头来进一步分析onStartCommand(Intent intent, int flags, int startId),这个方法有3个传入参数,它们的含义如下:

1
onStartCommand(Intent intent, int flags, int startId)

intent :启动时,启动组件传递过来的Intent,如Activity可利用Intent封装所需要的参数并传递给Service

flags:表示启动请求时是否有额外数据,可选值有 0,START_FLAG_REDELIVERY,START_FLAG_RETRY,0代表没有,它们具体含义如下:

START_FLAG_REDELIVERY
这个值代表了onStartCommand方法的返回值为
START_REDELIVER_INTENT,而且在上一次服务被杀死前会去调用stopSelf方法停止服务。其中START_REDELIVER_INTENT意味着当Service因内存不足而被系统kill后,则会重建服务,并通过传递给服务的最后一个 Intent 调用 onStartCommand(),此时Intent时有值的。

START_FLAG_RETRY
该flag代表当onStartCommand调用后一直没有返回值时,会尝试重新去调用onStartCommand()。

startId : 指明当前服务的唯一ID,与stopSelfResult (int startId)配合使用,stopSelfResult 可以更安全地根据ID停止服务。

  实际上onStartCommand的返回值int类型才是最最值得注意的,它有三种可选值, START_STICKY,START_NOT_STICKY,START_REDELIVER_INTENT,它们具体含义如下:

START_STICKY
  当Service因内存不足而被系统kill后,一段时间后内存再次空闲时,系统将会尝试重新创建此Service,一旦创建成功后将回调onStartCommand方法,但其中的Intent将是null,除非有挂起的Intent,如pendingintent,这个状态下比较适用于不执行命令、但无限期运行并等待作业的媒体播放器或类似服务。

START_NOT_STICKY
  当Service因内存不足而被系统kill后,即使系统内存再次空闲时,系统也不会尝试重新创建此Service。除非程序中再次调用startService启动此Service,这是最安全的选项,可以避免在不必要时以及应用能够轻松重启所有未完成的作业时运行服务。

START_REDELIVER_INTENT
  当Service因内存不足而被系统kill后,则会重建服务,并通过传递给服务的最后一个 Intent 调用 onStartCommand(),任何挂起 Intent均依次传递。与START_STICKY不同的是,其中的传递的Intent将是非空,是最后一次调用startService中的intent。这个值适用于主动执行应该立即恢复的作业(例如下载文件)的服务。

  由于每次启动服务(调用startService)时,onStartCommand方法都会被调用,因此我们可以通过该方法使用Intent给Service传递所需要的参数,然后在onStartCommand方法中处理的事件,最后根据需求选择不同的Flag返回值,以达到对程序更友好的控制。好~,以上便是Service在启动状态下的分析,接着我们在来看看绑定状态的Service又是如何处理的?

Service绑定服务

  绑定服务是Service的另一种变形,当Service处于绑定状态时,其代表着客户端-服务器接口中的服务器。当其他组件(如 Activity)绑定到服务时(有时我们可能需要从Activity组建中去调用Service中的方法,此时Activity以绑定的方式挂靠到Service后,我们就可以轻松地方法到Service中的指定方法),组件(如Activity)可以向Service(也就是服务端)发送请求,或者调用Service(服务端)的方法,此时被绑定的Service(服务端)会接收信息并响应,甚至可以通过绑定服务进行执行进程间通信 (即IPC,这个后面再单独分析)。与启动服务不同的是绑定服务的生命周期通常只在为其他应用组件(如Activity)服务时处于活动状态,不会无限期在后台运行,也就是说宿主(如Activity)解除绑定后,绑定服务就会被销毁。那么在提供绑定的服务时,该如何实现呢?实际上我们必须提供一个 IBinder接口的实现类,该类用以提供客户端用来与服务进行交互的编程接口,该接口可以通过三种方法定义接口:

扩展 Binder 类

  如果服务是提供给自有应用专用的,并且Service(服务端)与客户端相同的进程中运行(常见情况),则应通过扩展 Binder 类并从 onBind() 返回它的一个实例来创建接口。客户端收到 Binder 后,可利用它直接访问 Binder 实现中以及Service 中可用的公共方法。如果我们的服务只是自有应用的后台工作线程,则优先采用这种方法。 不采用该方式创建接口的唯一原因是,服务被其他应用或不同的进程调用。

使用 Messenger

  Messenger可以翻译为信使,通过它可以在不同的进程中共传递Message对象(Handler中的Messager,因此 Handler 是 Messenger 的基础),在Message中可以存放我们需要传递的数据,然后在进程间传递。如果需要让接口跨不同的进程工作,则可使用 Messenger 为服务创建接口,客户端就可利用 Message 对象向服务发送命令。同时客户端也可定义自有 Messenger,以便服务回传消息。这是执行进程间通信 (IPC) 的最简单方法,因为 Messenger 会在单一线程中创建包含所有请求的队列,也就是说Messenger是以串行的方式处理客户端发来的消息,这样我们就不必对服务进行线程安全设计了。

使用 AIDL

   由于Messenger是以串行的方式处理客户端发来的消息,如果当前有大量消息同时发送到Service(服务端),Service仍然只能一个个处理,这也就是Messenger跨进程通信的缺点了,因此如果有大量并发请求,Messenger就会显得力不从心了,这时AIDL(Android 接口定义语言)就派上用场了, 但实际上Messenger 的跨进程方式其底层实现 就是AIDL,只不过android系统帮我们封装成透明的Messenger罢了 。因此,如果我们想让服务同时处理多个请求,则应该使用 AIDL。 在此情况下,服务必须具备多线程处理能力,并采用线程安全式设计。使用AIDL必须创建一个定义编程接口的 .aidl 文件。Android SDK 工具利用该文件生成一个实现接口并处理 IPC 的抽象类,随后可在服务内对其进行扩展。
  以上3种实现方式,我们可以根据需求自由的选择,但需要注意的是大多数应用“都不会”使用 AIDL 来创建绑定服务,因为它可能要求具备多线程处理能力,并可能导致实现的复杂性增加。因此,AIDL 并不适合大多数应用,本篇中也不打算阐述如何使用AIDL(后面会另开一篇分析AIDL),接下来我们分别针对扩展 Binder 类和Messenger的使用进行分析。

扩展 Binder 类

  前面描述过,如果我们的服务仅供本地应用使用,不需要跨进程工作,则可以实现自有 Binder 类,让客户端通过该类直接访问服务中的公共方法。其使用开发步骤如下

1.创建BindService服务端,继承自Service并在类中,创建一个实现IBinder 接口的实例对象并提供公共方法给客户端调用
2.从 onBind() 回调方法返回此 Binder 实例。
3.在客户端中,从 onServiceConnected() 回调方法接收 Binder,并使用提供的方法调用绑定服务。
  注意:此方式只有在客户端和服务位于同一应用和进程内才有效,如对于需要将 Activity 绑定到在后台播放音乐的自有服务的音乐应用,此方式非常有效。另一点之所以要求服务和客户端必须在同一应用内,是为了便于客户端转换返回的对象和正确调用其 API。服务和客户端还必须在同一进程内,因为此方式不执行任何跨进程编组。
  以下是一个扩展 Binder 类的实例,先看看Service端的实现BindService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
package com.zejian.ipctest.service;

import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.support.annotation.Nullable;
import android.util.Log;

/**
* Created by zejian
* Time 2016/10/2.
* Description:绑定服务简单实例--服务端
*/
public class LocalService extends Service{
private final static String TAG = "wzj";
private int count;
private boolean quit;
private Thread thread;
private LocalBinder binder = new LocalBinder();

/**
* 创建Binder对象,返回给客户端即Activity使用,提供数据交换的接口
*/
public class LocalBinder extends Binder {
// 声明一个方法,getService。(提供给客户端调用)
LocalService getService() {
// 返回当前对象LocalService,这样我们就可在客户端端调用Service的公共方法了
return LocalService.this;
}
}

/**
* 把Binder类返回给客户端
*/
@Nullable
@Override
public IBinder onBind(Intent intent) {
return binder;
}


@Override
public void onCreate() {
super.onCreate();
Log.i(TAG, "Service is invoke Created");
thread = new Thread(new Runnable() {
@Override
public void run() {
// 每间隔一秒count加1 ,直到quit为true。
while (!quit) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
}
});
thread.start();
}

/**
* 公共方法
* @return
*/
public int getCount(){
return count;
}
/**
* 解除绑定时调用
* @return
*/
@Override
public boolean onUnbind(Intent intent) {
Log.i(TAG, "Service is invoke onUnbind");
return super.onUnbind(intent);
}

@Override
public void onDestroy() {
Log.i(TAG, "Service is invoke Destroyed");
this.quit = true;
super.onDestroy();
}
}

  BindService类继承自Service,在该类中创建了一个LocalBinder继承自Binder类,LocalBinder中声明了一个getService方法,客户端可访问该方法获取LocalService对象的实例,只要客户端获取到LocalService对象的实例就可调用LocalService服务端的公共方法,如getCount方法,值得注意的是,我们在onBind方法中返回了binder对象,该对象便是LocalBinder的具体实例,而binder对象最终会返回给客户端,客户端通过返回的binder对象便可以与服务端实现交互。接着看看客户端BindActivity的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
package com.zejian.ipctest.service;

import android.app.Activity;
import android.app.Service;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
import android.view.View;
import android.widget.Button;

import com.zejian.ipctest.R;

/**
* Created by zejian
* Time 2016/10/2.
* Description:绑定服务实例--客户端
*/
public class BindActivity extends Activity {
protected static final String TAG = "wzj";
Button btnBind;
Button btnUnBind;
Button btnGetDatas;
/**
* ServiceConnection代表与服务的连接,它只有两个方法,
* onServiceConnected和onServiceDisconnected,
* 前者是在操作者在连接一个服务成功时被调用,而后者是在服务崩溃或被杀死导致的连接中断时被调用
*/
private ServiceConnection conn;
private LocalService mService;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_bind);
btnBind = (Button) findViewById(R.id.BindService);
btnUnBind = (Button) findViewById(R.id.unBindService);
btnGetDatas = (Button) findViewById(R.id.getServiceDatas);
//创建绑定对象
final Intent intent = new Intent(this, LocalService.class);

// 开启绑定
btnBind.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d(TAG, "绑定调用:bindService");
//调用绑定方法
bindService(intent, conn, Service.BIND_AUTO_CREATE);
}
});
// 解除绑定
btnUnBind.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d(TAG, "解除绑定调用:unbindService");
// 解除绑定
if(mService!=null) {
mService = null;
unbindService(conn);
}
}
});

// 获取数据
btnGetDatas.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mService != null) {
// 通过绑定服务传递的Binder对象,获取Service暴露出来的数据

Log.d(TAG, "从服务端获取数据:" + mService.getCount());
} else {

Log.d(TAG, "还没绑定呢,先绑定,无法从服务端获取数据");
}
}
});


conn = new ServiceConnection() {
/**
* 与服务器端交互的接口方法 绑定服务的时候被回调,在这个方法获取绑定Service传递过来的IBinder对象,
* 通过这个IBinder对象,实现宿主和Service的交互。
*/
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.d(TAG, "绑定成功调用:onServiceConnected");
// 获取Binder
LocalService.LocalBinder binder = (LocalService.LocalBinder) service;
mService = binder.getService();
}
/**
* 当取消绑定的时候被回调。但正常情况下是不被调用的,它的调用时机是当Service服务被意外销毁时,
* 例如内存的资源不足时这个方法才被自动调用。
*/
@Override
public void onServiceDisconnected(ComponentName name) {
mService=null;
}
};
}
}

  在客户端中我们创建了一个ServiceConnection对象,该代表与服务的连接,它只有两个方法, onServiceConnected和onServiceDisconnected,其含义如下:

onServiceConnected(ComponentName name, IBinder service)
系统会调用该方法以传递服务的 onBind() 方法返回的 IBinder。其中service便是服务端返回的IBinder实现类对象,通过该对象我们便可以调用获取LocalService实例对象,进而调用服务端的公共方法。而ComponentName是一个封装了组件(Activity, Service, BroadcastReceiver, or ContentProvider)信息的类,如包名,组件描述等信息,较少使用该参数。

onServiceDisconnected(ComponentName name)
Android 系统会在与服务的连接意外中断时(例如当服务崩溃或被终止时)调用该方法。注意:当客户端取消绑定时,系统“绝对不会”调用该方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
conn = new ServiceConnection() {

@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.d(TAG, "绑定成功调用:onServiceConnected");
// 获取Binder
LocalService.LocalBinder binder = (LocalService.LocalBinder) service;
mService = binder.getService();
}

@Override
public void onServiceDisconnected(ComponentName name) {
mService=null;
}
};
}

  在onServiceConnected()被回调前,我们还需先把当前Activity绑定到服务LocalService上,绑定服务是通过通过bindService()方法,解绑服务则使用unbindService()方法,这两个方法解析如下:

bindService(Intent service, ServiceConnection conn, int flags)
该方法执行绑定服务操作,其中Intent是我们要绑定的服务(也就是LocalService)的意图,而ServiceConnection代表与服务的连接,它只有两个方法,前面已分析过,flags则是指定绑定时是否自动创建Service。0代表不自动创建、BIND_AUTO_CREATE则代表自动创建。

unbindService(ServiceConnection conn)
该方法执行解除绑定的操作,其中ServiceConnection代表与服务的连接,它只有两个方法,前面已分析过。

Activity通过bindService()绑定到LocalService后,ServiceConnection#onServiceConnected()便会被回调并可以获取到LocalService实例对象mService,之后我们就可以调用LocalService服务端的公共方法了,最后还需要在清单文件中声明该Service。而客户端布局文件实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="match_parent">

<Button
android:id="@+id/BindService"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="绑定服务器"
/>

<Button
android:id="@+id/unBindService"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="解除绑定"
/>

<Button
android:id="@+id/getServiceDatas"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="获取服务方数据"
/>
</LinearLayout>

  我们运行程序,点击绑定服务并多次点击绑定服务接着多次调用LocalService中的getCount()获取数据,最后调用解除绑定的方法移除服务,其结果如下:

  通过Log可知,当我们第一次点击绑定服务时,LocalService服务端的onCreate()、onBind方法会依次被调用,此时客户端的ServiceConnection#onServiceConnected()被调用并返回LocalBinder对象,接着调用LocalBinder#getService方法返回LocalService实例对象,此时客户端便持有了LocalService的实例对象,也就可以任意调用LocalService类中的声明公共方法了。更值得注意的是,我们多次调用bindService方法绑定LocalService服务端,而LocalService得onBind方法只调用了一次,那就是在第一次调用bindService时才会回调onBind方法。接着我们点击获取服务端的数据,从Log中看出我们点击了3次通过getCount()获取了服务端的3个不同数据,最后点击解除绑定,此时LocalService的onUnBind、onDestroy方法依次被回调,并且多次绑定只需一次解绑即可。此情景也就说明了绑定状态下的Service生命周期方法的调用依次为onCreate()、onBind、onUnBind、onDestroy。ok~,以上便是同一应用同一进程中客户端与服务端的绑定回调方式。

使用Messenger

  前面了解了如何使用IBinder应用内同一进程的通信后,我们接着来了解服务与远程进程(即不同进程间)通信,而不同进程间的通信,最简单的方式就是使用 Messenger 服务提供通信接口,利用此方式,我们无需使用 AIDL 便可执行进程间通信 (IPC)。以下是 Messenger 使用的主要步骤:

1.服务实现一个 Handler,由其接收来自客户端的每个调用的回调

2.Handler 用于创建 Messenger 对象(对 Handler 的引用)

3.Messenger 创建一个 IBinder,服务通过 onBind() 使其返回客户端

4.客户端使用 IBinder 将 Messenger(引用服务的 Handler)实例化,然后使用Messenger将 Message 对象发送给服务

5.服务在其 Handler 中(在 handleMessage() 方法中)接收每个 Message
以下是一个使用 Messenger 接口的简单服务示例,服务端进程实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package com.zejian.ipctest.messenger;

import android.app.Service;
import android.content.Intent;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.util.Log;

/**
* Created by zejian
* Time 2016/10/3.
* Description:Messenger服务端简单实例,服务端进程
*/
public class MessengerService extends Service {

/** Command to the service to display a message */
static final int MSG_SAY_HELLO = 1;
private static final String TAG ="wzj" ;

/**
* 用于接收从客户端传递过来的数据
*/
class IncomingHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_SAY_HELLO:
Log.i(TAG, "thanks,Service had receiver message from client!");
break;
default:
super.handleMessage(msg);
}
}
}

/**
* 创建Messenger并传入Handler实例对象
*/
final Messenger mMessenger = new Messenger(new IncomingHandler());

/**
* 当绑定Service时,该方法被调用,将通过mMessenger返回一个实现
* IBinder接口的实例对象
*/
@Override
public IBinder onBind(Intent intent) {
Log.i(TAG, "Service is invoke onBind");
return mMessenger.getBinder();
}
}

  首先我们同样需要创建一个服务类MessengerService继承自Service,同时创建一个继承自Handler的IncomingHandler对象来接收客户端进程发送过来的消息并通过其handleMessage(Message msg)进行消息处理。接着通过IncomingHandler对象创建一个Messenger对象,该对象是与客户端交互的特殊对象,然后在Service的onBind中返回这个Messenger对象的底层Binder即可。下面看看客户端进程的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
package com.zejian.ipctest.messenger;

import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.util.Log;
import android.view.View;
import android.widget.Button;

import com.zejian.ipctest.R;

/**
* Created by zejian
* Time 2016/10/3.
* Description: 与服务器交互的客户端
*/
public class ActivityMessenger extends Activity {
/**
* 与服务端交互的Messenger
*/
Messenger mService = null;

/** Flag indicating whether we have called bind on the service. */
boolean mBound;

/**
* 实现与服务端链接的对象
*/
private ServiceConnection mConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {
/**
* 通过服务端传递的IBinder对象,创建相应的Messenger
* 通过该Messenger对象与服务端进行交互
*/
mService = new Messenger(service);
mBound = true;
}

public void onServiceDisconnected(ComponentName className) {
// This is called when the connection with the service has been
// unexpectedly disconnected -- that is, its process crashed.
mService = null;
mBound = false;
}
};

public void sayHello(View v) {
if (!mBound) return;
// 创建与服务交互的消息实体Message
Message msg = Message.obtain(null, MessengerService.MSG_SAY_HELLO, 0, 0);
try {
//发送消息
mService.send(msg);
} catch (RemoteException e) {
e.printStackTrace();
}
}

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_messenager);
Button bindService= (Button) findViewById(R.id.bindService);
Button unbindService= (Button) findViewById(R.id.unbindService);
Button sendMsg= (Button) findViewById(R.id.sendMsgToService);

bindService.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d("zj","onClick-->bindService");
//当前Activity绑定服务端
bindService(new Intent(ActivityMessenger.this, MessengerService.class), mConnection,
Context.BIND_AUTO_CREATE);
}
});

//发送消息给服务端
sendMsg.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
sayHello(v);
}
});


unbindService.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// Unbind from the service
if (mBound) {
Log.d("zj","onClick-->unbindService");
unbindService(mConnection);
mBound = false;
}
}
});
}

}

  在客户端进程中,我们需要创建一个ServiceConnection对象,该对象代表与服务端的链接,当调用bindService方法将当前Activity绑定到MessengerService时,onServiceConnected方法被调用,利用服务端传递给来的底层Binder对象构造出与服务端交互的Messenger对象,接着创建与服务交互的消息实体Message,将要发生的信息封装在Message中并通过Messenger实例对象发送给服务端。关于ServiceConnection、bindService方法、unbindService方法,前面已分析过,这里就不重复了,最后我们需要在清单文件声明Service和Activity,由于要测试不同进程的交互,则需要将Service放在单独的进程中,因此Service声明如下:

1
2
3
<service android:name=".messenger.MessengerService"
android:process=":remote"
/>

其中android:process=”:remote”代表该Service在单独的进程中创建,最后我们运行程序,结果如下:

  接着多次点击绑定服务,然后发送信息给服务端,最后解除绑定,Log打印如下:

  通过上述例子可知Service服务端确实收到了客户端发送的信息,而且在Messenger中进行数据传递必须将数据封装到Message中,因为Message和Messenger都实现了Parcelable接口,可以轻松跨进程传递数据(关于Parcelable接口可以看博主的另一篇文章:序列化与反序列化之Parcelable和Serializable浅析),而Message可以传递的信息载体有,what,arg1,arg2,Bundle以及replyTo,至于object字段,对于同一进程中的数据传递确实很实用,但对于进程间的通信,则显得相当尴尬,在android2.2前,object不支持跨进程传输,但即便是android2.2之后也只能传递android系统提供的实现了Parcelable接口的对象,也就是说我们通过自定义实现Parcelable接口的对象无法通过object字段来传递,因此object字段的实用性在跨进程中也变得相当低了。不过所幸我们还有Bundle对象,Bundle可以支持大量的数据类型。接着从Log我们也看出无论是使用拓展Binder类的实现方式还是使用Messenger的实现方式,它们的生命周期方法的调用顺序基本是一样的,即onCreate()、onBind、onUnBind、onDestroy,而且多次绑定中也只有第一次时才调用onBind()。好~,以上的例子演示了如何在服务端解释客户端发送的消息,但有时候我们可能还需要服务端能回应客户端,这时便需要提供双向消息传递了,下面就来实现一个简单服务端与客户端双向消息传递的简单例子。
  先来看看服务端的修改,在服务端,我们只需修改IncomingHandler,收到消息后,给客户端回复一条信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/**
* 用于接收从客户端传递过来的数据
*/
class IncomingHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_SAY_HELLO:
Log.i(TAG, "thanks,Service had receiver message from client!");
//回复客户端信息,该对象由客户端传递过来
Messenger client=msg.replyTo;
//获取回复信息的消息实体
Message replyMsg=Message.obtain(null,MessengerService.MSG_SAY_HELLO);
Bundle bundle=new Bundle();
bundle.putString("reply","ok~,I had receiver message from you! ");
replyMsg.setData(bundle);
//向客户端发送消息
try {
client.send(replyMsg);
} catch (RemoteException e) {
e.printStackTrace();
}

break;
default:
super.handleMessage(msg);
}
}
}

  接着修改客户端,为了接收服务端的回复,客户端也需要一个接收消息的Messenger和Handler,其实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 用于接收服务器返回的信息
*/
private Messenger mRecevierReplyMsg= new Messenger(new ReceiverReplyMsgHandler());


private static class ReceiverReplyMsgHandler extends Handler{
private static final String TAG = "zj";

@Override
public void handleMessage(Message msg) {
switch (msg.what) {
//接收服务端回复
case MessengerService.MSG_SAY_HELLO:
Log.i(TAG, "receiver message from service:"+msg.getData().getString("reply"));
break;
default:
super.handleMessage(msg);
}
}
}

  除了添加以上代码,还需要在发送信息时把接收服务器端的回复的Messenger通过Message的replyTo参数传递给服务端,以便作为同学桥梁,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public void sayHello(View v) {
if (!mBound) return;
// 创建与服务交互的消息实体Message
Message msg = Message.obtain(null, MessengerService.MSG_SAY_HELLO, 0, 0);
//把接收服务器端的回复的Messenger通过Message的replyTo参数传递给服务端
msg.replyTo=mRecevierReplyMsg;
try {
//发送消息
mService.send(msg);
} catch (RemoteException e) {
e.printStackTrace();
}
}

  ok~,到此服务端与客户端双向消息传递的简单例子修改完成,我们运行一下代码,看看Log打印,如下:

  由Log可知,服务端和客户端确实各自收到了信息,到此我们就把采用Messenge进行跨进程通信的方式分析完了,最后为了辅助大家理解,这里提供一张通过Messenge方式进行进程间通信的原理图:

关于绑定服务的注意点

  1.多个客户端可同时连接到一个服务。不过,只有在第一个客户端绑定时,系统才会调用服务的 onBind() 方法来检索 IBinder。系统随后无需再次调用 onBind(),便可将同一 IBinder 传递至任何其他绑定的客户端。当最后一个客户端取消与服务的绑定时,系统会将服务销毁(除非 startService() 也启动了该服务)。

  2.通常情况下我们应该在客户端生命周期(如Activity的生命周期)的引入 (bring-up) 和退出 (tear-down) 时刻设置绑定和取消绑定操作,以便控制绑定状态下的Service,一般有以下两种情况:

如果只需要在 Activity 可见时与服务交互,则应在 onStart() 期间绑定,在 onStop() 期间取消绑定。

如果希望 Activity 在后台停止运行状态下仍可接收响应,则可在 onCreate() 期间绑定,在 onDestroy() 期间取消绑定。需要注意的是,这意味着 Activity 在其整个运行过程中(甚至包括后台运行期间)都需要使用服务,因此如果服务位于其他进程内,那么当提高该进程的权重时,系统很可能会终止该进程。

  3.通常情况下(注意),切勿在 Activity 的 onResume() 和 onPause() 期间绑定和取消绑定,因为每一次生命周期转换都会发生这些回调,这样反复绑定与解绑是不合理的。此外,如果应用内的多个 Activity 绑定到同一服务,并且其中两个 Activity 之间发生了转换,则如果当前 Activity 在下一次绑定(恢复期间)之前取消绑定(暂停期间),系统可能会销毁服务并重建服务,因此服务的绑定不应该发生在 Activity 的 onResume() 和 onPause()中。

  4.我们应该始终捕获 DeadObjectException DeadObjectException 异常,该异常是在连接中断时引发的,表示调用的对象已死亡,也就是Service对象已销毁,这是远程方法引发的唯一异常,DeadObjectException继承自RemoteException,因此我们也可以捕获RemoteException异常。

  5.应用组件(客户端)可通过调用 bindService() 绑定到服务,Android 系统随后调用服务的 onBind() 方法,该方法返回用于与服务交互的 IBinder,而该绑定是异步执行的。

5.关于启动服务与绑定服务间的转换问题
  通过前面对两种服务状态的分析,相信大家已对Service的两种状态有了比较清晰的了解,那么现在我们就来分析一下当启动状态和绑定状态同时存在时,又会是怎么的场景?
  虽然服务的状态有启动和绑定两种,但实际上一个服务可以同时是这两种状态,也就是说,它既可以是启动服务(以无限期运行),也可以是绑定服务。有点需要注意的是Android系统仅会为一个Service创建一个实例对象,所以不管是启动服务还是绑定服务,操作的是同一个Service实例,而且由于绑定服务或者启动服务执行顺序问题将会出现以下两种情况:

先绑定服务后启动服务

  如果当前Service实例先以绑定状态运行,然后再以启动状态运行,那么绑定服务将会转为启动服务运行,这时如果之前绑定的宿主(Activity)被销毁了,也不会影响服务的运行,服务还是会一直运行下去,指定收到调用停止服务或者内存不足时才会销毁该服务。

先启动服务后绑定服务

  如果当前Service实例先以启动状态运行,然后再以绑定状态运行,当前启动服务并不会转为绑定服务,但是还是会与宿主绑定,只是即使宿主解除绑定后,服务依然按启动服务的生命周期在后台运行,直到有Context调用了stopService()或是服务本身调用了stopSelf()方法抑或内存不足时才会销毁服务。

  以上两种情况显示出启动服务的优先级确实比绑定服务高一些。不过无论Service是处于启动状态还是绑定状态,或处于启动并且绑定状态,我们都可以像使用Activity那样通过调用 Intent 来使用服务(即使此服务来自另一应用)。 当然,我们也可以通过清单文件将服务声明为私有服务,阻止其他应用访问。最后这里有点需要特殊说明一下的,由于服务在其托管进程的主线程中运行(UI线程),它既不创建自己的线程,也不在单独的进程中运行(除非另行指定)。 这意味着,如果服务将执行任何耗时事件或阻止性操作(例如 MP3 播放或联网)时,则应在服务内创建新线程来完成这项工作,简而言之,耗时操作应该另起线程执行。只有通过使用单独的线程,才可以降低发生“应用无响应”(ANR) 错误的风险,这样应用的主线程才能专注于用户与 Activity 之间的交互, 以达到更好的用户体验。

前台服务以及通知发送

  前台服务被认为是用户主动意识到的一种服务,因此在内存不足时,系统也不会考虑将其终止。 前台服务必须为状态栏提供通知,状态栏位于“正在进行”标题下方,这意味着除非服务停止或从前台删除,否则不能清除通知。例如将从服务播放音乐的音乐播放器设置为在前台运行,这是因为用户明确意识到其操作。 状态栏中的通知可能表示正在播放的歌曲,并允许用户启动 Activity 来与音乐播放器进行交互。如果需要设置服务运行于前台, 我们该如何才能实现呢?Android官方给我们提供了两个方法,分别是startForeground()和stopForeground(),这两个方式解析如下:

startForeground(int id, Notification notification)
该方法的作用是把当前服务设置为前台服务,其中id参数代表唯一标识通知的整型数,需要注意的是提供给 startForeground() 的整型 ID 不得为 0,而notification是一个状态栏的通知。

stopForeground(boolean removeNotification)
该方法是用来从前台删除服务,此方法传入一个布尔值,指示是否也删除状态栏通知,true为删除。 注意该方法并不会停止服务。 但是,如果在服务正在前台运行时将其停止,则通知也会被删除。

下面我们结合一个简单案例来使用以上两个方法,ForegroundService代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
package com.zejian.ipctest.foregroundService;

import android.app.Notification;
import android.app.Service;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.os.IBinder;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationCompat;

import com.zejian.ipctest.R;

/**
* Created by zejian
* Time 2016/10/4.
* Description:启动前台服务Demo
*/
public class ForegroundService extends Service {

/**
* id不可设置为0,否则不能设置为前台service
*/
private static final int NOTIFICATION_DOWNLOAD_PROGRESS_ID = 0x0001;

private boolean isRemove=false;//是否需要移除

/**
* Notification
*/
public void createNotification(){
//使用兼容版本
NotificationCompat.Builder builder=new NotificationCompat.Builder(this);
//设置状态栏的通知图标
builder.setSmallIcon(R.mipmap.ic_launcher);
//设置通知栏横条的图标
builder.setLargeIcon(BitmapFactory.decodeResource(getResources(),R.drawable.screenflash_logo));
//禁止用户点击删除按钮删除
builder.setAutoCancel(false);
//禁止滑动删除
builder.setOngoing(true);
//右上角的时间显示
builder.setShowWhen(true);
//设置通知栏的标题内容
builder.setContentTitle("I am Foreground Service!!!");
//创建通知
Notification notification = builder.build();
//设置为前台服务
startForeground(NOTIFICATION_DOWNLOAD_PROGRESS_ID,notification);
}


@Override
public int onStartCommand(Intent intent, int flags, int startId) {
int i=intent.getExtras().getInt("cmd");
if(i==0){
if(!isRemove) {
createNotification();
}
isRemove=true;
}else {
//移除前台服务
if (isRemove) {
stopForeground(true);
}
isRemove=false;
}

return super.onStartCommand(intent, flags, startId);
}

@Override
public void onDestroy() {
//移除前台服务
if (isRemove) {
stopForeground(true);
}
isRemove=false;
super.onDestroy();
}

@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
}

  在ForegroundService类中,创建了一个notification的通知,并通过启动Service时传递过来的参数判断是启动前台服务还是关闭前台服务,最后在onDestroy方法被调用时,也应该移除前台服务。以下是ForegroundActivity的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.zejian.ipctest.foregroundService;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

import com.zejian.ipctest.R;

/**
* Created by zejian
* Time 2016/10/4.
* Description:
*/
public class ForegroundActivity extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_foreground);
Button btnStart= (Button) findViewById(R.id.startForeground);
Button btnStop= (Button) findViewById(R.id.stopForeground);
final Intent intent = new Intent(this,ForegroundService.class);


btnStart.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
intent.putExtra("cmd",0);//0,开启前台服务,1,关闭前台服务
startService(intent);
}
});


btnStop.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
intent.putExtra("cmd",1);//0,开启前台服务,1,关闭前台服务
startService(intent);
}
});
}
}

  代码比较简单,我们直接运行程序看看结果:

  ok~,以上便是有关于Service前台服务的内容,接下来再聊聊服务与线程的区别

服务Service与线程Thread的区别

两者概念的迥异

Thread 是程序执行的最小单元,它是分配CPU的基本单位,android系统中UI线程也是线程的一种,当然Thread还可以用于执行一些耗时异步的操作。

Service是Android的一种机制,服务是运行在主线程上的,它是由系统进程托管。它与其他组件之间的通信类似于client和server,是一种轻量级的IPC通信,这种通信的载体是binder,它是在linux层交换信息的一种IPC,而所谓的Service后台任务只不过是指没有UI的组件罢了。

两者的执行任务迥异

在android系统中,线程一般指的是工作线程(即后台线程),而主线程是一种特殊的工作线程,它负责将事件分派给相应的用户界面小工具,如绘图事件及事件响应,因此为了保证应用 UI 的响应能力主线程上不可执行耗时操作。如果执行的操作不能很快完成,则应确保它们在单独的工作线程执行。

Service 则是android系统中的组件,一般情况下它运行于主线程中,因此在Service中是不可以执行耗时操作的,否则系统会报ANR异常,之所以称Service为后台服务,大部分原因是它本身没有UI,用户无法感知(当然也可以利用某些手段让用户知道),但如果需要让Service执行耗时任务,可在Service中开启单独线程去执行。

两者使用场景

当要执行耗时的网络或者数据库查询以及其他阻塞UI线程或密集使用CPU的任务时,都应该使用工作线程(Thread),这样才能保证UI线程不被占用而影响用户体验。

在应用程序中,如果需要长时间的在后台运行,而且不需要交互的情况下,使用服务。比如播放音乐,通过Service+Notification方式在后台执行同时在通知栏显示着。

两者的最佳使用方式

在大部分情况下,Thread和Service都会结合着使用,比如下载文件,一般会通过Service在后台执行+Notification在通知栏显示+Thread异步下载,再如应用程序会维持一个Service来从网络中获取推送服务。在Android官方看来也是如此,所以官网提供了一个Thread与Service的结合来方便我们执行后台耗时任务,它就是IntentService,(如果想更深入了解IntentService,可以看博主的另一篇文章:Android 多线程之IntentService 完全详解),当然 IntentService并不适用于所有的场景,但它的优点是使用方便、代码简洁,不需要我们创建Service实例并同时也创建线程,某些场景下还是非常赞的!由于IntentService是单个worker thread,所以任务需要排队,因此不适合大多数的多任务情况。

两者的真正关系

两者没有半毛钱关系。

管理服务生命周期

  关于Service生命周期方法的执行顺序,前面我们已分析得差不多了,这里重新给出一张执行的流程图(出自Android官网)

  其中左图显示了使用 startService() 所创建的服务的生命周期,右图显示了使用 bindService() 所创建的服务的生命周期。通过图中的生命周期方法,我们可以监控Service的整体执行过程,包括创建,运行,销毁,关于Service不同状态下的方法回调在前面的分析中已描述得很清楚,这里就不重复了,下面给出官网对生命周期的原文描述:

  服务的整个生命周期从调用 onCreate() 开始起,到 onDestroy() 返回时结束。与 Activity 类似,服务也在 onCreate() 中完成初始设置,并在 onDestroy() 中释放所有剩余资源。例如,音乐播放服务可以在 onCreate() 中创建用于播放音乐的线程,然后在 onDestroy() 中停止该线程。
  无论服务是通过 startService() 还是 bindService() 创建,都会为所有服务调用 onCreate() 和 onDestroy() 方法。
  服务的有效生命周期从调用 onStartCommand() 或 onBind() 方法开始。每种方法均有 Intent 对象,该对象分别传递到 startService() 或 bindService()。
  对于启动服务,有效生命周期与整个生命周期同时结束(即便是在 onStartCommand() 返回之后,服务仍然处于活动状态)。对于绑定服务,有效生命周期在 onUnbind() 返回时结束。

  从执行流程图来看,服务的生命周期比 Activity 的生命周期要简单得多。但是,我们必须密切关注如何创建和销毁服务,因为服务可以在用户没有意识到的情况下运行于后台。管理服务的生命周期(从创建到销毁)有以下两种情况:

启动服务

该服务在其他组件调用 startService() 时创建,然后无限期运行,且必须通过调用 stopSelf() 来自行停止运行。此外,其他组件也可以通过调用 stopService() 来停止服务。服务停止后,系统会将其销毁。

绑定服务

该服务在另一个组件(客户端)调用 bindService() 时创建。然后,客户端通过 IBinder 接口与服务进行通信。客户端可以通过调用 unbindService() 关闭连接。多个客户端可以绑定到相同服务,而且当所有绑定全部取消后,系统即会销毁该服务。 (服务不必自行停止运行)

  虽然可以通过以上两种情况管理服务的生命周期,但是我们还必须考虑另外一种情况,也就是启动服务与绑定服务的结合体,也就是说,我们可以绑定到已经使用 startService() 启动的服务。例如,可以通过使用 Intent(标识要播放的音乐)调用 startService() 来启动后台音乐服务。随后,可能在用户需要稍加控制播放器或获取有关当前播放歌曲的信息时,Activity 可以通过调用 bindService() 绑定到服务。在这种情况下,除非所有客户端均取消绑定,否则 stopService() 或 stopSelf() 不会真正停止服务。因此在这种情况下我们需要特别注意。

Android 5.0以上的隐式启动问题

既然有隐式启动,那么就会有显示启动,那就先来了解一下什么是隐式启动和显示启动。

显示启动

直接上代码一目了然,不解释了。

1
2
3
//显示启动
Intent intent = new Intent(this,ForegroundService.class);
startService(intent);

隐式启动

需要设置一个Action,我们可以把Action的名字设置成Service的全路径名字,在这种情况下android:exported默认为true。

1
2
final Intent serviceIntent=new Intent(); serviceIntent.setAction("com.android.ForegroundService");
startService(serviceIntent);

存在的意义

如果在同一个应用中,两者都可以用。在不同应用时,只能用隐式启动。

Android 5.0以上的隐式启动问题

  Android 5.0之后google出于安全的角度禁止了隐式声明Intent来启动Service。如果使用隐式启动Service,会出没有指明Intent的错误,如下:

  主要原因我们可以从源码中找到,这里看看Android 4.4的ContextImpl源码中的validateServiceIntent(Intent service),可知如果启动service的intent的component和package都为空并且版本大于KITKAT的时候只是报出一个警报,告诉开发者隐式声明intent去启动Service是不安全的.

  而在android5.0之后呢?我们这里看的是android6.0的源码如下(sublime text查android各个版本源码就是爽呀!!):

  从源码可以看出如果启动service的intent的component和package都为空并且版本大于LOLLIPOP(5.0)的时候,直接抛出异常,该异常与之前隐式启动所报的异常时一致的。那么该如何解决呢?

解决方式

设置Action和packageName

1
2
3
final Intent serviceIntent=new Intent(); serviceIntent.setAction("com.android.ForegroundService");
serviceIntent.setPackage(getPackageName());//设置应用的包名
startService(serviceIntent);

将隐式启动转换为显示启动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static Intent getExplicitIntent(Context context, Intent implicitIntent) {
// Retrieve all services that can match the given intent
PackageManager pm = context.getPackageManager();
List<ResolveInfo> resolveInfo = pm.queryIntentServices(implicitIntent, 0);
// Make sure only one match was found
if (resolveInfo == null || resolveInfo.size() != 1) {
return null;
}
// Get component info and create ComponentName
ResolveInfo serviceInfo = resolveInfo.get(0);
String packageName = serviceInfo.serviceInfo.packageName;
String className = serviceInfo.serviceInfo.name;
ComponentName component = new ComponentName(packageName, className);
// Create a new intent. Use the old one for extras and such reuse
Intent explicitIntent = new Intent(implicitIntent);
// Set the component to be explicit
explicitIntent.setComponent(component);
return explicitIntent;
}

调用方式如下:

1
2
3
4
Intent mIntent=new Intent();//辅助Intent
mIntent.setAction("com.android.ForegroundService");
final Intent serviceIntent=new Intent(getExplicitIntent(this,mIntent));
startService(serviceIntent);

到此问题完美解决。

如何保证服务不被杀死

  实际上这种做法并不推荐,但是既然谈到了,我们这里就给出一些实现思路吧。主要分以下3种情况

因内存资源不足而杀死Service

这种情况比较容易处理,可将onStartCommand() 方法的返回值设为 START_STICKY或START_REDELIVER_INTENT ,该值表示服务在内存资源紧张时被杀死后,在内存资源足够时再恢复。也可将Service设置为前台服务,这样就有比较高的优先级,在内存资源紧张时也不会被杀掉。这两点的实现,我们在前面已分析过和实现过这里就不重复。简单代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 返回 START_STICKY或START_REDELIVER_INTENT
* @param intent
* @param flags
* @param startId
* @return
*/
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// return super.onStartCommand(intent, flags, startId);
return START_STICKY;
}

用户通过 settings -> Apps -> Running -> Stop 方式杀死Service

这种情况是用户手动干预的,不过幸运的是这个过程会执行Service的生命周期,也就是onDestory方法会被调用,这时便可以在 onDestory() 中发送广播重新启动。这样杀死服务后会立即启动。这种方案是行得通的,但为程序更健全,我们可开启两个服务,相互监听,相互启动。服务A监听B的广播来启动B,服务B监听A的广播来启动A。这里给出第一种方式的代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package com.zejian.ipctest.neverKilledService;

import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.IBinder;
import android.support.annotation.Nullable;

/**
* Created by zejian
* Time 2016/10/4.
* Description:用户通过 settings -> Apps -> Running -> Stop 方式杀死Service
*/
public class ServiceKilledByAppStop extends Service{

private BroadcastReceiver mReceiver;
private IntentFilter mIF;

@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}

@Override
public void onCreate() {
super.onCreate();
mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Intent a = new Intent(ServiceKilledByAppStop.this, ServiceKilledByAppStop.class);
startService(a);
}
};
mIF = new IntentFilter();
//自定义action
mIF.addAction("com.restart.service");
//注册广播接者
registerReceiver(mReceiver, mIF);
}

@Override
public void onDestroy() {
super.onDestroy();

Intent intent = new Intent();
intent.setAction("com.restart.service");
//发送广播
sendBroadcast(intent);

unregisterReceiver(mReceiver);
}
}

用户通过 settings -> Apps -> Downloaded -> Force Stop 方式强制性杀死Service

这种方式就比较悲剧了,因为是直接kill运行程序的,不会走生命周期的过程,前面两种情况只要是执行Force Stop ,也就废了。也就是说这种情况下无法让服务重启,或者只能去设置Force Stop 无法操作了,不过也就没必要了,太流氓了。。。。

ok~,以上便是保证服务在一定场景下不被杀死的解决思路,关于第3种情况,如果有解决方案,请留言哈。好,关于Service的全部介绍就此完结。

树莓派android系统,vnc远程访问

A VNC Server for Android allows remotely accessing the device’s screen. In this post, I will show you how to run a VNC server on Android and access its screen remotely.

Let us clear one thing up front. We are not going to be discussing VNC clients for Android. We are going to be looking at the other side of Virtual Network Computing (VNC), which is running an actual VNC server on Android device.

Information on how to remotely access Android device’s homescreen or interface is sparse. Most of the information out there seem to look at the reverse scenario - accessing a remote server on Android device using VNC Client app. Read: Setup VNC Server on Ubuntu: Complete Ubuntu Remote Desktop Guide
Setting up a VNC server for Android just as simple. In this post, I will show you how to setup an Android VNC server without root.

WHAT IS A VNC SERVER?

VNC stands for Virtual Network Computing. It is a way of sharing the graphical desktop of a system to a remote system. In addition to the graphical interface, input events (keyboard, mouse, etc.), audio, and clipboard can also be shared with the remote system.

Xubuntu Desktop From Remote Server On Tightvnc Viewer In Windows

This is a super cool way to access remote systems and works as if you are actually in front of the remote workstation. With my recent move to Proxmox, VNC setup on Ubuntu Server has as been very handy for me. [Read: Ultimate Docker Home Server with Traefik 2, LE, and OAuth / Authelia [2020]]

In this post, we are going to try to access my Android phone’s interface via VNC from a Windows laptop.

What can you do with VNC Server on Android?

Well, this is the million dollar question isn’t it?

In my case, I have Android tablets showing Home Assistant dashboards for home automation. [Read: My Smart Home setup – All gadgets and apps I use in my automated home]

Sometimes I want to check what is showing on the dashboard, which having to be in front of it.

SmartHomeBeginner brings in-depth tutorials easy enough to understand even for beginners. This takes considerable amount of work. If this post helps you, please consider supporting us as a token of appreciation:

  • Feeling generous? Buy me a coffee (or two).
  • May be another day? Shop on Amazon using our links. Your prices won’t change but we get a small commission.
  • Don’t feel like spending? You can still show your support by sharing this post, linking to it in forums, or even commenting below.
    That is just one application. If you have other applications you can think, please share with the rest of the us in the comments section.

VNC SERVER FOR ANDROID WITHOUT ROOT

While there are numerous VNC clients for Android, there aren’t many VNC Server Android apps. In fact, when I did my research I could only find one that worked well: droidVNC-NG.

Droidvnc-Ng App - Vnc Server For Android

The best part is, droidVNC-NG app is free. Go ahead, head to the Play store and install droidVNC-NG and let us get started with setting up an Android VNC server. Alternatively, if you have droidVNC-NG VNC server Android apk, install it and proceed.

Is VNC Server free?
It depends on the server app. Some are paid. But there are free ones too (eg. droidVNC-NG for Android). Once a VNC Server is running, there are several free client apps to access the server.

  1. VNC Server Port and Password
    When you open the app, you should see an Android VNC server interface that looks like what is shown in the image below.
    Droidvnc App - Set The Port And Vnc Password

First, provide a port number for VNC server. Typically it is 5900. But you could change it to whatever (typically 59xx). In this Android VNC server guide, I am going to use 5902.

Next, provide a password for the VNC connection. Pick a strong password. In this tutorial, I am picking test (NOT a strong password).

We are not going to be discussing advanced configurations such as VNC through SSH tunnel, which provides an encrypted connection. Therefore, setting up a strong password is a bare minimum for security.

  1. Accessibility Permissions for Android VNC Server
    Next, we need to enable Accessibility permissions for the Android VNC server. This allows remote control of the Android device. So click on Screen Capturing and grant accessibility permissions to droidVNC-NG app, as shown below.

    Enable Android Accessibility Permissions
  2. Grant View and Control Permissions
    We also need to grant view and control permissions to the VNC server for Android. Click on Input to grant this permission in the screens that follow (shown below).

    Enable View And Control Permissions
  3. Grant File Access Permissions to VNC Server on Android
    Finally, click on File Access on the droidVNC-NG app configuration screen. Allow the app to access files on your android device, as shown below.

    Enable File Access Permissions
  4. Start VNC Server on Android
    Finally, hit the start button to start VNC server for Android. You will be presented with the warning screen shown below.

    Sensitive Information Casting Warning!!!

Carefully read and understand the implications prior to continuing. Once started, the Android VNC server should be listening for connections on the port you chose (5902 in this how-to).

Start Android Virtual Network Computing Server

In addition to the port number, you will also need the IP address of the Android device. As shown above, in this guide it is 192.168.1.120, which is the LAN IP address of my Android device.

If you are trying to access an Android system from outside your local network, then remember to setup port-forwarding on your router/gateway.

CONNECT TO ANDROID VNC SERVER

Now that our VNC Server on Android is running, let us see how to access the screen remotely. There are multiple ways to do this.

One of my favorite ways is to use Guacamole, which offers a HTML5 browser based access to VNC servers.

But for simplicity, in this guide, we are going to use TightVNC client for Windows. Using this VNC client app, we will view the Android device from my Windows system.

  1. Provide VNC Server details on the Client app
    Open the VNC client and provide the IP address and port number. In this case, they are 192.168.1.20:5902.
    Connect To Android Vnc Server Using Vnc Client

Wait!!! But the image above shows 2, and not 5902 for port.

As explained previously, VNC ports are usually in the range 5900 to 5999. For VNC ports, it is customary to provide only the incremental number from 5900. Therefore, for port 5902, the increment is 2. The TightVNC client app automatically trims the port number down to just the incremental number.

  1. Provide VNC Password
    Next provide the VNC password that you chose while setting up the VNC Server for Android.
    Enter Vnc Server Password

SmartHomeBeginner brings in-depth tutorials easy enough to understand even for beginners. This takes considerable amount of work. If this post helps you, please consider supporting us as a token of appreciation.
3. Access Android VNC Server on Windows
You should now be able to remotely access and interact with your Android device. You could work on it and control it using your mouse (and keyboard) - mouse-click to select, click and drag to move screen or scroll, etc.

Android Interface Via Vnc Client On Windows

So there you have it - the full graphical interface of Android that can be remotely controlled from practically any platform.

FINAL THOUGHTS ON RUNNING A VNC SERVER ON ANDROID

While this is all fun to do, I want to share my experience in using VNC Server for Android without root. Simply put, the performance was not great and it was not a smooth experience controlling the Android device using a mouse. It was choppy and slow. And this was on my Pixel 3, which has decent specifications at this point (March 2021).

On older Android devices, the experience might be even worse. Nevertheless, the droidVNC-NG app works great, is really one of a kind at this point, and free to use.

There are alternatives such as the VNC Server app from XDA Developers. This app allows both wifi and USB access. Wired access may provide a smoother experience but I have not tried this yet. If you have tried this, I would appreciate if you share your experience in the comments.

Other than that, it was a fun and easy project to setup a VNC Server for Android and access it remotely.

树莓派android系统,无屏远程访问

当树莓派首次安装完Android系统,需要借助屏幕设置wifi密码(暂时没有其他好方案)。设置完成后,能够正常上网,后续操作可以在无屏的方式下,远程访问树莓派。
一般远程访问有多种方案,这里介绍两种方式:

  • vysor投屏
  • vnc投屏

准备工具

  • vysor
  • vnc viewer
  • droidVNC-NG
  • adb
  • mac book & pc

打开Android开发者设置

参考《树莓派安装Android操作系统

方式一:vysor投屏

vysor是一款手机投屏软件,通过PC控制手机。通过官网https://www.vysor.io/ 下载桌面版软件或chrome插件,安装完成后,启动软件。

  • 按照下图,连接adb server:

  • 链接成功后,如下图
  • 点击上图“播放”按钮,开始远程投屏

注意:

  1. vysor可以通过USB或wifi连接(需要收费)。
  2. USB是通过ADB方式连接,同一时间只能一个连接,如Android studio开发联调和vysor投屏会冲突。

方式二:vnc投屏

参考《树莓派android系统,vnc远程访问

树莓派安装Android操作系统

准备工具

  • Raspberry Pi Imager
  • Android 9 OS
  • 16G SD卡
  • Mac Book

制作安装盘

下载Android系统

树莓派4B支持免费Android操作系统,下载地址为:https://konstakang.com/devices/rpi4/ ,选择自己喜欢版本

烧制操作系统

  • 解压下载zip文件
  • 启动Raspberry Pi Imager,选择刚才解压img文件,并选择SD卡,最后点击“烧录”按钮,如下图:

启动系统

  • 将烧制的SD卡插入Raspberry主板
  • 插上电脑屏幕,连接键盘和鼠标
  • 按下开机键,android系统将启动
  • 第一次在Raspberry Pi上启动LineageOS时,看到以下屏幕。点击右下角的 “Next >”按钮,开始初始化。
  • 在下一个屏幕上,选择Android设备使用的语言。可以使用屏幕中间的选项来选择您要使用的语言(1.置后后,请点击 “下一步 >”按钮
  • 现在使用屏幕左侧的下拉框设置设备的时区(1.也可以使用此设置来控制当前的日期和时间。设置好时区后,可以点击 “下一步 >”按钮进行设置
  • 如果没有使用以太网连接,下一个屏幕是设置连接到Wi-Fi。Raspberry Pi应该已经扫描了可用的Wi-Fi网络,点击要连接的网络(1.)。连接后,可以点击 “下一步 “按钮继续
  • 现在可以禁用或启用LineageOS的一些功能。其中一些功能包括隐私保护,可以阻止应用程序在未经批准的情况下读取你的联系人、信息或通话记录。设置好之后,请点击 “Next >”按钮继续进行设置。
  • 最后一个设置页面为的Raspberry Pi支持的Android设备设置PIN、模式或密码。请注意,如果打算安装谷歌应用,建议直接跳到下一步。否则,如果想设置锁屏,请点击 “设置 “按钮(1.如果不想设置密码,请点击 “SKIP >”按钮(2)。
  • 在树莓派上完成Android设置,点击右下角的 “START >”按钮。
  • 最终,进入android桌面。

开启开发者设置

  • 在安卓设备的桌面上,从最下面的三个应用中点击并向上拖动,弹出应用库。
  • 在该菜单内,点击 “设置 “应用。
  • 向下滚动到底部,找到 “关于平板电脑 “选项并点击。
  • 在这个设置页面中,向下滚动到底部,直到看到 “Build Number “的文字。需要点击这个文字(1.),弹出一条信息,通知你 “你已经启用了开发设置!”。应该只需要5次点击就可以启用设置。完成后,点击返回按钮(3.)返回到设置页面。
  • 再次向下滚动到底部,打开 “系统 “设置页面。
  • 请点击本页底部的 “高级 “切换。
  • 滚动到底部,找到 “开发者选项”,然后点击它。
  • 有两个不同的选项,我们需要在这个页面中进行配置。首先,向下滚动,直到你看到 “Root access”,然后点击它。
  • 在弹窗中,我们需要同时启用 “Apps和ADB “的root权限。点击相应的选项进行操作。
  • 最后,会被警告关于在你的Android设备上启用root权限可能带来的影响。需要root权限,点击 “确定 “按钮。
  • 接下来,需要下拉到 “本地终端 “选项,点击切换。
  • 为了让本地终端出现,需要使用Android界面重启我们的Raspberry Pi。要调出电源选项菜单,您需要按键盘上的F5键。进入电源菜单后,点击 “重新启动 “按钮。

修复android,wifi不能连接和时钟错误

WIFI联网会出现感叹号,这是因为系统自带的网络测试时国外的,所以连不上,要解决这个问题,打开开发选项后用adb命令解决。

  • 在安卓设备的桌面上,从最下面的三个应用中点击并向上拖动,弹出应用库。
  • 选择“终端”。
  • 打开后,输入指令:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 指定国内服务
    settings delete global captive_portal_https_url
    settings delete global captive_portal_http_url
    settings put global captive_portal_https_url https://connect.rom.miui.com/generate_204
    settings put global captive_portal_http_url http://connect.rom.miui.com/generate_204

    # 时钟源
    setprop persist.sys.timezone Asia/Shanghai
    settings put global ntp_server ntp1.aliyun.com
  • 经过上述操作,wifi能够正确连接了

修改系统环境变量,以自如的切换多个版本的 jdk

1
2
3
vi ~/.bash_profile 并填写下文内容
source ~/.bash_profile // 刷新环境变量
java -version // 查看当前的 jdk 版本

需要填写的内容为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 设置自带的 jdk1.6
export JAVA_6_HOME=`/usr/libexec/java_home -v 1.6`
# 设置 jdk1.7
export JAVA_7_HOME=`/usr/libexec/java_home -v 1.7`
# 设置 jdk1.8
export JAVA_8_HOME=`/usr/libexec/java_home -v 1.8`

# 默认 jdk 使用1.6版本
export JAVA_HOME=$JAVA_6_HOME

# alias 命令动态切换 jdk 版本
alias jdk6="export JAVA_HOME=$JAVA_6_HOME"
alias jdk7="export JAVA_HOME=$JAVA_7_HOME"
alias jdk8="export JAVA_HOME=$JAVA_8_HOME"

每次使用 jdk6、jdk7、jdk8 命令切换 jdk 版本时,都可以输入 java -version 来查看是否已经成功。

原理

当你安装 jdk 完成,进入 /Library/Java/JavaVirtualMachines 目录你会看到相应 jdk 文件夹的存在。当你安装了多版本 jdk 时,它们同时存在于这个目录下。

那么你使用的 java -version 做了什么?我们一步一步追踪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
➜  ~ which java // 使用 `which java` 得到 `/usr/bin/java`
/usr/bin/java
➜ ~ ll /usr/bin/java // 使用 `ll /usr/bin/java` 得到 `.../Current/Commands/java`
lrwxr-xr-x 1 root wheel 74B 10 9 17:53 /usr/bin/java -> /System/Library/Frameworks/JavaVM.framework/Versions/Current/Commands/java
➜ ~ cd /System/Library/Frameworks/JavaVM.framework/Versions
➜ Versions ls // 在这里,有苹果维护的多个 jdk 版本
1.4 1.5 1.6 A CurrentJDK
1.4.2 1.5.0 1.6.0 Current
➜ Versions pwd
/System/Library/Frameworks/JavaVM.framework/Versions
➜ Versions ll
total 64
lrwxr-xr-x 1 root wheel 10B 10 9 17:53 1.4 -> CurrentJDK
lrwxr-xr-x 1 root wheel 10B 10 9 17:53 1.4.2 -> CurrentJDK
lrwxr-xr-x 1 root wheel 10B 10 9 17:53 1.5 -> CurrentJDK
lrwxr-xr-x 1 root wheel 10B 10 9 17:53 1.5.0 -> CurrentJDK
lrwxr-xr-x 1 root wheel 10B 10 9 17:53 1.6 -> CurrentJDK
lrwxr-xr-x 1 root wheel 10B 10 9 17:53 1.6.0 -> CurrentJDK
drwxr-xr-x 10 root wheel 340B 10 9 17:53 A
lrwxr-xr-x 1 root wheel 1B 10 9 17:53 Current -> A
lrwxr-xr-x 1 root wheel 52B 10 9 17:53 CurrentJDK -> /Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents

可见苹果公司在开发 macOS 时已经内置了 jdk 版本管理的功能。它也提供了相应的 command line 工具。使用 /usr/libexec/java_home 即可直接查看当前的 $JAVA_HOME

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
➜  ~ /usr/libexec/java_home
/Library/Java/JavaVirtualMachines/jdk1.8.0_102.jdk/Contents/Home
➜ ~ /usr/libexec/java_home -V
Matching Java Virtual Machines (4):
1.8.0_102, x86_64: "Java SE 8" /Library/Java/JavaVirtualMachines/jdk1.8.0_102.jdk/Contents/Home
1.7.0_80, x86_64: "Java SE 7" /Library/Java/JavaVirtualMachines/jdk1.7.0_80.jdk/Contents/Home
1.6.0_65-b14-468, x86_64: "Java SE 6" /Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home
1.6.0_65-b14-468, i386: "Java SE 6" /Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home

/Library/Java/JavaVirtualMachines/jdk1.8.0_102.jdk/Contents/Home
➜ ~ /usr/libexec/java_home -v 1.6
/Library/Java/JavaVirtualMachines/1.6.0.jdk/Contents/Home
➜ ~ /usr/libexec/java_home -v 1.7
/Library/Java/JavaVirtualMachines/jdk1.7.0_80.jdk/Contents/Home
➜ ~ /usr/libexec/java_home -v 1.8
/Library/Java/JavaVirtualMachines/jdk1.8.0_102.jdk/Contents/Home

所以,我们在 .bash_profile 文件中加入 $JAVA_HOME 的动态设置,就可以自如的切换多个 jdk 版本了

关于为什么使用 .bash_profile 文件,你可能要参考如下内容了解:

为什么会有管理多个 jdk 的需求?

以我自己为例好了,比如我平时做 Android 或者 Java 开发,都是指定了某个 jdk 版本的,例如 1.7。因为它支持了足够多的特性。然而,我有的时候需要使用某个低版本的软件比如 Charles,它竟然要求运行在 java1.6 环境下,没错,有些软件就是会有这样的依赖。这个时候怎么办呢,其实让系统同时存在多个 jdk,需要的时候进行切换就可以了。

参考:

android修改包名方法

在 Studio 3.0之后, 可直接通过 Androidmenifest 修改部分包名。

修改流程如下:

  • 进入Androidmanifest.xml 文件,找到 package 名称,选中需要修改的部分。
    比如原包名为
    com.kch8.android
    如果需要修改中间的 kch8 ,那么我们就选中 kch8 ,
  • 右键 -> Refactor -> Rename , (Mac 快捷键为 fn + shift+F6)
  • 然后选择 Rename package , 输入要修改目标的名称 ,直接点击 Refactor , 左下方继续点击 Do Refactor
  • 包名修改成功后,选择build -> clean project,然后关闭Studio
  • 删除工程根目录的.idea文件夹
  • 通过Studio重新开启工程和运行 工程

通过上述操作,修改包名后,同时热更新也生效!