Android重学系列 Service 启动和绑定原理

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android重学系列 Service 启动和绑定原理相关的知识,希望对你有一定的参考价值。

参考技术A 我们已经了解了BroadcastReceiver的原理,我们再来看看四大组件之一的Service是怎么启动的,以及怎么运行的原理。

如果遇到什么问题可以来到 https://www.jianshu.com/p/c4927c0b80a9 本文下进行交流

启动Service的入口就是startService和bindService方法。我们先来看看startService在ContextImpl中做了什么。

文件:/ frameworks / base / core / java / android / app / ContextImpl.java

此时调用的就是AMS的startService方法。

mServices是一个ActiveServices对象。这个对象是在AMS的构造函数中初始化好的。

这里调用了ActiveServices的startServiceLocked。

文件:/ frameworks / base / services / core / java / com / android / server / am / ActiveServices.java

核心流程有如下三个:

注意这里addToStarting是一个比较关键的判断,addToStarting默认为false。

如果此时不是启动前台服务,则需要进一步进行处理。如果ProcessRecord为空或者curProcState大于PROCESS_STATE_RECEIVER这个优先级数值;也就是优先级更小。

为了避免此时App应用是没有任何的前台ui,或者App应用还没有声明。避免有的App通过startService进行应用的包活或者拉起应用。就会进行如下能够存在的最大后台服务数量,则放入mDelayedStartList中进行延时启动后台服务,现在直接返回了。

不然则说明能够允许启动后台服务, 就设置为addToStarting为true。

通过ComponentName也就是包名和类名查找ServiceRecord;通过Intent意图过滤找到ServiceRecord。·

核心方法是bringUpServiceLocked。如果bringUpServiceLocked返回了异常,就返回一个特殊的ComponentName对象。

addToStarting为true,说明此时是一个能够启动的后台服务,则ServiceRecord添加到mStartingBackground中。如果mStartingBackground的数量为0,则直接调用ServiceMap的rescheduleDelayedStartsLocked启动后台服务。

这几个关键的步骤,让我们依次的考察,先来看看scheduleCreateService中做了什么。

如果第一次启动就走第一个if的分支:

能看到和BroadcastReceiver的ANR思路一样,通过一个延时的Handler,如果达到时间了还没有移除这个Handler消息则报ANR异常。

这里根据启动前台和后台分为两种超时时间:

前台分别是10秒,后台服务不属于后台进程组(判断adj是否在SCHED_GROUP_BACKGROUND)是20秒,后台服务属于后台进程组 200秒。

把数据封装成CreateServiceData后,通过Handler调用如下方法:

透三点的核心原理可以看我写的的Application创建和BroadcastReceiver原理两篇文章。来看看Service中都做了什么?

很简单就是保存了传递过来的参数,值得注意的是这个IBinder对象其实就是指ServiceRecord对象。

接着执行Service.onCreate这个空实现的方法。

本质上还是调用了ActiveServices的serviceDoneExecutingLocked方法。

type是SERVICE_DONE_EXECUTING_ANON,所不会做更多的处理。 最后执行了serviceDoneExecutingLocked方法。

这个方法不断的循环遍历List<ServiceStartArgs>分发SERVICE_ARGS消息,这个消息通过主线程的Looper调用handleServiceArgs。

bindService在开发中用的不是很多,这里稍微提一下他的使用。
首先申明一个ServiceConnection对象,用于绑定服务端的Service。

调用bindService的方法绑定到某个Service中。当服务端的service成功回调onBind方法,我们只需要返回对应的Binder对象。就能使用调用bindService的客户端在ServiceConnection的onServiceConnected的回调中获得Binder对象。

之后就能通过这个Binder调用本进程或者其他进程的的方法了。实际上我们可以把这个过程看成一个多对多的服务-客户端模型。多个客户端通过Binder向多服务端Service通信,每一次我们都可以通过ComponentName判断不同服务返回来的Binder对象。

这里面很简单,和BroadcastReceiver的思路很像。动态注册的BroadcastReceiver会封装成一个ReceiverDispatcher,而这里把ServiceConnection封装成LoadedApk.ServiceDispatcher对象。

并且会把ServiceDispatcher作为value,ServiceConnection作为key缓存一个map中。并且以context为key,把这个临时的map作为value缓存起来。这样一个Context就映射有了多个ServiceDispatcher对象,也就可以注册多个监听被绑定Service状态的监听者了。

重学SpringCloud系列二之服务注册与发现---上

重学SpringCloud系列之服务注册与发现---上


系列专辑:

  • 重学SpringCloud系列二之服务注册与发现
  • 重学SpringCloud系列二之服务注册与发现 (当前专辑)

构建eureka服务注册中心

Eureka服务注册中心

生产环境中的Eureka服务注册中心的架构应该如下图所示


可以简单的理解:

  • 服务注册中心就是一个婚姻介绍所
  • A服务找老婆,B服务找老公
  • 每个服务需要向婚姻介绍所登记(服务注册),也都需要从婚姻介绍所获取候选人信息(服务发现)

为了保障微服务的高可用,提供更高的并发能力,通常需要部署多份实现集群部署。为了快速的让大家入手Spring Cloud 微服务架构,此文只做简化版的服务注册中心的构建。后续我们会讲集群部署。


搭建Eureka服务注册中心

Spring Cloud版本管理

eureka服务注册中心,是我们第一次使用Spring Cloud相关的组件。所以我们需要在的父项目中添加统一的版本管理。

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <!--  聚合工程,父工程打包方式为pom-->
  <groupId>dhy.xpy</groupId>
  <artifactId>StudyCloudWithMe</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>pom</packaging>

  <modules>
    <module>dhy-service-sms</module>
      <module>dhy-service-common</module>
    <module>dhy-dao-service</module>
      <module>dhy-service-rabc</module>
  </modules>

  <!--基础属性设置-->
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <java.version>1.8</java.version>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>

  <!-- 统一版本管理 -->
  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.5.RELEASE</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <version>Hoxton.SR3</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <dependency>
        <groupId>dhy.xpy</groupId>
        <artifactId>dhy-service-common</artifactId>
        <version>1.0</version>
      </dependency>
      <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.1.4</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
      <dependency>
        <groupId>dhy.xpy</groupId>
        <artifactId>dhy-dao-service</artifactId>
        <version>1.0-SNAPSHOT</version>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-devtools</artifactId>
      <scope>runtime</scope>
      <optional>true</optional>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
          <excludes>
            <exclude>
              <groupId>org.projectlombok</groupId>
              <artifactId>lombok</artifactId>
            </exclude>
          </excludes>
        </configuration>
      </plugin>
    </plugins>
  </build>

</project>


新建Spring Boot项目

需要注意的是:eureka服务注册中心本身也是一个微服务,它是使用Spring Boot为基础服务框架搭建的。所以我们需要在父项目中新建一个Spring Boot子项目:dhy-server-eureka

这个过程,我们已经多次做过了,就不写详细步骤了(翻看前面系列)。此处是新建及项目调整完成之后的pom.xml文件。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>StudyCloudWithMe</artifactId>
        <groupId>dhy.xpy</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>dhy-server-eureka</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
    </dependencies>

</project>
  • 注意这里spring Boot 2.0引入的包是spring-cloud-starter-netflix-eureka-server,正是该包帮助我们实现eureka服务注册中心。
  • 在Spring Boot 1.0版本引入的包是spring-cloud-starter-eureka,在Spring Boot2.0版本中不再使用。
  • 因为在父项目中进行了版本号的管理,所以子项目中不必写版本号。

修改application配置文件

server:
  port: 8761
  servlet:
    context-path: /eureka

spring:
  application:
    name: dhy-eureka-server

eureka:
  instance:
    hostname: localhost  #该服务部署的主机名称,参考windows的hosts文件或linux的/etc/hosts
  client:
    #是否从其他实例获取服务注册信息,因为这是一个单节点的EurekaServer,不需要同步其他的EurekaServer节点的数据,所以设置为false;
    fetch-registry: false
    #表示是否向eureka注册服务,即在自己的eureka中注册自己,默认为true,此处应该设置为false;
    register-with-eureka: false
    service-url:
      #设置与Eureka server交互的地址查询服务和注册服务都需要依赖这个地址。
      defaultZone: http://$eureka.instance.hostname:$server.port/eureka/

配置启动类注解

@SpringBootApplication
@EnableEurekaServer 
public class Main 
    public static void main(String[] args) 
        SpringApplication.run(Main.class,args);
    



访问测试

访问http://localhost:8761/eureka/,出现如下页面表示我们服务注册中心搭建成功。


向服务注册中心注册服务

微服务注册客户端构建

在每个微服务中(dhy-service-rbac和dhy-service-sms)做如下的一些操作,将微服务注册到服务注册中心。通过maven引入eureka注册客户端。

       <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

配置application.yml

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/eureka/    

注意:这里是两个eureka,我没写错。这里的url取决于eureka服务注册中心的配置。后文会说明。

在主启动类上添加@EnableEurekaClient注解

@EnableEurekaClient
public class DhyServiceSmsApplication 

当通过http://localhost:8761/eureka/访问服务注册中心用户界面的时候,出现如下红框中的微服务表示服务注册成功。


常见bug

  • 碰上这个报错,第一时间要取检查defaultZone配置的Url是否正确
  • 其次,要保证启动顺序,服务注册中心先启动,微服务后启动
  • 服务注册中心是否默认配置了Spring Security?(我们上一节没有配置相关安全认证)
  • 注意eureka客户端:http://localhost:8761/eureka/eureka/,有两个eureka。第一个eureka是server.servlet.context-path:
    /eureka。可以通过配置改变的;第二个eureka是服务注册的服务端点。

这里我们没有配置server.servlet.context-path,因此不需要加两个eureka


第一个微服务调用


我们模拟一个短信发送的简单业务逻辑,业务不是重点,重点在于远程服务调用的演示

基础原理说明:

为了在dhy-service-rbac中更方便的调用远程服务,我们使用OpenFeign。它能使我们像调用本地函数一样调用远程服务api。


服务调用者基础配置(dhy-service-rbac)

通过maven坐标引入OpenFeign

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

在启动类上面加上注解

@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class Main 

服务调用者业务实现(dhy-service-rbac)

在dhy-service-rbac本地服务中,伪装一个dhy-service-sms远程服务api接口。

@Component
@FeignClient("DHY-SERVICE-SMS")
public interface SmsService 
    @PostMapping("/sms/send")
    AjaxResponse send(@RequestParam("phoneNo") String phoneNo,@RequestParam("content") String content);

注意

  • "DHY-SERVICE-SMS"决定了我们要在当前服务中调用哪一个远程服务,"DHY-SERVICE-SMS"是远程服务的名称(spring.application.name)转大写,而且必须是大写。
  • 接口抽象方法定义决定了我们要远程调用"DHY-SERVICE-SMS"中的哪一个api,该抽象方法的定义就是一个"伪装"。将远程服务方法伪装成本地方法。

像调用本地服务一样调用伪装之后的远程服务

@RestController
public class SmsController 
    @Resource
    private SmsService smsService;
    
    @PostMapping("/sms/send")
    public AjaxResponse sendSms(@RequestParam("phoneNo") String phoneNo, @RequestParam("content") String content)
   
       return smsService.send(phoneNo,content);
   


在dhy-service-sms微服务模块中被真正调用的方法:


测试:


远程服务调用

HttpClient远程服务调用

实现服务进程之间的调用方法有很多,在SpringCloud之前常用如下:

  • HttpClient提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包。
  • RestTemplate设计是为了Spring更好的请求并解析Restful风格的接口返回值而设计的,对HttpClient进行了封装以提高其易用性。



在spring cloud体系中,先后出现了ribbon、Feign、OpenFeign等远程服务调用框架,易用性不断提高、功能也不断完善。这些内容在后面的章节中,会依次的详细介绍。

所以,我们要学习OpenFeign,实际上就是要学习

  • HttpClient或HttpUrlConnection或OkHttp
  • RestTemplate
  • Ribbon
  • Feign

先来看看最佳实践

下图是我们使用OpenFeign来定义的声明式远程服务调用接口,也是我们的最佳实践。我们要在心里面记住这六行代码,然后在本章中跟着我学习:Spring Cloud体系的远程服务调用是如何一步一步的从HttpClient 进化到 OpenFeign。


使用HttpClient实现远程服务调用

Junit测试类,远程调用:“/sms/send"短信发送接口服务。在测试之前,我们需要先把aservice-sms微服务启动起来。

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
   <scope>test</scope>
   <exclusions>
      <exclusion>
         <groupId>org.junit.vintage</groupId>
         <artifactId>junit-vintage-engine</artifactId>
      </exclusion>
   </exclusions>
</dependency>

执行测试用例之前,要确保已经maven引入了spring-boot-starter-test(Junit)

import com.fasterxml.jackson.databind.ObjectMapper;
import com.msg.AjaxResponse;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.junit.jupiter.api.Test;

import java.io.*;
import java.util.ArrayList;
import java.util.List;

public class HttpClientTest 

  @Test
  void httpPost() throws Exception 
    //发送远程的http请求的地址
    String url = "http://localhost:2333/sms/send";
    //创建HttpClient对象
    CloseableHttpClient client = HttpClients.createDefault();
    //创建HttpPost对象, 发送post请求
    HttpPost以上是关于Android重学系列 Service 启动和绑定原理的主要内容,如果未能解决你的问题,请参考以下文章

Android深入四大组件Service的绑定过程

Android重学系列 IMS与事件分发(下)

重学SpringCloud系列二之服务注册与发现---上

Android 之 Service(一)启动,绑定服务

Android 重学系列 View的绘制流程(六) 硬件渲染(上)

浅谈Android四大组建之一Service---Service与Activity的绑定