关于service:前端缓存机制提升网站性能-Service-Worker

PWA 简介PWA(Progressive Web Apps)不是特指某一项技术,而是利用多项技术来改善用户体验的 Web App,为 Web App 提供相似 Native App 的用户体验。其核心技术包含 Web App Manifest,Web Push,Service Worker 和 Cache Api 等,用户体验才是 PWA 的外围。 PWA 次要特点如下:牢靠 - 即便在网络不稳固甚至断网的环境下,也能霎时加载并展示用户体验 - 疾速响应,具备平滑的过渡动画及用户操作的反馈用户黏性 - 和 Native App 一样,能够被增加到桌面,能承受离线告诉,具备沉迷式的用户体验 写在后面文章不具体解说 PWA 技术细节,如果对 PWA 技术感兴趣,文末筹备了一些材料,能够参考学习此次调研目标并非为网站残缺接入 PWA 技术,而是利用其缓存机制晋升网站性能次要用到的技术为 Service Worker + Cache Api 前端多级缓存模型当浏览器想要获取近程的数据时,咱们并不会立刻出发(发送申请),在计算机领域,很多性能问题都会通过减少缓存来解决,前端也不例外。和许多后端服务一样,前端缓存也是多级的。 本地读取阶段,这个阶段咱们不会发动任何 HTTP 申请,只在本地读取数据作为响应HTTP request 阶段,这个阶段咱们发动了 HTTP 申请,然而数据仍然是从本地读取。目前为止,咱们可能还没有收回一个真正的申请。这也意味着,在缓存查看阶段咱们就会有很多机会将后续的性能问题扼杀在摇篮之中真正申请阶段,如果很可怜本地没有任何无效数据,这时候才会发动真正的申请前端多级缓存具体流程图如下:

March 21, 2022 · 1 min · jiezi

Knative-Serving-之路由管理和-Ingress

Knative 默认会为每一个 Service 生成一个域名,并且 Istio Gateway 要根据域名判断当前的请求应该转发给哪个 Knative Service。Knative 默认使用的主域名是 example.com,这个域名是不能作为线上服务的。本文我首先介绍一下如何修改 默认主域名,然后再深入一层介绍如何添加自定义域名以及如何根据 path 关联到不同的 Knative Service。 Knative Serving 的默认域名 example.com首先需要部署一个 Knative Service,可以参考 Knative 初体验:Serving Hello World。如果你已经有了一个 Knative 集群,那么直接把下面的内容保存到 helloworld.yaml 文件中。然后执行一下 kubectl apply -f helloworld.yaml  即可把 hello 服务部署到 helloworld namespace 中。 ---apiVersion: v1kind: Namespacemetadata: name: helloworld---apiVersion: serving.knative.dev/v1alpha1kind: Servicemetadata: name: hello namespace: helloworldspec: template: metadata: labels: app: hello annotations: autoscaling.knative.dev/target: "10" spec: containers: - image: registry.cn-hangzhou.aliyuncs.com/knative-sample/simple-app:132e07c14c49 env: - name: TARGET value: "World!"现在我们来看一下 Knative Service 自动生成的域名配置: ...

August 19, 2019 · 2 min · jiezi

玩转运维编排服务的权限Assume-RolePass-Role

什么是运维编排服务?阿里云运维编排服务(Operation Orchestration Service,简称OOS)是云上的自动化运维平台,提供运维任务的管理和执行。典型使用场景包括:事件驱动运维,批量操作运维,定时运维任务,跨地域运维等,OOS为重要运维场景提供审批,通知等功能。OOS帮用户实现标准化运维任务,从而实践运维即代码(Operations as Code)的先进理念。OOS支持跨产品使用,用户可以使用OOS管理ECS、RDS、SLB、VPC等云产品。 用大白话讲,就是阿里云的用户编写一个包含运维逻辑的模板,提交给OOS,然后OOS在云端,以用户的身份执行这个模板里面的运维逻辑。 阿里云OOS的公共模板已经开源在github 更多的信息请参考之前的文章 阿里云重磅发布云上自动化利器——运维编排OOS或参考运维编排官方帮助文档 权限相关的三个基本概念:用户,授权策略,角色先说用户,用户就是账号,这个概念很好理解。用户分为两类:主账号和子账号。 阿里云上的用户在最开始使用阿里云服务的时候,都会注册一个云账号,这个注册生成的账号就叫做主账号,可以理解为root账号,拥有最高的权限。 接下来,拥有主账号的用户如果想要创建子账号,就需要开通一个叫做RAM (Resource Access Management)的云产品。开通RAM之后,就可以在RAM控制台上,创建子账户。 创建子账户的目的,是为了限制子账户的权限,避免root账户被滥用和泄露的风险。 那么,子账户的权限如何限制呢?这儿就要先引入授权策略的概念。授权策略表示一组权限的集合,比如针对ECS的创建销毁启停的权限集合。授权策略也分两类,一类是阿里云内置的系统授权策略,另一类是用户自定义的授权策略。OOS服务默认包含了两个系统授权策略,一个叫做OOSFullAccess,相当于拥有了OOS这个服务的全部权限,另一个叫做OOSReadOnlyAccess,相当于只能读取OOS的模板和历史执行的数据,不能做修改模板也不能启动执行。 账号和授权策略之间,是多对多的关系。主账户或者管理员,可以决定,每一个子账号拥有哪些权限策略。比如,给user1分配OOSFullAccess权限策略,而给user2分配OOSReadOnlyAccess权限策略。 那么,什么是角色(Role)呢?角色是一种虚拟的用户,它不对应任何可以控制台登录的账户,但主账号或管理员可以创建Role并给Role像普通用户一样赋予授权策略。那么谁来使用这些虚拟用户(Role)呢?典型的使用场景就是云产品。主账户或者管理员,可以为角色配置“信任关系”,也就是可以决定哪些云产品能够使用这些虚拟账户。 权限问题1:Assume RoleOOS的核心功能,是要按照用户的身份去执行用户提前编写好的模板。这里面就涉及到了一个基本问题:OOS作为一个云产品,用什么样的身份去执行用户编写的模板呢? 如果单从简单实现的角度,而不考虑安全,那么阿里云直接给OOS赋予一个超级权限就可以了,反正是“自家”产品。这样做可以吗?我们认为不可以。用户需要有能力对OOS进行限制,需要可以决定OOS可以操作哪些资源,不可以操作哪些资源。也就是说,OOS在执行用户的模板的时候,一定是“扮演”了某个用户可控的身份。 相信聪明的读者已经想到了,这里用到了前面介绍的角色(Role),以及Assume Role这个功能。Assume Role,就是云产品,扮演用户的某个角色。用户,需要进行两个操作步骤:第一步,就是创建一个角色(Role),并且信任运维编排服务,如下图所示: 第二步,就是给这个角色赋予权限策略,用户如果希望OOS来管理ECS,就可以考虑把AliyunECSFullAccess这个权限策略赋予给这个角色。 那么,接下来,OOS是怎么Assume Role执行模板的呢? 在OOS模板里面,可以通过“RamRole”这个字段指定一个角色,OOS在后台执行这个模板的时候,就会尝试扮演这个Role。如果模板没有指定,那么OOS就会尝试扮演默认的角色,也就是OOSServiceRole这个角色。 FormatVersion: OOS-2019-06-01RamRole: 'OOSServiceRole'Tasks:- Name: describeRunningInstancesByTag Action: ACS::ExecuteApi Description: Views running ECS instances by specifying tag. Properties: Service: ECS API: DescribeInstances Parameters: Status: Running Tags: - Key: 'tagkey' Value: 'tagvalue'如果用户没有预先创建相应的角色,那么在执行的时候,就会报错,错误消息类似于“:Assumes role failed. Code: EntityNotExist.Role, msg: The role not exists: acs::111111:role/OOSServiceRole.” ...

July 10, 2019 · 1 min · jiezi

探究K8S-Service内部iptables路由规则

前言 在K8S集群内部,应用常使用Service互访,那么,了解Service技术优缺点将有利于应用规划与部署,鉴于此,本文将通过简单案例以探索Cluster-Ip类型Service服务的利弊。 为便于讲解,我们先创建如下应用及Service服务: # kubectl run --image=nginx nginx-web-1 --image-pull-policy='IfNotPresent'# kubectl expose deployment nginx-web-1 --port=80 --target-port=80Service探索 作者的K8S环境是1.9版本,其Service内部服务由Kube-Proxy1提供,且默认用iptables技术实现,故本文探索K8S集群Service技术,即研究iptables在K8S上的技术实现。 Service Route(服务路由) 如下可知,通过nginx-web-1服务可实际访问到后端pod: # nginx pod ip地址:# kubectl describe pod nginx-web-1-fb8d45f5f-dcbtt | grep "IP"IP: 10.129.1.22# Service服务,通过172.30.132.253:80则实际访问到10.129.1.22:80# kubectl describe svc nginx-web-1 ...Type: ClusterIPIP: 172.30.132.253Port: <unset> 80/TCPTargetPort: 80/TCPEndpoints: 10.129.1.22:80Session Affinity: None...# 重置nginx web页面:# kubectl exec -it nginx-web-1-fb8d45f5f-dcbtt -- \ sh -c "echo hello>/usr/share/nginx/html/index.html"# curl 10.129.1.22hello# curl 172.30.132.253hello Service服务分配的CLUSTER-IP以及监听的端口均虚拟的,即在K8S集群节点上执行ip a与netstat -an命令均无法找到,其实际上,IP与Port是由iptables配置在每K8S节点上的。在节点上执行如下命令可找到此Service相关的iptables配置,简析如下: 当通过Service服务IP:172.30.132.253:80访问时,匹配第3条规则链(KUBE-SERVICES)后,跳转到第4条子链(KUBE-SVC-...)上;第4条子链做简单注释后,继而跳转到第1、2规则链(KUBE-SEP-...)上;当源Pod通过Service访问自身时,匹配第1条规则,继而跳转到KUBE-MARK-MASQ链中;匹配到第2条规则,此时通过DNAT被重定向到后端Pod:108.29.1.22:80。# iptables-save | grep nginx-web-1-A KUBE-SEP-UWNFTKZFYWNNNTK7 -s 10.129.1.22/32 -m comment --comment "demo/nginx-web-1:" \ -j KUBE-MARK-MASQ-A KUBE-SEP-UWNFTKZFYWNNNTK7 -p tcp -m comment --comment "demo/nginx-web-1:" \ -m tcp -j DNAT --to-destination 10.129.1.22:80-A KUBE-SERVICES -d 172.30.132.253/32 -p tcp -m comment \ --comment "demo/nginx-web-1: cluster IP" -m tcp --dport 80 -j KUBE-SVC-SNP24T7IBBNZDJ76-A KUBE-SVC-SNP24T7IBBNZDJ76 -m comment --comment "demo/nginx-web-1:" \ -j KUBE-SEP-UWNFTKZFYWNNNTK7 详细分析iptables规则,执行iptables-save命令可发现nat的PREROUTING与OUTPUT链中均有KUBE-SERVICES规则链,且处于第一顺位。 ...

June 28, 2019 · 3 min · jiezi

如何使用curl访问k8s的apiserver

使用TOKEN授权访问api-server在k8s运维场景中比较常见, apiserver有三种级别的客户端认证方式 1,HTTPS证书认证:基于CA根证书签名的双向数字证书认证方式 2,HTTP Token认证:通过一个Token来识别合法用户 3,HTTP Base认证:通过用户名+密码的认证方式 通常的运维场景使用第二种Token较为方便Token的权限是关联service account, # kubectl describe secrets admin-token-2q28f -n kube-systemName: admin-token-2q28fNamespace: kube-systemLabels: <none>Annotations: kubernetes.io/service-account.name: admin kubernetes.io/service-account.uid: 93316ffa-7545-11e9-b617-00163e06992dType: kubernetes.io/service-account-tokenData====ca.crt: 1419 bytesnamespace: 11 bytestoken: eyJhbGciOiJ******Service Account 的权限来自Clusterrolebinding-->ClusterRole # kubectl describe serviceaccount admin -n kube-systemName: adminNamespace: kube-systemLabels: <none>Annotations: kubectl.kubernetes.io/last-applied-configuration: {"apiVersion":"v1","kind":"ServiceAccount","metadata":{"annotations":{},"name":"admin","namespace":"kube-system"}}Image pull secrets: <none>Mountable secrets: admin-token-2q28fTokens: admin-token-2q28fEvents: <none>通过clusterrolebinding 可以拿到ClusterRole对应的rolename # kubectl get clusterrolebinding admin -o yamlapiVersion: rbac.authorization.k8s.io/v1kind: ClusterRoleBindingmetadata: annotations: kubectl.kubernetes.io/last-applied-configuration: | {"apiVersion":"rbac.authorization.k8s.io/v1beta1","kind":"ClusterRoleBinding","metadata":{"annotations":{},"name":"admin"},"roleRef":{"apiGroup":"rbac.authorization.k8s.io","kind":"ClusterRole","name":"cluster-admin"},"subjects":[{"kind":"ServiceAccount","name":"admin","namespace":"kube-system"}]} creationTimestamp: 2019-05-13T06:08:49Z name: admin resourceVersion: "1523" selfLink: /apis/rbac.authorization.k8s.io/v1/clusterrolebindings/admin uid: 93356439-7545-11e9-b617-00163e06992droleRef: apiGroup: rbac.authorization.k8s.io kind: ClusterRole name: cluster-adminsubjects:- kind: ServiceAccount name: admin namespace: kube-system这个role是什么权限? ...

June 24, 2019 · 2 min · jiezi

Knative-初体验Eventing-Hello-World

作者 | 阿里云智能事业群高级开发工程师 元毅 基于事件驱动是Serveless的核心功能之一,通过事件驱动服务,满足了用户按需付费(Pay-as-you-go)的需求。在之前的文章中我们介绍过 Knative Eventing 由事件源、事件处理模型和事件消费 3 个主要部分构成,那么事件如何通过这 3 个组件产生、处理以及消费呢? 本文通过 Kubernetes Event Source 示例介绍一下 Knative Eventing 中如何获取事件,并且将事件传递给 Serving 进行消费。其中事件处理基于 Broker/Trigger 模型。 []() 背景知识 先了解一下Broker/Trigger 事件处理模型。从 v0.5 开始,Knative Eventing 定义 Broker 和 Trigger 对象,从而能方便的对事件进行过滤。 Broker 提供一个事件集,可以通过属性选择该事件集。它负责接收事件并将其转发给由一个或多个匹配 Trigger 定义的订阅者。Trigger 描述基于事件属性的过滤器。同时可以根据需要创建多个 Trigger。Broker/Tiggger 模型流程处理如图所示: []() 前置准备 Knative 版本 >= 0.5安装完成 Knative Serving安装完成 Knative Eventing[]() 操作步骤先看一下 Kubernetes Event Source 示例处理流程,如图所示: 接下来介绍一下各个阶段如何进行操作处理。 []() 创建 Service Account 为 ApiServerSource 创建 Service Account, 用于授权 ApiServerSource 获取 Kubernetes Events 。 ...

June 18, 2019 · 3 min · jiezi

基于ExternalDNS的多集群Service-DNS实践

概述External-DNS提供了编程方式管理Kubernetes Service资源的DNS的功能,类似于容器服务kubernetes federation v2实践一:基于External-DNS的多集群Ingress DNS实践,External-DNS会监听LoadBalancer类型的Service,然后与云厂商打通,按照可用区、region和全局三个维度生成独自的域名解析记录,便于服务间调用引导流量。本文简单介绍如何在阿里云容器平台上使用External-DNS管理多集群Service DNS。 环境准备参考容器服务kubernetes federation v2实践一:基于External-DNS的多集群Ingress DNS实践完成【联邦集群准备】、【配置RAM信息】和【部署External-DNS】部分,并配置好kubeConfig,如下所示: kubectl config get-contextsCURRENT NAME CLUSTER AUTHINFO NAMESPACE* cluster1 cluster1 kubernetes-admin1 cluster2 cluster2 kubernetes-admin2资源部署创建FederatedDeployment和FederatedServiceyaml如下,注意FederatedService类型为LoadBalancer apiVersion: v1kind: Namespacemetadata: name: test-namespace---apiVersion: types.federation.k8s.io/v1alpha1kind: FederatedNamespacemetadata: name: test-namespace namespace: test-namespacespec: placement: clusterNames: - cluster1 - cluster2---apiVersion: types.federation.k8s.io/v1alpha1kind: FederatedDeploymentmetadata: name: test-deployment namespace: test-namespacespec: template: metadata: labels: app: nginx spec: replicas: 2 selector: matchLabels: app: nginx template: metadata: labels: app: nginx spec: containers: - image: nginx name: nginx placement: clusterNames: - cluster1 - cluster2---apiVersion: types.federation.k8s.io/v1alpha1kind: FederatedServicemetadata: name: test-service namespace: test-namespacespec: template: spec: selector: app: nginx type: LoadBalancer ports: - name: http port: 80 placement: clusterNames: - cluster2 - cluster1查看各个集群Service详情: ...

June 14, 2019 · 2 min · jiezi

Knative-初体验Serving-Hello-World

通过前面两章的学习你已经掌握了很多 Knative 的理论知识,基于这些知识你应该对 Knative 是谁、它来自哪里以及它要做什么有了一定的认识。可是即便如此你可能还是会有一种犹抱琵琶半遮面,看不清真容的感觉,这就好比红娘拿姑娘的 100 张生活照给你看也不如你亲自去见一面。按常理出牌,一般到这个阶段就该 Hello World 出场了。本篇文章就通过一个 Hello World 和 Knative 来一个“约会”,让你一睹 Knative 这位白富美的真容。 安装 KnativeKnative 社区提供的安装步骤见这里,整个流程大概包含如下三个部分: 准备 kubernetes 环境(你可以在阿里云容器服务中快速创建一个 kubernetes 集群 )安装istio安装 Knative组件虽然看起来只有三步,但是每一步其实都需要手动做大量的工作,执行一堆命令。另外社区文档提供的 yaml 文件默认使用了大量的 gcr.io 镜像,目前国内无法拉取 gcr.io 镜像。所以这些 yaml 文件在国内不能直接使用,至少需要手动同步 30 多个镜像才行。 不过别着急,阿里云容器服务的应用目录已经有 Knative 的安装包,现在只需要在阿里云容器服务上面点击几下鼠标就能轻轻松松搭建一个 Knative 集群 O ^ ~ ^ O O ^ ~ ^ O O ^ ~ ^ O 创建 Kubernetes 集群阿里云容器服务可以通过管理控制台非常方便地创建 Kubernetes 集群。具体过程可以参考创建Kubernetes集群。容器服务提供了专有集群和托管集群两种类型,如果不知道该怎么选择建议你直接选择托管版的 Kubernetes 集群。托管版无需你自己承担 Kubernetes Master 组件的管理和运维,你只需要提供 Node 节点即可。 ...

June 11, 2019 · 6 min · jiezi

谈谈应用-ANR-之-Service-超时

1. 核心源码关键类路径(/frameworks/base/)ActiveServices.javaservices/core/java/com/android/server/am/ActiveServices.javaActivityManagerService.javaservices/core/java/com/android/server/am/ActivityManagerService.javaAppErrors.javaservices/core/java/com/android/server/am/AppErrors.java2. ANR 基础认知2.1 ANR 是什么?ANR(Application Not Responding),应用程序无响应,简单一个定义,却涵盖了很多 Android 系统的设计思想。 首先,ANR 属于应用程序的范畴,这不同于 SNR(System Not Respoding),SNR 反映的问题是系统进程(system_server)失去了响应能力,而 ANR 明确将问题圈定在应用程序。SNR 由 Watchdog 机制保证,ANR 由消息处理机制保证,Android 在系统层实现了一套精密的机制来发现 ANR,核心原理是消息调度和超时处理。 其次,ANR 机制主体实现在系统层。所有与 ANR 相关的消息,都会经过系统进程(system_server)调度,然后派发到应用进程完成对消息的实际处理,同时,系统进程设计了不同的超时限制来跟踪消息的处理。一旦应用程序处理消息不当,超时限制就起作用了,它收集一些系统状态,例如:CPU/IO使用情况、进程函数调用栈,并且报告用户有进程无响应了(ANR 对话框)。 然后,ANR 问题本质是一个性能问题。ANR 机制实际上对应用程序主线程的限制,要求主线程在限定的时间内处理完一些最常见的操作(启动服务、处理广播、处理输入),如果处理超时,则认为主线程已经失去了响应其他操作的能力。主线程中的耗时操作,例如:密集CPU运算、大量IO、复杂界面布局等,都会降低应用程序的响应能力。 最后,部分 ANR 问题是很难分析的,有时候由于系统底层的一些影响,导致消息调度失败,出现问题的场景又难以复现。这类 ANR 问题往往需要花费大量的时间去了解系统的一些行为,超出了 ANR 机制本身的范畴。 2.2 ANR 机制分析一些初级的 ANR 问题,只需要简单理解最终输出的日志即可,但对于一些由系统问题(例如:CPU 负载过高、进程卡死)引发的 ANR,就需要对整个 ANR 机制有所了解,才能定位出问题的原因。 ANR 机制可以分为两部分:       ✎  ANR 的监测:Android 对于不同的 ANR 类型(Broadcast,Service,InputEvent)都有一套监测机制。       ✎  ANR 的报告:在监测到 ANR 以后,需要显示 ANR 对话框、输出日志(发生 ANR 时的进程函数调用栈、CPU 使用情况等)。 ...

April 29, 2019 · 7 min · jiezi

提升不止一点点,Dubbo 3.0 预览版详细解读

Dubbo 自 2011 年 10 月 27 日开源后,已被许多非阿里系的公司使用,其中既有当当网、网易考拉等互联网公司,也不乏中国人寿、青岛海尔等大型传统企业。更多用户信息,可以访问Dubbo @GitHub,issue#1012: Wanted: who's using dubbo。 自去年 12 月开始,Dubbo 3.0 便已正式进入开发阶段,并备受社区和广大 Dubbo 用户的关注,本文将为您详细解读 3.0 预览版的新特性和新功能。 下面先解答一下两个有意思的与 Dubbo 相关的疑问。 为什么 Dubbo 一开源就是 2.0 版本?之前是否存在 1.0 版本?笔者曾做过 Dubbo 协议的适配兼容,Dubbo 确实存在过 1.x 版本,而且从协议设计和模型设计上都与 2.0 的开源版本协议是完全不一样的。下图是关于 Dubbo 的发展路径: 阿里内部正在使用 Dubbo 开源版本吗?是的,非常确定,当前开源版本的 Dubbo 在阿里巴巴被广泛使用,而阿里的电商核心部门是用的 HSF2.2 版本,这个版本是兼容了 Dubbo 使用方式和 Remoting 协议。当然,我们现在正在做 HSF2.2 的升级,直接依赖开源版本的 Dubbo 来做内核的统一。所以,Dubbo 是得到大规模线上系统验证的分布式服务框架,这一点毋容置疑。 Dubbo 3.0 预览版的要点Dubbo 3.0 在设计和功能上的新增支持和改进,主要是以下四方面: Dubbo 内核之 Filter 链的异步化这里要指出的是,3.0 中规划的异步去阻塞和 2.7 中提供的异步是两个层面的特性。2.7 中的异步是建立在传统 RPC 中 request – response 会话模型上的,而 3.0 中的异步将会从通讯协议层面由下向上构建,关注的是跨进程、全链路的异步问题。通过底层协议开始支持 streaming 方式,不单单可以支持多种会话模型,还可以在协议层面开始支持反压、限流等特性,使得整个分布式体系更具有弹性。综上所述,2.7 关注的异步更局限在点对点的异步(一个 consumer 调用一个 provider),3.0 关注的异步化,宽度上则关注整个调用链上的异步,高度上则向上又可以包装成 Rx 的编程模型。有趣的是,Spring 5.0 发布了对 Flux 的支持,随后开始解决跨进程的异步问题。 ...

April 22, 2019 · 6 min · jiezi

Kubernetes Ingress 日志分析与监控的最佳实践

Ingress 主要提供 HTTP 层(7 层)路由功能,是目前 K8s 中 HTTP/HTTPS 服务的主流暴露方式。为简化广大用户对于 Ingress 日志分析与监控的门槛,阿里云容器服务和日志服务将 Ingress 日志打通,只需要应用一个 yaml 资源即可完成日志采集、分析、可视化等一整套 Ingress 日志方案的部署。前言目前 Kubernetes(K8s)已经真正地占领了容器编排市场,是默认的云无关计算抽象,越来越多的企业开始将服务构建在K8s集群上。在 K8s 中,组件通过 Service 对外暴露服务,常见的包括 NodePort、LoadBalancer、Ingress 等。其中 Ingress 主要提供 HTTP 层(7 层)路由功能,相比 TCP(4 层)的负载均衡具备非常多的优势(路由规则更加灵活、支持金丝雀、蓝绿、A/B Test 发布模式、SSL 支持、日志、监控、支持自定义扩展等),是目前 K8s 中 HTTP/HTTPS 服务的主流暴露方式。Ingress 简介K8s 中 Ingress 只是一种 API 资源的声明,具体的实现需要安装对应的 Ingress Controller,由 Ingress Controller 接管 Ingress 定义,将流量转发到对应的 Service。目前 Ingress Controller 的实现有非常多种(具体可以参考 Ingress Controller官方文档),比较流行的有 Nginx、Traefik、Istio、Kong 等,在国内接受度最高的是 Nginx Ingress Controller。日志与监控日志和监控是所有 Ingress Controller 都会提供的基础功能,日志一般包括访问日志(Access Log)、控制日志(Controller Log)和错误日志(Error Log),监控主要从日志以及 Controller 中提取部分 Metric 信息。这些数据中访问日志的量级最大、信息最多、价值也最高,一般7层的访问日志包括:URL、源 IP、UserAgent、状态码、入流量、出流量、响应时间等,对于 Ingress Controller 这种转发型的日志,还包括转发的 Service 名、Service 响应时间等额外信息。从这些信息中,我们能够分析出非常多的信息,例如:网站访问的 PV、UV;访问的地域分布、设备端分布;网站访问的错误比例;后端服务的响应延迟;不同 URL 访问分布。我们的开发、运维、运营、安全等人员可以基于这些信息完成各自的需求,例如:新老版本发布前后的数据指标对比;网站质量监控、集群状态监控;恶意攻击检测、反作弊;网站访问量统计、广告转化率统计。然而手动搭建、运维一整套的 Ingress 日志分析与监控系统非常复杂,系统所需要的模块有:部署日志采集 Agent 并配置采集、解析规则;由于 K8s 集群中,访问量相对较大,因此需要搭建一个缓冲队列,例如 Redis、Kafka 等;部署实时数据分析引擎,例如 Elastic Search、clickhouse 等;部署可视化组件并搭建报表,例如 grafana、kibana 等;部署告警模块并配置告警规则,例如 ElastAlert、alertmanager 等。阿里云日志服务Ingress解决方案为简化广大用户对于 Ingress 日志分析与监控的门槛,阿里云容器服务和日志服务将 Ingress 日志打通(官方文档https://help.aliyun.com/document_detail/86532.html)),只需要应用一个 yaml 资源即可完成日志采集、分析、可视化等一整套 Ingress 日志方案的部署。Ingress 可视化分析日志服务默认为 Ingress 创建 5 个报表,分别是:Ingress 概览、Ingress 访问中心、Ingress 监控中心、Ingress 蓝绿发布监控中心、Ingress 异常检测中心。不同角色的人员可根据需求使用不同的报表,同时每个报表均提供筛选框用于筛选特定的 Service、URL、状态码等。所有的报表均基于日志服务提供的基础可视化组件实现,可根据公司实际场景进行定制化调整。Ingress 概览Ingress 概览报表主要展示当前 Ingress 的整体状态,主要包括以下几类信息:整体架构状态(1 天),包括:PV、UV、流量、响应延迟、移动端占比、错误比例等;网站实时状态(1 分钟),包括:PV、UV、成功率、5XX 比例、平均延迟、P95/P99 延迟等;用户请求类信息(1 天),包括:1天/7天访问PV对比、访问地域分布、TOP访问省份/城市、移动端占比、Android/IOS 占比等;TOPURL 统计(1 小时),包括:访问 TOP10、延迟 TOP10、5XX 错误 TOP10、404 错误 TOP10。Ingress 访问中心Ingress 访问中心主要侧重于用于访问请求相关的统计信息,一般用于运营分析,包括:当日 UV/PV、UV/PV 分布、UV/PV 趋势、TOP 访问省份/城市、TOP 访问浏览器、TOP 访问IP、移动端占比、Android/IOS 占比等。Ingress 监控中心Ingress 监控中心主要侧重于网站实时监控数据,一般用于实时监控与告警,包括:请求成功率、错误比例、5XX 比例、请求未转发比例、平均延迟、P95/P99/P9999 延迟、状态码分布、Ingress 压力分布、Service 访问 TOP10、Service 错误 TOP10、Service 延迟 TOP10、Service 流量 TOP10 等。Ingress 蓝绿发布监控中心Ingress 蓝绿发布监控中心主要用于版本发布时的实时监控与对比(版本前后对比以及蓝绿版本当前对比),以便在服务发布时快速检测异常并进行回滚。在该报表中需要选择进行对比的蓝绿版本(ServiceA 和 ServiceB),报表将根据选择动态显示蓝绿版本相关指标,包括:PV、5XX 比例、成功率、平均延迟、P95/P99/P9999 延迟、流量等。Ingress 异常检测中心Ingress 异常检测中心基于日志服务提供的机器学习算法,通过多种时序分析算法从 Ingress 的指标中自动检测异常点,提高问题发现的效率。实时监控与告警Ingress 作为 K8s 网站请求的主要入口,实时监控与告警是必不可少的 Ops 手段之一。在日志服务上,基于上述的报表,只需 3 个简单的步骤即可完成告警的创建。下述示例为 Ingress 配置 5XX 比例的告警,告警每 5 分钟执行一次,当 5XX 比例超过 1% 时触发。除了通用的告警功能外,日志服务还额外支持:多维度数据关联,即通过多组 SQL 结果交叉判断进行告警,增加告警准确度;除支持短信、语音、通知中心、email 外,还支持钉钉机器人通知、自定义 WebHook 扩展;告警的记录也以日志的形式记录,可以实现对告警失败进行告警的双保险。订阅报告日志服务除支持通过告警方式通知外,还支持报表订阅功能,可使用该功能将报表定期渲染成图片并通过邮件、钉钉群等方式发送。例如每天早上 10 点向运营群中发送昨日网站访问情况、每周发送报告到邮件组中存档、新版本发布时每 5 分钟发送一次监控报表…自定义分析如果容器服务 Kubernetes 版提供的默认报表无法满足你的分析需求,可以直接使用日志服务 SQL、仪表盘等功能进行自定义的分析和可视化。尝鲜为了让大家可以体验 Kubernetes 审计日志功能,我们特别开通了体验中心,大家可以通过 https://promotion.aliyun.com/ntms/act/logdoclist.html 进入,该页面提供了非常多和 Kubernetes相关的报表。参考文档[1]https://www.aliyun.com/product/sls[2]https://www.aliyun.com/product/kubernetes[3]https://help.aliyun.com/document_detail/86532.html[4]https://help.aliyun.com/document_detail/48162.html[5]https://help.aliyun.com/document_detail/107758.html[6]https://kubernetes.io/docs/concepts/services-networking/ingress/[7]https://kubernetes.io/docs/concepts/services-networking/ingress-controllers/本文作者:jessie筱姜阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

March 27, 2019 · 1 min · jiezi

Kubernetes Ingress 日志分析与监控的最佳实践

摘要: Ingress主要提供HTTP层(7层)路由功能,是目前K8s中HTTP/HTTPS服务的主流暴露方式。为简化广大用户对于Ingress日志分析与监控的门槛,阿里云容器服务和日志服务将Ingress日志打通,只需要应用一个yaml资源即可完成日志采集、分析、可视化等一整套Ingress日志方案的部署。前言目前Kubernetes(K8s)已经真正地占领了容器编排市场,是默认的云无关计算抽象,越来越多的企业开始将服务构建在K8s集群上。在K8s中,组件通过Service对外暴露服务,常见的包括NodePort、LoadBalancer、Ingress等。其中Ingress主要提供HTTP层(7层)路由功能,相比TCP(4层)的负载均衡具备非常多的优势(路由规则更加灵活、支持金丝雀、蓝绿、A/B Test发布模式、SSL支持、日志、监控、支持自定义扩展等),是目前K8s中HTTP/HTTPS服务的主流暴露方式。Ingress简介K8s中Ingress只是一种API资源的声明,具体的实现需要安装对应的Ingress Controller,由Ingress Controller接管Ingress定义,将流量转发到对应的Service。目前Ingress Controller的实现有非常多种(具体可以参考Ingress Controller官方文档),比较流行的有Nginx、Traefik、Istio、Kong等,在国内接受度最高的是Nginx Ingress Controller。日志与监控日志和监控是所有Ingress Controller都会提供的基础功能,日志一般包括访问日志(Access Log)、控制日志(Controller Log)和错误日志(Error Log),监控主要从日志以及Controller中提取部分Metric信息。这些数据中访问日志的量级最大、信息最多、价值也最高,一般7层的访问日志包括:URL、源IP、UserAgent、状态码、入流量、出流量、响应时间等,对于Ingress Controller这种转发型的日志,还包括转发的Service名、Service响应时间等额外信息。从这些信息中,我们能够分析出非常多的信息,例如:网站访问的PV、UV;访问的地域分布、设备端分布;网站访问的错误比例;后端服务的响应延迟;不同URL访问分布。我们的开发、运维、运营、安全等人员可以基于这些信息完成各自的需求,例如:新老版本发布前后的数据指标对比;网站质量监控、集群状态监控;恶意攻击检测、反作弊;网站访问量统计、广告转化率统计。然而手动搭建、运维一整套的Ingress日志分析与监控系统非常复杂,系统所需要的模块有:部署日志采集Agent并配置采集、解析规则;由于K8s集群中,访问量相对较大,因此需要搭建一个缓冲队列,例如Redis、Kafka等;部署实时数据分析引擎,例如Elastic Search、clickhouse等;部署可视化组件并搭建报表,例如grafana、kibana等;部署告警模块并配置告警规则,例如ElastAlert、alertmanager等。阿里云日志服务Ingress解决方案为简化广大用户对于Ingress日志分析与监控的门槛,阿里云容器服务和日志服务将Ingress日志打通(官方文档),只需要应用一个yaml资源即可完成日志采集、分析、可视化等一整套Ingress日志方案的部署。Ingress可视化分析日志服务默认为Ingress创建5个报表,分别是:Ingress概览、Ingress访问中心、Ingress监控中心、Ingress蓝绿发布监控中心、Ingress异常检测中心。不同角色的人员可根据需求使用不同的报表,同时每个报表均提供筛选框用于筛选特定的Service、URL、状态码等。所有的报表均基于日志服务提供的基础可视化组件实现,可根据公司实际场景进行定制化调整。Ingress概览Ingress概览报表主要展示当前Ingress的整体状态,主要包括以下几类信息:整体架构状态(1天),包括:PV、UV、流量、响应延迟、移动端占比、错误比例等;网站实时状态(1分钟),包括:PV、UV、成功率、5XX比例、平均延迟、P95/P99延迟等;用户请求类信息(1天),包括:1天/7天访问PV对比、访问地域分布、TOP访问省份/城市、移动端占比、Android/IOS占比等;TOPURL统计(1小时),包括:访问TOP10、延迟TOP10、5XX错误TOP10、404错误TOP10。Ingress访问中心Ingress访问中心主要侧重于用于访问请求相关的统计信息,一般用于运营分析,包括:当日UV/PV、UV/PV分布、UV/PV趋势、TOP访问省份/城市、TOP访问浏览器、TOP访问IP、移动端占比、Android/IOS占比等。Ingress监控中心Ingress监控中心主要侧重于网站实时监控数据,一般用于实时监控与告警,包括:请求成功率、错误比例、5XX比例、请求未转发比例、平均延迟、P95/P99/P9999延迟、状态码分布、Ingress压力分布、Service访问TOP10、Service错误TOP10、Service延迟TOP10、Service流量TOP10等。Ingress蓝绿发布监控中心Ingress蓝绿发布监控中心主要用于版本发布时的实时监控与对比(版本前后对比以及蓝绿版本当前对比),以便在服务发布时快速检测异常并进行回滚。在该报表中需要选择进行对比的蓝绿版本(ServiceA和ServiceB),报表将根据选择动态显示蓝绿版本相关指标,包括:PV、5XX比例、成功率、平均延迟、P95/P99/P9999延迟、流量等。Ingress异常检测中心Ingress异常检测中心基于日志服务提供的机器学习算法,通过多种时序分析算法从Ingress的指标中自动检测异常点,提高问题发现的效率。实时监控与告警Ingress作为K8s网站请求的主要入口,实时监控与告警是必不可少的Ops手段之一。在日志服务上,基于上述的报表,只需3个简单的步骤即可完成告警的创建。下述示例为Ingress配置5XX比例的告警,告警每5分钟执行一次,当5XX比例超过1%时触发。除了通用的告警功能外,日志服务还额外支持:多维度数据关联,即通过多组SQL结果交叉判断进行告警,增加告警准确度;除支持短信、语音、通知中心、email外,还支持钉钉机器人通知、自定义WebHook扩展;告警的记录也以日志的形式记录,可以实现对告警失败进行告警的双保险。订阅报告日志服务除支持通过告警方式通知外,还支持报表订阅功能,可使用该功能将报表定期渲染成图片并通过邮件、钉钉群等方式发送。例如每天早上10点向运营群中发送昨日网站访问情况、每周发送报告到邮件组中存档、新版本发布时每5分钟发送一次监控报表…自定义分析如果容器服务Kubernetes版提供的默认报表无法满足你的分析需求,可以直接使用日志服务SQL、仪表盘等功能进行自定义的分析和可视化。尝鲜为了让大家可以体验Kubernetes审计日志功能,我们特别开通了体验中心,大家可以通过 https://promotion.aliyun.com/ntms/act/logdoclist.html 进入,该页面提供了非常多和Kubernetes相关的报表。参考文档阿里云日志服务阿里云容器服务Kubernetes版Ingress日志分析与监控告警配置订阅报表Ingress官方文档Ingress Controller官方文档一站式开发者服务,海量学习资源0元起!阿里热门开源项目、机器学习干货、开发者课程/工具、小微项目、移动研发等海量资源;更有开发者福利Kindle、技术图书幸运抽奖,100%中–》https://www.aliyun.com/acts/product-section-2019/developer?utm_content=g_1000047140本文作者:元乙阅读原文本文为云栖社区原创内容,未经允许不得转载。

March 19, 2019 · 1 min · jiezi

在阿里,我们如何管理测试环境

前言阿里的许多实践看似简单,背后却蕴涵着许多思考,譬如测试环境的管理。互联网产品的服务通常是由Web应用、中间件、数据库和许多后台业务程序组成的,一套运行环境就是一个自成一体的小生态。最基本的运行环境是线上环境,部署产品的正式发布版本,为用户提供持续可靠的服务。除此以外,还有许多不对外部用户开放的运行环境,用于产品团队日常的开发和验证,统称为测试环境。正式环境的稳定性,除去软件自身的质量因素,主要与运行的主机、网络等基础设施相关,而测试环境的稳定性则更多受到人为因素影响。由于频繁的版本变更,以及部署未经充分验证的代码,测试环境出故障的情况屡见不鲜。良好的代码提交习惯、适当的变更前检查有助于减少故障的发生,但无法彻底杜绝后患。增加多套测试环境副本能够有效控制故障的影响范围,然而企业的资源终归有限,降低测试环境成本和提高测试环境稳定性成为了矛盾的两面。在这个领域里,独具匠心的阿里研发效能团队设计了一种服务级复用的虚拟化技术,称为“特性环境”,其巧妙的思路令人赞叹。本文将围绕测试环境管理的话题,聊聊这种具有阿里特色的工作方式。测试环境管理的困局测试环境的用途很广泛,常见的测试环境譬如系统集成测试环境、用户验收测试环境、预发布测试环境、灰度测试环境等,它们体现了产品的交付生命周期,也间接反映出整个团队的组织结构。小作坊型产品团队的测试环境管理起来十分简单,每个工程师本地就能启动全套软件组件进行调试,倘若不放心,再加上一个公共的集成测试环境也就足够。随着产品规模扩大,本地启动所有服务组件逐渐变得既费时又费事,工程师们只能在本地运行一部分待调试的组件,然后利用公共测试环境上的其余组件组成完整系统。与此同时,团队规模的扩张,使得每个团队成员的职责进一步细分,新的子团队被划分出来,这意味着项目的沟通成本增加,公共测试环境的稳定性开始变得难以控制。在这个过程中,测试环境管理复杂性带来的影响,不仅体现在服务联调变得繁琐,更直接反映在交付流程和资源成本的变化上。在交付流程方面,一个显著的变化是测试环境种类增多。出于不同的用途和目的,工程师们设计出了各式各样的专用测试环境。这些测试环境的组合形成了各个企业独具特色的交付流程。下图展示了一种用于大型项目的复杂交付流程。从单独服务的角度来看,环境与环境之间是由流水线相连的,再加上自动化测试或手工审批操作组成关卡,实现环境之间的传递。通常越高级别环境的部署频率越低,因此相对稳定性也越高。与之相反,在级别较低的环境上,就随时可能存在新的部署,会打扰正在使用该环境的其他人。有时为了复现某些特殊的问题场景,一些开发者不得不直接登录到服务器上面去“搞事情”,进一步影响环境的稳定性和可用性。面对随时可能崩溃的测试环境,小企业会试着去“堵”:约束服务变更时间、设立严格的变更规范,大企业则善于用“疏”:增加测试环境副本,隔离故障影响范围。显然,不堪重负的测试环境一定越“堵”越“漏”,千年以前大禹治水的故事早就揭示了的道理,刻意的管控拯救不了脆弱的测试环境。近年来,DevOps文化的兴起,端到端解放了开发者的双手,这对于测试环境的管理而言却是一把双刃剑。一方面,DevOps鼓励开发人员参与运维,了解产品的完整生命周期,有助于减少不必要的低级运维事故;另一方面,DevOps让更多的手伸向测试环境,更多的变更、更多的Hotfix出现了。这些实践从全局来看利大于弊,然而并不能缓解测试环境的动荡。单纯的流程疏通同样拯救不了脆弱的测试环境。那么该投入的还得投入。将不同团队所用的低级别测试环境各自独立,此时每个团队看到的都是线性流水线,从整体上观察,则会程现出河流汇聚的形状。由此推广,理想情况下,每位开发者都应该得到独占且稳定的测试环境,各自不受干扰的完成工作。然而由于成本因素,现实中在团队内往往只能共享有限的测试资源,不同成员在测试环境相互干扰成为影响软件开发质量的隐患。增加测试环境副本数本质上是一种提高成本换取效率的方法,然而许多试图在成本和效率之间寻找最优平衡的探索者们,似乎都在同一条不归路上越行越远。由于客观的规模和体量,上述这些测试环境管理的麻烦事儿,阿里的产品团队都无法幸免。首先是测试环境种类的管理。在阿里内部,同样有十分丰富的测试环境区分。各种测试环境的命名与其作用息息相关,虽然业界有些常用的名称,但都未形成权威的标准。实际上,环境的名称只是一种形式,关键还在于各种测试环境应当分别适配于特定应用场景,且场景之间应当或多或少存在一些差异。这种差异有些在于运行的服务种类,譬如性能测试环境很可能只需要运行与压力测试相关的那部分访问量最大的关键业务服务,其他服务运行了也是浪费资源。有些差异在于接入数据的来源,譬如开发自测的环境的数据源与正式环境肯定不一样,这样测试使用的假数据就不会污染线上用户的请求;预发布环境(或用户验收测试环境)会用与正式环境一致的数据源(或正式数据源的拷贝),以便反映新功能在真实数据上运行的情况;自动化测试相关的环境会有单独的一套测试数据库,以避测试运行过程中受到其他人为操作的干扰。还有些差异在于使用者的不同,譬如灰度和预发布环境都使用正式的数据源,但灰度环境的使用者是一小撮真实的外部用户,而预发布环境的使用者都是内部人员。总之,没必要为一个不存在业务特殊性的测试场景专门发明一种测试环境。在集团层面,阿里对流水线形式的约束相对宽松。客观的讲,只有在一线的开发团队知道最适合团队的交付流程应该是什么样子。阿里的开发平台只是规范了一些推荐的流水线模板,开发者可在此基础上进行发挥。列举几个典型的模板例子:这里出现了几种外界不太常见的环境类型名称,稍后会详细介绍。其次是测试环境成本的管理。成本管理的问题十分棘手且十分值得深究。与测试环境相关的成本主要包括管理环境所需的“人工成本”和购买基础设施所需的“资产成本”。通过自动化以及自服务化的工具可以有效降低人工相关的成本,自动化又是个很大的话题,宜另起一篇文章讨论,此处暂且收住。资产购买成本的降低依赖技术的改良和进步(排除规模化采购带来的价格变化因素),而基础设施技术的发展史包括两大领域:硬件和软件。硬件发展带来的成本大幅下降,通常来自于新的材料、新的生产工艺、以及新的硬件设计思路;软件发展带来的基础设施成本大幅下降,目前看来,大多来自于虚拟化(即资源隔离复用)技术的突破。最早的虚拟化技术是虚拟机,早在20世纪50年代,IBM就开始利用这种硬件级的虚拟化方法获得成倍的资源利用率提升。虚拟机上的不同隔离环境之间各自运行完整操作系统,具有很好的隔离性,通用性强,但对于运行业务服务的场景,显得略为笨重。2000年后,KVM、XEN等开源项目使得硬件级虚拟化广泛普及。与此同时,另一种更轻量的虚拟化技术出现了,以OpenVZ、LXC为代表的早期容器技术,实现了建立于操作系统内核之上的运行环境虚拟化,减少了独立操作系统的资源消耗,以牺牲一定隔离性为代价,获得更高的资源利用率。之后诞生的Docker以其镜像封装和单进程容器的理念,将这种内核级虚拟化技术推上百万人追捧的高度。阿里紧随技术前进的步伐,早早的就用上了虚拟机和容器,在2017年双十一时,在线业务服务的容器化比例已经达到100%。然而,接下来的挑战是,基础设施资源利用率还能做得更高吗?甩掉了虚拟机的硬件指令转换和操作系统开销,运行在容器中的程序与普通程序之间只有一层薄薄的内核Namespace隔离,完全没有运行时性能损耗,虚拟化在这个方向上似乎已经发展到了极限。唯一的可能是,抛开通用场景,专注到测试环境管理的特定场景上,继续寻找突破。终于,阿里在这个领域里发现了新的宝藏:服务级虚拟化。所谓服务级虚拟化,本质上是基于消息路由的控制,实现集群中部分服务的复用。在服务级虚拟化方式下,许多外表庞大的独立测试环境实际只需要消耗极小的额外基础设施资源,即使给每个开发者配备一套专用的测试环境集群都不再是吹牛。具体来说,在阿里的交付流程上,包含两种特殊类型的测试环境:“公共基础环境”和“特性环境”,它们形成了具有阿里特色的测试环境使用方法。公共基础环境是一个全套的服务运行环境,它通常运行一个相对稳定的服务版本,也有些团队将始终部署各服务的最新版本的低级别环境(称为“日常环境”)作为公共基础环境。特性环境是这套方法中最有意思的地方,它是虚拟的环境。从表面上看,每个特性环境都是一套独立完整的测试环境,由一系列服务组成集群,而实际上,除了个别当前使用者想要测试的服务,其余服务都是通过路由系统和消息中间件虚拟出来的,指向公共基础环境的相应服务。由于在阿里通常的开发流程中,开发任务需要经过特性分支、发布分支和诸多相关环节最后发布上线,大多数环境都从发布分支部署,唯独这种开发者自用的虚拟环境部署来自代码特性分支的版本,故可称为“特性环境”(阿里内部叫“项目环境”)。举个具体例子,某交易系统的完整部署需要由鉴权服务、交易服务、订单服务、结算服务等十几种小系统以及相应的数据库、缓存池、消息中间件等组成,那么它的公共基础环境就是这样一套具备所有服务和周边组件的完整环境。假设此时有两套特性环境在运行,一套只启动了交易服务,另一套启动了交易服务、订单服务和结算服务。对于第一套特性环境的使用者而言,虽然除交易服务外的所有服务实际上都由公共基础环境代理,但在使用时就像是自己独占一整套完整环境:可以随意部署和更新环境中交易服务的版本,并对它进行调试,不用担心会影响其他用户。对于第二套特性环境的使用者,则可以对部署在该环境中的三个服务进行联调和验证,倘若在场景中使用到了鉴权服务,则由公共基础环境的鉴权服务来响应。咋看起来,这不就是动态修改域名对应的路由地址、或者消息主题对应的投递地址么?实事并没那么简单,因为不能为了某个特性环境而修改公共基础环境的路由,所以单靠正统路由机制只能实现单向目标控制,即特性环境里的服务主动发起调用能够正确路由,若请求的发起方在公共基础环境上,就无法知道该将请求发给哪个特性环境了。对于HTTP类型的请求甚至很难处理回调的情况,当处于公共基础环境的服务进行回调时,域名解析会将目标指向公共基础环境上的同名服务。如何才能实现数据双向的正确路由和投递呢?不妨先回到这个问题的本质上来:请求应该进入哪个特性环境,是与请求的发起人相关的。因此实现双向绑定的关键在于,识别请求发起人所处的特性环境和进行端到端的路由控制。这个过程与“灰度发布”很有几分相似,可采用类似的思路解决。得益于阿里在中间件领域的技术积累,和鹰眼等路由追踪工具的广泛使用,识别请求发起人和追溯回调链路都不算难事。如此一来,路由控制也就水到渠成了。当使用特性环境时,用户需要“加入”到该环境,这个操作会将用户标识(如IP地址或用户ID)与指定的特性环境关联起来,每个用户只能同时属于一个特性环境。当数据请求经过路由中间件(消息队列、消息网关、HTTP网关等),一旦识别到请求的发起人当前处在特性环境中,就会尝试把请求路由给该环境中的服务,若该环境没有与目标一致的服务,才路由或投递到公共基础环境上。特性环境并不是孤立存在的,它可以建立在容器技术之上,从而获得更大的灵活性。正如将容器建立在虚拟机之上得到基础设施获取的便利性一样,在特性环境中,通过容器快速而动态的部署服务,意味着用户可以随时向特性环境中增加一个需要修改或调试的服务,也可以将环境中的某个服务随时销毁,让公共基础环境的自动接替它。还有一个问题是服务集群调试。配合AoneFlow的特性分支工作方式,倘若将几个服务的不同特性分支部署到同一个特性环境,就可以进行多特性的即时联调,从而将特性环境用于集成测试。不过,即使特性环境的创建成本很低,毕竟服务是部署在测试集群上的。这意味着每次修改代码都需要等待流水线的构建和部署,节约了空间开销,却没有缩短时间开销。为了进一步的降低成本、提高效率,阿里团队又捣鼓出了一种开脑洞的玩法:将本地开发机加入特性环境。在集团内部,由于开发机和测试环境都使用内网IP地址,稍加变通其实不难将特定的测试环境请求直接路由到开发机。这意味着,在特性环境的用户即使访问一个实际来自公共基础环境的服务,在后续处理链路上的一部分服务也可以来自特性环境,甚至来自本地环境。现在,调试集群中的服务变得非常简单,再也不用等待漫长的流水线构建,就像整个测试环境都运行在本地一样。DIY体验特性环境觉得服务级虚拟化太小众,离普通开发者很远?实事并非如此,我们现在就可以动手DIY个体验版的特性环境来玩。阿里的特性环境实现了包括HTTP调用、RPC调用、消息队列、消息通知等各类常用服务通信方式的双向路由服务级虚拟化。要完成这样的功能齐全的测试环境有点费劲,从通用性角度考虑,咱不妨从最符合大众口味的HTTP协议开始,做个支持单向路由的简易款。为了便于管理环境,最好得有一个能跑容器的集群,在开源社区里,功能齐全的Kubernetes是个不错的选择。在Kubernetes中有些与路由控制有关的概念,它们都以资源对象的形式展现给用户。简单介绍一下,Namespace对象能隔离服务的路由域(与容器隔离使用的内核Namespace不是一个东西,勿混淆),Service对象用来指定服务的路由目标和名称,Deployment对象对应真实部署的服务。类型是ClusterIP(以及NodePort和LoadBalancer类型,暂且忽略它们)的Service对象可路由相同Namespace内的一个真实服务,类型是ExternalName的Service对象则可作为外部服务在当前Namespace的路由代理。这些资源对象的管理都可以使用YAML格式的文件来描述,大致了解完这些,就可以开始动工了。基础设施和Kubernetes集群搭建的过程略过,下面直接进正题。先得准备路由兜底的公共基础环境,这是一个全量测试环境,包括被测系统里的所有服务和其他基础设施。暂不考虑对外访问,公共基础环境中的所有服务相应的Service对象都可以使用ClusterIP类型,假设它们对应的Namespace名称为pub-base-env。这样一来,Kubernetes会为此环境中的每个服务自动赋予Namespace内可用的域名“服务名.svc.cluster”和集群全局域名“服务名.pub-base-env.svc.cluster”。有了兜底的保障后,就可以开始创建特性环境了,最简单的特性环境可以只包含一个真实服务(例如trade-service),其余服务全部用ExternalName类型的Service对象代理到公共基础环境上。假设它使用名称为feature-env-1的Namespace,其描述的YAML如下(省略了非关键字段的信息):kind: Namespacemetadata:name: feature-env-1*kind: Servicemetadata:name: trade-servicenamespace: feature-env-1spec:type: ClusterIP…*kind: Deploymentmetadata:name: trade-servicenamespace: feature-env-1spec:…*kind: Servicemetadata:name: order-servicenamespace: feature-env-1spec:type: ExternalNameexternalName: order-service.pub-base-env.svc.cluster…*kind: Service…注意其中的order-service服务,它在当前特性环境Namespace中可以使用局部域名order-service.svc.cluster访问,请求会路由到它配置的全局域名order-service.pub-base-env.svc.cluster,即公共基础环境的同名服务上处理。处于该Namespace中的其它服务感知不到这个差异,而是会觉得这个Namespace中部署了所有相关的服务。若在特性的开发过程中,开发者对order-service服务也进行了修改,此时应该将修改过的服务版本添加到环境里来。只需修改order-service的Service对象属性(使用Kubernetes的patch操作),将其改为ClusterIP类型,同时在当前Namespace中创建一个Deployment对象与之关联即可。由于修改Service对象只对相应Namespace(即相应的特性环境)内的服务有效,无法影响从公共基础环境回调的请求,因此路由是单向的。在这种情况下,特性环境中必须包含待测调用链路的入口服务和包含回调操作的服务。例如待测的特性是由界面操作发起的,提供用户界面的服务就是入口服务。即使该服务没有修改,也应该在特性环境中部署它的主线版本。通过这种机制也不难实现把集群服务局部替换成本地服务进行调试开发的功能,倘若集群和本地主机都在内网,将ExternalName类型的Service对象指向本地的IP地址和服务端口就可以了。否则需要为本地服务增加公网路由,通过动态域名解析来实现。与此同时,云效也正在逐步完善基于Kubernetes的特性环境解决方案,届时将会提供更加全面的路由隔离支持。值得一提的是,由于公有云的特殊性,在联调时将本地主机加入云上集群是个必须克服的难题。为此云效实现了通过隧道网络+kube-proxy自身路由能力,将本地局域网主机(无需公网IP地址)加入到不在同一内网Kubernetes集群进行联调的方式。其中的技术细节也将在近期的云效公众号向大家揭晓,敬请留意。小结当许多人还在等待,在虚拟机和容器之后,下一轮虚拟化技术的风口何时到来的时候,阿里已经给出了一种答案。创业者的心态让阿里人懂得,能省必须省。其实,限制创新的往往不是技术而是想象力,服务级虚拟化的理念突破了人们对环境副本的传统认知,以独特的角度化解了测试环境成本与稳定性的矛盾。作为一种颇具特色的技术载体,特性环境的价值不仅仅在于轻量的测试环境管理体验,更在于为每位开发人员带来流畅的工作方式,实则是“简约而不简单”。实践出真知,阿里巴巴云效平台致力于解决大型项目协作、敏捷高速迭代、海量代码托管、高效测试工具、分布式秒级构建、大规模集群部署发布等世界级业务和技术难题,为阿里巴巴集团内部、生态伙伴以及云上开发者服务。诚挚欢迎业界同行与我们探讨交流。相关阅读:在阿里,我们如何管理代码分支当kubernetes应用遇到阿里分批发布模式本文作者:云效鼓励师阅读原文本文为云栖社区原创内容,未经允许不得转载。

January 25, 2019 · 1 min · jiezi

开源 serverless 产品原理剖析(二) - Fission

摘要: Fission 是由私有云服务提供商领导开源的 serverless 产品,它借助 kubernetes 灵活强大的编排能力完成容器的管理调度工作,而将重心投入到 FaaS 功能的开发上,其发展目标是成为 AWS lambda 的开源替代品。背景本文是开源 serverless 产品原理剖析系列文章的第二篇,关于 serverless 背景知识的介绍可参考文章开源 serverless 产品原理剖析(一) - Kubeless,这里不再赘述。Fission 简介Fission 是由私有云服务提供商 Platform9 领导开源的 serverless 产品,它借助 kubernetes 灵活强大的编排能力完成容器的管理调度工作,而将重心投入到 FaaS 功能的开发上,其发展目标是成为 AWS lambda 的开源替代品。从 CNCF 视角,fission 属于 serverless 平台型产品。核心概念Fission 包含 Function、Environment 、Trigger 三个核心概念,其关系如下图所示:Function - 代表用特定语言编写的需要被执行的代码片段。Environment- 用于运行用户函数的特定语言环境。Trigger - 用于关联函数和事件源。如果把事件源比作生产者,函数比作执行者,那么触发器就是联系两者的桥梁。关键组件Fission 包含 Controller、Router、Executor 三个关键组件:Controller - 提供了针对 fission 资源的增删改查操作接口,包括 functions、triggers、environments、Kubernetes event watches 等。它是 fission CLI 的主要交互对象。Router - 函数访问入口,同时也实现了 HTTP 触发器。它负责将用户请求以及各种事件源产生的事件转发至目标函数。Executor - fission 包含 PoolManager 和 NewDeploy 两类执行器,它们控制着 fission 函数的生命周期。原理剖析本章节将从以下几个方面介绍 fission 的基本原理:函数执行器 - 它是理解 fission 工作原理的基础。Pod 特化 - 它是理解 fission 如何根据用户源码构建出可执行函数的关键。触发器 - 它是理解 fission 函数各种触发原理的入口。自动伸缩 - 它是理解 fission 如何根据负载动态调整函数个数的捷径。日志处理 - 它是理解 fission 如何处理各函数日志的有效手段。本文所做的调研基于kubeless 0.12.0和k8s 1.13。函数执行器CNCF 对函数生命周期的定义如下图所示,它描绘了函数构建、部署、运行的一般流程。要理解 fission,首先需要了解它是如何管理函数生命周期的。Fission 的函数执行器是其控制函数生命周期的关键组件。Fission 包含 PoolManager 和 NewDeploy 两类执行器,下面分别对两者进行介绍。PoolManagerPoolmgr 使用了池化技术,它通过为每个 environment 维持了一定数量的通用 pod 并在函数被触发时将 pod 特化,大大降低了函数的冷启动的时间。同时,poolmgr 会自动清理一段时间内未被访问的函数,减少闲置成本。该执行器的原理如下图所示。此时,函数的生命周期如下:使用 fission CLI 向 controller 发送请求,创建函数运行时需要的特定语言环境。例如,以下命令将创建一个 python 运行环境。fission environment create –name python –image fission/python-envPoolmgr 定期同步 environment 资源列表,参考 eagerPoolCreator。Poolmgr 遍历 environment 列表,使用 deployment 为每个 environment 创建一个通用 pod 池,参考 MakeGenericPool。使用 fission CLI 向 controller 发送创建函数的请求。此时,controller 只是将函数源码等信息持久化存储,并未真正构建好可执行函数。例如,以下命令将创建一个名为 hello 的函数,该函数选用已经创建好的 python 运行环境,源码来自 hello.py,执行器为 poolmgr。fission function create –name hello –env python –code hello.py –executortype poolmgrRouter 接收到触发函数执行的请求,加载目标函数相关信息。Router 向 executor 发送请求获取函数访问入口,参考 GetServiceForFunction。Poolmgr 从函数指定环境对应的通用 pod 池里随机选择一个 pod 作为函数执行的载体,这里通过更改 pod 的标签让其从 deployment 中“独立”出来,参考 _choosePod。K8s 发现 deployment 所管理 pod 的实际副本数少于目标副本数后会对 pod 进行补充,这样便实现了保持通用 pod 池中的 pod 个数的目的。特化处理被挑选出来的 pod,参考 specializePod。为特化后的 pod 创建 ClusterIP 类型的 service,参考 createSvc。将函数的 service 信息返回给 router,router 会将 serviceUrl 缓存以避免频繁向 executor 发送请求。Router 使用返回的 serviceUrl 访问函数。请求最终被路由至运行函数的 pod。如果该函数一段时间内未被访问会被自动清理,包括该函数的 pod 和 service,参考 idleObjectReaper。NewDeployPoolmgr 很好地平衡了函数的冷启动时间和闲置成本,但无法让函数根据度量指标自动伸缩。NewDeploy 执行器实现了函数 pod 的自动伸缩和负载均衡,该执行器的原理如下图所示。此时,函数的生命周期如下:使用 fission CLI 向 controller 发送请求,创建函数运行时需要的特定语言环境。使用 fission CLI 向 controller 发送创建函数的请求。例如,以下命令将创建一个名为 hello 的函数,该函数选用已经创建好的 python 运行环境,源码来自 hello.py,执行器为 newdeploy,目标副本数在 1 到 3 之间,目标 cpu 使用率是 50%。fission fn create –name hello –env python –code hello.py –executortype newdeploy –minscale 1 –maxscale 3 –targetcpu 50Newdeploy 会注册一个 funcController 持续监听针对 function 的 ADD、UPDATE、DELETE 事件,参考 initFuncController。Newdeploy 监听到了函数的 ADD 事件后,会根据 minscale 的取值判断是否立即为该函数创建相关资源。minscale > 0,则立即为该函数创建 service、deployment、HPA(deployment 管理的 pod 会特化)。minscale <= 0,延迟到函数被真正触发时创建。Router 接收到触发函数执行的请求,加载目标函数相关信息。Router 向 newdeploy 发送请求获取函数访问入口。如果函数所需资源已被创建,则直接返回访问入口。否则,创建好相关资源后再返回。Router 使用返回的 serviceUrl 访问函数。如果该函数一段时间内未被访问,函数的目标副本数会被调整成 minScale,但不会删除 service、deployment、HPA 等资源,参考 idleObjectReaper。执行器比较实际使用过程中,用户需要从延迟和闲置成本两个角度考虑选择何种类型的执行器。不同执行器的特点如下表所示。执行器类型最小副本数延迟闲置成本Newdeploy0高非常低 - pods 一段时间未被访问会被自动清理掉。Newdeploy>0低中等 - 每个函数始终会有一定数量的 pod 在运行。Poolmgr0低低 - 通用池中的 pod 会一直运行。小结Fission 将函数执行器的概念暴露给了用户,增加了产品的使用成本。实际上可以将 poolmgr 和 newdeploy 技术相结合,通过创建 deployment 将特化后的 pod 管理起来,这样可以很自然地利用 HPA 来实现对函数的自动伸缩。Pod 特化在介绍函数执行器时多次提到了 pod 特化,它是 fission 将环境容器变成函数容器的奥秘。Pod 特化的本质是通过向容器发送特化请求让其加载用户函数,其原理如下图所示。一个函数 pod 由下面两种容器组成:Fetcher - 下载用户函数并将其放置在共享 volume 里。不同语言环境使用了相同的 fetcher 镜像,fetcher 的工作原理可参考代码 fetcher.go。Env - 用户函数运行的载体。当它成功加载共享 volume 里的用户函数后,便可接收用户请求。具体步骤如下:容器 fetcher 接收到拉取用户函数的请求。Fetcher 从 K8s CRD 或 storagesvc 处获取用户函数。Fetcher 将函数文件放置在共享的 volume 里,如果文件被压缩还会负责解压。容器 env 接收到加载用户函数的命令。Env 从共享 volume 中加载 fetcher 为其准备好的用户函数。特化流程结束,容器 env 开始处理用户请求。触发器前面的章节介绍了 fission 函数的构建、加载和执行的逻辑,本章节主要介绍如何基于各种事件源触发 fission 函数的执行。CNCF 将函数的触发方式分成了如下图所示的几种类别,关于它们的详细介绍可参考链接 Function Invocation Types。对于 fission 函数,最简单的触发方式是使用 fission CLI,另外还支持通过各种触发器。下表展示了 fission 函数目前支持的触发方式以及它们所属的类别。触发方式类别fission CLISynchronous Req/RepHTTP TriggerSynchronous Req/RepTime TriggerJob (Master/Worker)Message Queue Trigger1. nats-streaming2. azure-storage-queue3. kafka | Async Message Queue || Kubernetes Watch Trigger | Async Message Queue |下图展示了 fission 函数部分触发方式的原理:HTTP trigger所有发往 fission 函数的请求都会由 router 转发,fission 通过为 router 创建 NodePort 或 LoadBalancer类型的 service 让其能够被外界访问。除了直接访问 router,还可以利用 K8s ingress 机制实现 http trigger。以下命令将为函数 hello 创建一个 http trigger,并指定访问路径为/echo。fission httptrigger create –url /echo –method GET –function hello –createingress –host example.com该命令会创建如下 ingress 对象,可以参考 createIngress 深入了解 ingress 的创建逻辑。apiVersion: extensions/v1beta1kind: Ingressmetadata: # 该 Ingress 的名称 name: xxx …spec: rules: - host: example.com http: paths: - backend: # 指向 router service serviceName: router servicePort: 80 # 访问路径 path: /echoIngress 只是用于描述路由规则,要让规则生效、实现请求转发,集群中需要有一个正在运行的 ingress controller。想要深入了解 ingress 原理可参考系列文章第一篇中的 HTTP trigger 章节。Time trigger如果希望定期触发函数执行,需要为函数创建 time trigger。Fission 使用 deployment 部署了组件 timer,该组件负责管理用户创建的 timer trigger。Timer 每隔一段时间会同步一次 time trigger 列表,并通过 golang 中被广泛使用的 cron 库 robfig/cron 定期触发和各 timer trigger 相关联函数的执行。以下命令将为函数 hello 创建一个名为halfhourly的 time trigger,该触发器每半小时会触发函数 hello 执行一次。这里使用了标准的 cron 语法定义执行计划。fission tt create –name halfhourly –function hello –cron “*/30 * * * *“trigger ‘halfhourly’ createdMessage queue trigger为了支持异步触发,fission 允许用户创建消息队列触发器。目前可供选择的消息队列有 nats-streaming、azure-storage-queue、kafka,下面以 kafka 为例描述消息队列触发器的使用方法和实现原理。以下命令将为函数 hello 创建一个基于 kafka 的消息队列触发器hellomsg。该触发器订阅了主题 input 中的消息,每当有消息到达它便会触发函数执行。如果函数执行成功,会将结果写入主题 output 中,否则将结果写入主题 error 中。fission mqt create –name hellomsg –function hello –mqtype kafka –topic input –resptopic output –errortopic error Fission 使用 deployment 部署了组件 mqtrigger-kafka,该组件负责管理用户创建的 kafka trigger。它每隔一段时间会同步一次 kafka trigger 列表,并为每个 trigger 创建 1 个用于执行触发逻辑的 go routine,触发逻辑如下:消费 topic 字段指定主题中的消息;通过向 router 发送请求触发函数执行并等待函数返回;如果函数执行成功,将返回结果写入 resptopic 字段指定的主题中,并确认消息已被处理;否则,将结果写入 errortopic 字段指定的主题中。小结Fission 提供了一些常用触发器,但缺少对 CNCF 规范里提到的Message/Record Streams触发方式的支持,该方式要求消息被顺序处理;如果有其它事件源想要接入可以参考 fission 触发器的设计模式自行实现。自动伸缩K8s 通过 Horizontal Pod Autoscaler 实现 pod 的自动水平伸缩。对于 fission,只有通过 newdeploy 方式创建的函数才能利用 HPA 实现自动伸缩。以下命令将创建一个名为 hello 的函数,运行该函数的 pod 会关联一个 HPA,该 HPA 会将 pod 数量控制在 1 到 6 之间,并通过增加或减少 pod 个数使得所有 pod 的平均 cpu 使用率维持在 50%。fission fn create –name hello –env python –code hello.py –executortype newdeploy –minmemory 64 –maxmemory 128 –minscale 1 –maxscale 6 –targetcpu 50Fission 使用的是autoscaling/v1版本的 HPA API,该命令将要创建的 HPA 如下:apiVersion: autoscaling/v1kind: HorizontalPodAutoscalermetadata: labels: executorInstanceId: xxx executorType: newdeploy functionName: hello … # 该 HPA 名称 name: hello-${executorInstanceId} # 该 HPA 所在命名空间 namespace: fission-function …spec: # 允许的最大副本数 maxReplicas: 6 # 允许的最小副本数 minReplicas: 1 # 该 HPA 关联的目标 scaleTargetRef: apiVersion: extensions/v1beta1 kind: Deployment name: hello-${executorInstanceId} # 目标 CPU 使用率 targetCPUUtilizationPercentage: 50想了解 HPA 的原理可参考系列文章第一篇中的自动伸缩章节,那里详细介绍了 K8s 如何获取和使用度量数据以及目前采用的自动伸缩策略。小结和 kubeless 类似,fission 避免了将创建 HPA 的复杂细节直接暴露给用户,但这是以牺牲功能为代价的;Fission 目前提供的自动伸缩功能过于局限,只对通过 newdeploy 方式创建的函数有效,且只支持基于 cpu 使用率这一种度量指标(kubeless 支出基于 cpu 和 qps)。本质上是因为 fission 目前仍然使用的是 v1 版本的 HPA,如果用户希望基于新的度量指标或者综合多项度量指标可以直接使用 hpa-v2 提供的功能;目前 HPA 的扩容缩容策略是基于既成事实被动地调整目标副本数,还无法根据历史规律预测性地进行扩容缩容。日志处理为了能更好地洞察函数的运行情况,往往需要对函数产生的日志进行采集、处理和分析。Fission 日志处理的原理如下图所示。日志处理流程如下:使用 DaemonSet 在集群中的每个工作节点上部署一个 fluentd 实例用于采集当前机器上的容器日志,参考 logger。这里,fluentd 容器将包含容器日志的宿主机目录/var/log/和/var/lib/docker/containers挂载进来,方便直接采集。Fluentd 将采集到的日志存储至 influxdb 中。用户使用 fission CLI 查看函数日志。例如,使用命令fission function logs –name hello可以查看到函数 hello 产生的日志。小结目前,fission 只做到了函数日志的集中化存储,能够提供的查询分析功能非常有限。另外,influxdb 更适合存储监控指标类数据,无法满足日志处理与分析的多样性需求。函数是运行在容器里的,因此函数日志处理本质上可归结为容器日志处理。针对容器日志,阿里云日志服务团队提供了成熟完备的解决方案,欲知详情可参考文章面向容器日志的技术实践。总结在介绍完 fission 的基本原理后,不妨从以下几个方面将其和第一篇介绍的 kubeless 作一个简单对比。触发方式 - 两款产品都支持常用的触发方式,但 kubeless 相比 fission 支持的更全面,且更方便接入新的数据源。自动伸缩 - 两款产品的自动伸缩能力都还比较基础,支持的度量指标都比较少,且底层都依赖于 K8s HPA。函数冷启动时间 - fission 通过池化技术降低了函数冷启动时间,kubeless 在这一块并未作过多优化。高级功能 - fission 支持灰度发布、自定义工作流等高级功能,kubeless 目前还不支持。参考资料Fission ArchitectureHow to Develop a Serverless Application with FissionFUNCTION EXECUTORS本文作者:吴波bruce_wu阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

December 11, 2018 · 4 min · jiezi

蚂蚁金服 Service Mesh 渐进式迁移方案|Service Mesh Meetup 实录

摘要: Service Mesher Meetup 上海站演讲内容整理。小蚂蚁说:本文是基于在 Service Mesher Meetup 上海站的主题分享《蚂蚁金服 Service Mesh 渐进式迁移方案》内容整理,完整的分享 PPT 获取方式见文章底部。敖小剑,蚂蚁金服高级技术专家,十六年软件开发经验,微服务专家,Service Mesh 布道师,Servicemesher 社区联合创始人;龙轼,阿里巴巴技术专家、前京东 Hadoop 负责人、Hadoop 代码贡献者、现负责UC 基于 Kubernetes 自研的 PaaS 平台整体的稳定性。大家好,今天给大家带来的演讲主题是《蚂蚁金服 Service Mesh 渐进式迁移方案》,给大家介绍一下我们蚂蚁金服主站的 Service Mesh 迁移方案,在稍后的内容中我会给大家解释什么是“渐进式”。今天的演讲方式有些特殊,将会是两位讲师合作。我是敖小剑,来自蚂蚁金服中间件团队,另外一位讲师 龙轼 ,来自 UC 基础研发部。Service Mesh 演进路线今天的内容将会有四块主要内容:Service Mesh演进路线:介绍蚂蚁金服计划在主站落地Service Mesh的方案,由于涉及到大量的存量应用和超大规模,又要保证迁移过程的平滑,因此我们的落地方案相比社区方案要复杂的多。实现平滑迁移的关键:介绍在整个迁移方案中,为了实现平滑迁移的几个关键做法,然后今天我们将详细展开其他的一个关键点:DNS寻址方案。DNS寻址方案的演进:详细介绍Kubernetes/Istio/SOFAMesh一路演进过来的DNS寻址方式。DNS寻址方案的后续规划:介绍我们在DNS寻址方案上的后续规划。前两块内容将由我来为大家介绍,后两块内容将由我的同事 龙轼 为大家介绍。在展开内容之前,先看一下背景,Service Mesh在蚂蚁金服主站落地的背景:目标:需要满足我们对长期目标的认可,具体指服务间通讯走Service Mesh,而且是Istio这种带完整的控制平面的Service Mesh形态,基础设施要构建在k8s之上,而应用的形态要向微服务靠拢。现状:而现实是存在很多挑战,首先还有很多应用没有实现微服务化,而且我们的k8s普及程度也不够,还有非常多的应用没有运行在kubernets之上。Istio的成熟程度也稍显不足,不够稳定,更大的挑战的是Istio目前无法原生支持我们蚂蚁金服的规模,我们还在试图对Istio进行改进和扩展。最后,在落地时必须考虑的非常现实的一点:现有系统中为数众多的应用不可能一夜之间全部迁移。关键需求:因此在落地实施时,非常重要的需求是:要实现平滑迁移。简单说,微服务 + Service Mesh + kubernetes 是我们的目标,但是如何从现有体系出发,向目标平稳和坚实的迈进,必须给出可行的实践指导。今天演讲的内容,要给大家介绍的就是,在这样的背景下,我们蚂蚁金服选择的Service Mesh主站落地演进方案。这个方案预期会在2019年初全面铺开。主站落地方案的实施原则,这是我们在过去半年的实践中,总结归纳出来的行为指导:符合远期规划:一定要有清晰的长期目标,明确的知道未来的大方向。避免走弯路,避免浪费投资,理想状态是计划中的每一步都可以为下一步奠定坚实的基础。即使因为某些原因不得已妥协或绕行,也应该清晰的知道后面应该如何回归,谢绝中途推倒重来——代价太高,无法承受。循序渐进:认清现实,如此之大的变革,一定是需要分步进行,不要心存一步登天的幻想,现实可行的方式是小步快跑。将整个过程拆解为若干个大步骤,每一步的工作量和复杂度都控制在一个可以接受的范围内,以保证每一步都简单方便,切实可行。有可操作性:在操作层面上,要有足够的弹性,即每个步骤中的工作内容,都应该是可以分批进行。以步步为营的方式,逐步扩大战果,杜绝一刀切。在接下来的演进路线中,大家将会体会到这三个原则在实际落地时的指导作用。这个图的信息量有点大,描述的是 Service Mesh 和 k8s 落地可能的多种演进路线。我们先从最下面开始看,这是当前蚂蚁金服主站大多数应用的现状:即应用"部署在非k8s上",应用也"不是Service Mesh形态"。 然后看最上面,这是我们期望的蚂蚁金服主站未来的应用终极形态:应用"部署在k8s上",应用也迁移到了"Service Mesh形态"。这里有个特别的地方,我们将Service Mesh形态细分为两种模式:Sidecar模式:只有Sidecar,没有控制平面,和外部系统的各种集成都是在Sidecar中直接进行。这是第一代的Service Mesh,Linkerd/Envoy都是如此,华为基于ServiceComb演进而来的mesher,新浪微博的Mesh,包括我们蚂蚁金服基于MOSN开发的用于取代多语言客户端的Mesh方案。Istio模式:有完善的控制平面,可以提供强大的控制能力,而且从数据平面分离,这是第二代的Service Mesh,典型如Istio和Conkduit/Linkerd 2.0。之所以将Service Mesh形态细分,是因为我们有着这样一个特殊背景:目前的原生Istio无法支撑我们蚂蚁金服的规模,因此在改进完善Istio之前,我们不得不暂时在Sidecar模式下短暂停留。另外一个原因就是考虑到存量应用的迁移,多一个Sidecar模式作为中间缓冲,会让整个迁移过程平滑很多。现在我们来介绍图中展示的四条演进路线:左边的路线1,思路是先将应用迁移k8s部署,再迁移到Service Mesh形态。这条路线的最大好处,是过程中每个阶段的绝大多数投资都将最终得以保留,因为符合k8s+service mesh的远期目标。右边的路线2,思路是跳过k8s,先迁移到Service Mesh形态,一路演进到Istio模式,然后最后迁移到k8s。中间的路线3,直接一步到位,这个路线是Istio默认的方式,或者说Istio根本没有考虑过迁移的问题,默认客户已经有完善的k8s,然后将改造好的应用直接部署在Istio上。这个路线对于蚂蚁金服主站的复杂场景,当然是不现实的。(补充:只是对蚂蚁金服主站不合适,对于大多数公司,规模不是那么巨大,也没有历史负担,也有k8s基础,完全可行。)还有一条特别的路线4,走位飘忽,先和路线2一样迁移到Sidecar模式,然后走回路线1,上k8s,再在有k8s支持的情况下继续演进到Istio模式。下面我们来详细分析各条演进路线的优劣和实施条件。演进路线2,和路线1的核心差别,在于:是先上k8s,还是先上Service Mesh。而且路线2是在非k8s条件下一路演进Service Mesh到我们期望的终极形态Istio模式,这意味着过程中和最终目标有非常大的偏移。演进路线2的好处,在于第一步非常的自然:没有k8s的限制,因此不依赖基础设施,实施方便。毕竟,k8s普及度是个大问题。在原有的侵入式框架的客户端SDK基础上,通过包裹一个proxy,重用原有SDK的能力,可以非常快速的得到一个基本可用的Sidecar。除了多一个proxy外,没有引入太多的新概念和新思想,符合现有开发人员/运维人员的心智,容易接受因此,路线2特别容易落地,可以快速达成短期目标,直接拿到Service Mesh的部分红利,如:多语言支持,方便类库升级等。但是,这个路线的问题在于再往后走,开始完善Service Mesh的功能以向Istio模式靠拢时,由于没有k8s的底层支持,因此不得不做大量的工作来提供类k8s的功能。尤其是Istio的非k8s支持,官方方案基本上只是一个demo,完全不具备生产可用性,要完善好,工作量很大。而关键点在于,这些投入,在迁移到k8s时,又因为和k8s提供的功能重复而被放弃。因此,结合我们前面的原则(符合远期规划,不浪费投资),路线2对蚂蚁金服主站落地是不合适的。演进路线4是一个非常特殊的路线,可以理解为路线1(先上k8s再上Service Mesh)的短期妥协版本。因为路线1的前提条件是要先大规模铺开k8s,将现有应用迁移到k8s之后再继续往Service Mesh演进,这对于还没有普及k8s的公司来说是一个非常高的门槛,很容易因此受阻而无法启动。因此,如果暂时不具备k8s条件, 又不想就此止步,那么选择路线2是唯一的出路。而上面我们分析过,路线2虽然能够在第一步快速拿到短期红利,但是由于偏离长期目标后续发展会有问题。怎么办?路线4可以是这种场景下的一个折衷选择:在k8s没有铺开之前,第一步沿路线2走,先吃下非k8s下Sidecar模式快速落地的红利。然后第二步避开非k8s下继续演进到Istio模式的大坑,切换到路线1,回归长期目标。好处非常明显:在k8s未铺开前,先往前迈进一步,避免就此卡壳。和路线2一样,第一步可以快速的拿到短期红利。后续转为路线1后,因为符合远期规划,因此后续演进不存在投资浪费的问题缺点就是存在少量的投资浪费,毕竟非k8s下的Sidecar模式还是有些工作内容在迁移到k8s之后会有改动。不过,这个改动不会太大,和拿到的红利相比还是值得的。路线4在操作时,存在一个变数:现有应用在向Sidecar模式的Service Mesh迁移,是需要一定时间的。有一种可能,就是在迁移过程中,k8s的普及开始了。这个变数的发生,取决于Sidecar模式的Service Mesh普及快,还是k8s的普及快。对路线4的分析结果:这是(k8s没有普及的)特殊时期的选择。在对四条可能的演进路线分析完成之后,我们来具体介绍蚂蚁金服的最终选择。坦言说,在过去半年中,我们的演进路线有几次摇摆和修订,今天我们公布的路线,和过去几个月中我们通过 meetup/技术大会/博客文章 等方式透露出来的方式会有一些变化。主要原因是在过去的这半年中,一方面我们对Sercice Mesh的认知更加深入,另一方面是蚂蚁金服的k8s背景也在变化。首先,在今年年初,我们确认Service Mesh大方向时,k8s还没有在蚂蚁金服普及,而且也没有明确的时间表。因此,我们在一番调研之后,选择了两条腿走路的方式:在非k8s环境下,以Sidecar模式先进行少量落地,主要是替换掉原有的多语言客户端 (拿短期红利)。开发SOFAMesh,集成MOSN到Istio,增加对多种RPC协议的支持,增加对RPC服务模式的兼容(为最终目标做准备 )在今年6月底的杭州第一届Service Mesh 线下 meetup 中,我们公布了 SOFAMesh 项目,我当时做了一个演讲 大规模微服务架构下的Service Mesh探索之路 ,有兴趣的同学可以去回顾一下我们当时的背景/需求/设计方案。大概在今年九月,我们完成了对非k8s下运行istio的深入调研,得出的结论是要实现这个模式需要非常多的工作。而且,我们对Service Mesh的认知也更加深刻,明确了通过Service Mesh将传统中间件能力向以k8s为代表的基础设施层下沉的战略方向。期间,内部也明确了k8s普及的大方向,因此,综合这两个重要输入,我们选择放弃继续在路线2上继续演进(即 istio on 非k8s)的想法。关于这一点,有兴趣的同学可以去阅读我在10月份QCon大会上的演讲内容 长路漫漫踏歌而行:蚂蚁金服Service Mesh实践探索 。最近,k8s普及的时间表再一次明确提前,蚂蚁金服将会在短时间内开启k8s的大面积普及。因此,我们的演进路线再一次发生变化。目前最新的演进路线将会是这样:当前还没有开始迁移的应用(处于演进路线图最下方),将按照路线1的方式进行迁移:先迁移到k8s,再迁移到Sidecar模式的Service Mesh。目前部分已经迁移的应用(路线2/4的第一步,非k8s部署的 Sidecar 模式),将沿路线4迁移,和路线1会师。由于应用众多,因此预计到 k8s + Sidecar模式 的迁移工作会持续比较长时间,在此期间,我们会同步完善Istio,和Istio官方一起合作来实现Istio对超大规模部署的支持。最后一步,迁移到最终目标(当然这一步的方案依然有很多待定内容,继续努力)需要强调的是:这个演进路线针对的是蚂蚁金服主站的特殊场景,并不具体普适性。大家可以在理解我们演进路线背后的思路和权衡方式之后,再结合自身的实际情况进行决策。比如,我们在UC落地时,由于UC有完善的k8s支持,而且目前落地的规模没那么夸张,因此是直接从"部署在k8s上" + “不是Service Mesh形态”,直接迁移到终态的。预计在金融云落实时,也会是如此,因为客户也不会有如此规模。总结:前面我们介绍了当应用程序向Service Mesh和K8s迁移时的几种可能的演进路线,分析了各条路线的利弊。并以蚂蚁金服主站为例,介绍了我们迁移的背景和演进路线的选择思路,希望能够帮助大家更好的理解Service Mesh的落地实践,以便在未来设计自家的落地方案时能有所参考。实现平滑迁移的关键前面给大家介绍了蚂蚁金服主站的Service Mesh演进路线,期间谈到要实现现有应用的平滑迁移。今天的第二个内容,将给大家介绍平滑迁移实现中的几个关键做法。首先,第一个关键是尽量保证迁移前后服务间网络互通。以向k8s迁移为例,在非k8s环境,典型的服务间访问方式是这样:每个服务向注册中心注册。客户端发起访问前,通过注册中心得到目标服务的实例列表信息,如IP地址/端口等在向k8s迁移的过程中,我们的做法是保证k8s内外网络打通,即服务的IP地址(在k8s中是pod ip)是可以相互直接访问的。基于这个前提,服务在迁移到k8s的过程中,原有的服务注册/服务发现/发起请求等逻辑都无需修改,是不是在k8s内,是不是pod ip,对原有服务化体系完全是透明的。因此,向k8s的迁移可以做到对业务应用非常的平滑,基本感知。透明拦截在迁移过程中,可以起到非常关键的作用。以Service-A要访问Service-B,在应用向Sidecar模式的Service Mesh迁移前后,会有有四种排列组合场景:Service-A和Service-B都没有迁移到Serive Mesh:此时请求会直接从Service-A发送到Service-B,称为直连,这是应用在开始迁移到Service Mesh之前的标准工作方式。Service-A已经迁移到Service Mesh,Service-B还没有:此时Service-A发出来的请求,会被劫持,然后发送到和Service-A一起部署的Sidecar(称为Outbound Sidecar),此时链路中只有一个Sidecar,称为(客户端)单跳。Service-B已经迁移到Service Mesh,Service-A还没有:此时Service-A发出来的请求,在到达Service-B时,会被劫持到和Service-B一起部署的Sidecar(称为Inbound Sidecar),此时链路中也只有一个Sidecar,称为(服务器端)单跳。Service-A和Service-B都迁移到Serive Mesh:此时Service-A发出来的请求,会被两次劫持,分别进入Outbound Sidecar和Inbound Sidecar,此时链路中有两个Sidecar,称为双跳。这是Istio的标准工作模式,也是我们迁移完成之后的最终工作模式。在这四种场景中,所有的网络请求,请求报文都是完全一致的,即不管是否被劫持到Sidecar,对请求报文都没有影响,也就是对发出请求报文的客户端和接受请求报文的客户端都是透明的,完全无感之。因此,在迁移过程中,可以单个服务逐个迁移,甚至服务的单个实例逐个迁移,而无需修改应用本身。在展开第三个关键点之前,我们来探讨一下:在Service Mesh时代,理想的客户端应该是什么样子?图中我们列举了一个传统的侵入式框架的客户端所包含的功能,在侵入式框架中,大部分的功能都是由客户端实现,因此会包含非常多的功能,如服务发现、负载均衡等基本功能,加密、认证、路由等高级功能。在应用迁移到Service Mesh之后,这些功能都下沉到Service Mesh中。因此,Service Mesh下的客户端可以进行大幅度的简化,成为一个新的轻量级客户端。对于这个轻量级客户端,我们希望可以尽可能的做的轻薄通用:实现简单,不管哪个编程语言都可以做到轻松实现,因此跨语言就方便了。而且越简单之后升级的可能性就会越少,以避免升级客户端。那我们来继续看,这个轻量级客户端里面最后还能剩下什么内容?图中列出了三个,其中最重要的,也是必不可少的是目标服务的标识,即无论如何简化,最低限度应该告之要访问谁吧?然后是序列化,对于RPC类肯定需要提供编解码功能,不过对于HTTP/REST类很多语言直接内置了标准实现。然后链路追踪,需要做一点工作来传递诸如SpanID之类的参数,同样这块也有可能通过自动埋点来实现。因此,最理想最单薄的客户端,可能只保留最后一个信息:目标服务的标示。在侵入式框架下,目标服务的标示是和服务注册/服务发现是直接关联的,这个标示通常都是服务名,通过服务发现机制实现了一个服务名到服务实例的寻址方式。在Service Mesh机制下,由于服务发现机制被下沉到Service Mesh中,因此只要底层Service Mesh能支持,这个目标服务的标示可以不必拘泥于服务名。那么,问题来了,对客户端来说:最简单,最通用,支持最广泛的寻址方式是什么?是DNS!在我们的迁移方案中,我们考虑引入DNS寻址方式。除了前面说的DNS是支持度最好,使用最普遍的寻址方式,在所有的编程语言和平台上都可以支持之外,我们还希望将DNS寻址方式作为未来产品的长期方向:在SOFAMesh和SOFAMosn中,我们已经基于名为x-protocol的方式实现了DNS通用寻址方式,用来解决Dubbo/HSF/SOFA等传统SOA服务模型在Service Mesh下的访问问题 (备注: 具体内容请见我的博客文章 SOFAMesh中的多协议通用解决方案x-protocol介绍系列(1)-DNS通用寻址方案 )未来在我们的serverless产品中,我们希望可以为运行其上的Function提供DNS寻址支持。可能还会有其他更加广泛的使用场景。因此,在我们的演进过程中,对于客户端SDK,我们有这样一个思路:一方面简化原有的SDK,去除和Sidecar重复的内容(满足短期需求)。另一方面,考虑到必然有一次客户端SDK的更换过程,那么我们希望在简化的同时引入基于DNS的通用寻址方式,以便在未来的后续迁移和功能扩展中可以依托这个机制来实现 (符合长期目标)图中描述的是在Service Mesh下,客户端通过域名来指定要访问的目标服务,然后通过DNS解析机制来串联底层的服务注册/DNS记录更新/透明劫持传递原始信息/Sidecar查找路由目标等详细实现机制。这里仅做简单示意,我就不详细展开了。在接下来的内容中,我的同事,来自UC基础研发部的 龙轼 同学,将为大家详细的展开DNS寻址方案的细节实现。DNS寻址方案的演进大家好,我是来自UC基础研发部的龙轼。 感谢小剑老师给我们介绍了蚂蚁和UC共建的Service Mesh的演进路线和实现平滑迁移的关键。接下来由我来向大家分享下实现平滑迁移的关键中的DNS寻址方案的演进。大家可以看上面的所示的DNS寻址方案的演进,我们先了解下各个服务寻址方案的背景。从 SOA 的寻址,到 Kubernetes 的寻址,然后再到 Istio 的寻址,最后是我们的 SOFAMesh 的DNS寻址方案。它们的寻址方案有什么不同,我们将一一分析它们的细节和总体寻址方案的演进路线。现在大家可以先来看下 SOA 架构下基于服务注册和服务发现的寻址。我们可以看到图中的 SOA 其实是单进程多接口的,依赖于 SOA 的服务注册与服务发现的。接下来我们看下 Kubernetes 的 DNS 寻址方式,它的寻址方式其实是通过DNS 的。从图中我们可以看到部署到K8S 上面的userservice 服务会生成一条DNS记录指向K8S 的ClusterIP。我们在 Pod 里面发起请求时通过 DNS 的 SearchDomain 域名补全规则就会从 DNS 里面查询得到ClusterIP,我们可以看出 Kubernetes 的寻址方案是单进程单接口的。看完 Kubernetes 的服务发现之后我们继续来看 Istio 的服务发现。从图中我们可以看出之前的流程都和 K8S 一脉相承,不同的地方在于 Istio 里面有个 SideCar 它把ClusterIP 拿到之后根据 ClusterIP 从 VirtualHost 里面匹配到 Rule 规则 转发给目标的 Pod 地址。最后我们来看下 SOFAMesh 的 DNS 通用寻址方案。根据我们之前分析的 SOA 寻址方案和 Kubernetes 寻址方案,我们可以看出如果我们的微服务不经过拆分和改造想上 Service Mesh 的话我们需要支持SOA之前的那种单个Pod 多个接口的。从图中看就是我们需要支持 com.alipay.userservice.interface1, com.alipay.userservice.interface2 这些接口解析到 ClusterIP, 我们知道k8s 中的service 是不支持的。那该如何是好,我们只能在DNS 上做文章修改DNS的记录来实现这一功能。确定了这一方案之后我们来看下我们设计的DNS寻址方案实现细节。大家看这张图:我们用 CRD 定义了一个 RPCService 和之前的 Service 有同样的 selector 的标签。然后用 RPC Service Controller 对 RPCService 做 Watch,当 RPCService 有更新的时候我们就把接口就是上述的 com.alipay.userservice.interface1 的记录写入 CoreDNS 里面。而 interface 是通过 Pod 里面的 Register Agent 来获取 Dubbo 里面暴露的。好的,说完这个方案的细节之后。我们可以看出其实其他的问题都不大,但是要更新DNS的这个我们需要支持。一开始我们 K8S 集群里面是用 Kube-DNS 来做 DNS 寻址的,但我们看这张 Kube-DNS 的架构图。可以看出修改它成本是比较大的,而且所有的DNS 都在同一个域里面,这个风险系数很高。 如果一旦修改错误势必会影响到之前的 k8s 的 service,导致线上的故障。这个时候我们跟踪到社区的 CoreDNS 项目,我们来看下 CoreDNS 的具体的架构。 它采用作为 Web 服务器 Caddy 的服务器框架,延用了Caddy 中的插件机制,大大的增加了 CoreDNS 的灵活性。 它的插件机制也特别简单,把所有的插件注册进一个Map里面来,在调用的时候从Map拿出他们有共同接口的函数。有兴趣的同学可以看下 Caddy 的插件代码实现。它的 DNS 协议库采用是由 Google 工程师 Meikg 开发的 DNS 库,他同时也是 SkyDNS 的开发者。后端可以采用 UDP/TCP、TLS 或者 gRPC 作为后端数据查询。上面有个Google工程师用 gRPC 做了一个 CoreDNS 插件的后端数据查询例子,有兴趣的同学可以看下。OK,既然 CoreDNS 的 Plugins 这么强大,我们可不可以用它来实现我们刚才说到的 Renew DNS的机制。 答案很显然是可以。我们看下上面的图,实现CoreDNS 的插件很简单,只需要继承上面的接口就可以了。 CoreDNS 官网有具体的教程在教我们怎么写一个插件。这个就不具体的展开了。到了我们最关键的点了:我们应该怎么更新我们的DNS。其实这点 CoreDNS 社区里面已经有人提出需求用 REST API 的形式提供更新 DNS 的接口。互联网任务工程小组也早在 rfc2136 定义了标准的 DNS UPDATE。 Google Cloud 和AWS 都有相应的实现。CoreDNS 社区其实已经把接口实现了,但是后端存储是基于file 的,数据没有落地。 蚂蚁和UC 这边扩展了 ETCD 插件的接口,把对应 DNS UPDATE 接口给实现了,实现 DNS 数据写入ETCD 里面。从图中我们可以看到 rpc.cluster.local 这个域 和 k8s 域 cluster.local 是在不同的插件链上的。这样在k8s域中没有 dynapirest 插件,我们就不能对k8s域中的DNS进行更新,这样就把之前Kube-DNS改造之后会对k8s域里面造成影响给去除了,更加的安全。我们可以看下 CoreDNS 后端存储的接口,其实和我们之前对数据操作的接口是没有什么差别的。目前 CoreDNS 的 DynAPI 还在主库代码没合并的状态。之后 DynAPI 这个项目会独立成一个插件项目。我们可以看下 CoreDNS 社区的 DynAPI 插件进展。OK,我们来看下我们的DynAPI 实现DNS 更新的一个效果。从图中我们可以看出 record.json 里面的一个域名的更新。通过 DynAPI 我们成功把 record.json 的DNS 记录给更新进去并且dns正常工作了。到现在我们通过CoreDNS 的插件就把DNS 更新的需求给解决了。其实CoreDNS 官网还有许多有趣的插件,可以丰富 CoreDNS 的功能和提升 CoreDNS 的性能。 大家可以看下中间的 autopath 插件,他把我们多次的在 searchdomain 拼凑的 DNS 记录的查询在在服务器上给实现了。 避免了多次的 Client 端和 Server 端的数据交互。有兴趣的同学可以看下 A-Deep-Dive-into-CoreDNS-2018。我们把 CoreDNS 的功能开发完了,上线的话很多人关注它的性能。 我们这边做了一个简单的性能测试,可以看出 CoreDNS 和 Bind DNS 这种现在比较通用的DNS的性能还是有点差距的。但是,我们通过上面的图可以看到在一定的QPS 下,CoreDNS 的延时是很低的。 我们可以看到所有的延时都落在4ms 之内。为了解决QPS的问题,我们通过 Kubernetes 的 HPA 给 CoreDNS 进行横向的扩展。一开始我们只是通过CPU的维度给 CoreDNS 扩展,但发现波动有点大。 之后我们切换成通过QPS的维度来进行扩容。CoreDNS 将会在Kubernetes 1.13 之后成为 Kubernetes 的默认的DNS服务。我们将会紧跟社区实施我们的方案并且反馈给社区。DNS寻址方案的后续规划我们再来看下我们后续的一些规划。可以看到我们的 DynAPI 其实在安全上还是有欠缺的。我们后续会把 HTTP 加强成 HTTPS 协议来增强 DynAPI 的安全性。还有如果我们 CoreDNS 的后端变化的更新的 Watch 由于 Watch的范围过大的话,会返回过多的数据。这样会影响到 Watch 的性能,CoreOS 在 ETCD3.2 增加了proxy 可以让我们根据不同的 ETCD KeySpace 去Watch,这样大大的提高了Watch的性能。最后一个,我们建议在创建 Kubernetes 集群的时候把 idc 的信息给带进Kubernetes的后缀域名中。这样我们之后可以通过 kubernetai 插件把不同的 Kubernetes 集群的域名进行整合通过本 IDC 缓存提高跨 IDC DNS 的访问速度。总结最后我们总结下,总体方面小剑老师给我们讲了蚂蚁金服主站 Service Mesh 的渐进式演进路线和实现平滑迁移的几个关键。 具体细节方面我们通过CoreDNS 的单点突破解决了 SOFAMesh 的 DNS 寻址的问题。感谢大家,希望这次演讲能让大家有所收获。视频回放与资料下载地址:https://tech.antfin.com/activ…(点击阅读原文可跳转到该网页)相关链接:SOFA 文档: http://www.sofastack.tech/SOFA: https://github.com/alipaySOFAMosn:https://github.com/alipay/sof…SOFAMesh:https://github.com/alipay/sof…本文作者:平生栗子阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

December 11, 2018 · 3 min · jiezi

Android 8.1 源码_启动篇 -- 探讨 Service 的使用方法

开篇核心源码关键类路径Service.javaframeworks/base/core/java/android/app/Service.java服务是什么?服务(Service)是 Android 中实现程序后台运行的解决方案,它非常适合去执行那些不需要和用户交互而且还需要长期进行的任务。服务的运行不依赖于任何用户界面,即使程序被切换到后台,或者用户打开了另外一个应用程序,服务仍然能够保持正常运行。不过需要注意的是,服务并不是运行在一个独立的进程当中的,而是依赖于创建服务时所在的应用程序进程。当某个应用程序进程被杀掉时,所有依赖于该进程的服务也会停止运行。Service 使用方法定义一个服务首先,我们定义一个MyService.java类,当然作为一个服务类,必须要继承 Service(android.app.Service),看代码:// 源码路径:frameworks/base/core/java/android/app/Service.javapublic abstract class Service extends ContextWrapper implements ComponentCallbacks2 { private static final String TAG = “Service”; … … @Nullable public abstract IBinder onBind(Intent intent); … …}Service 定义了一个抽象方法 onBind,子类继承它,必须复写此方法。public class MyService extends Service{ @Nullable @Override public IBinder onBind(Intent intent) { // 这是一个抽象方法,那么子类是必须要重写的 return null; }}服务既然已经定义好了,自然应该在服务中去处理一些事情,那处理事情的逻辑应该写在哪里?我们需要在服务里重写 Service 中的另一些常用的方法:public class MyService extends Service{ @Nullable @Override public IBinder onBind(Intent intent) { // 这是一个抽象方法,那么子类是必须要重写的 return null; } @Override public void onCreate() { // 服务创建时调用 super.onCreate(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { // 服务启动时调用 return super.onStartCommand(intent, flags, startId); } @Override public void onDestroy() { // 服务销毁时调用 super.onDestroy(); }}和添加 Activity 一样,我们添加了一个服务,那么在 AndroidManifest.xml 文件中必须进行注册才能生效!<?xml version=“1.0” encoding=“utf-8”?><manifest xmlns:android=“http://schemas.android.com/apk/res/android" package=“com.example.xin02ma.myapplication”> <application android:allowBackup=“true” android:icon="@mipmap/ic_launcher” …… <activity android:name=".MainActivity"> …… </activity> <service android:name=".MyService" android:enabled=“true” android:exported=“true” /> </application></manifest>启动和停止服务服务定义好了,接下来就应该考虑如何去启动以及停止这个服务了。(1)先添加两个Button(activity_main.xml)<Button android:id="@+id/start_service" android:layout_width=“match_parent” android:layout_height=“wrap_content” android:text="@string/start_service" /><Button android:id="@+id/stop_service" android:layout_width=“match_parent” android:layout_height=“wrap_content” android:text="@string/stop_service" /><center></center> (2)接下来,修改主函数 MainActivity 的代码:public class MainActivity extends Activity implements View.OnClickListener{ private Button startService; private Button stopService; protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 采用布局 startService = (Button) super.findViewById(R.id.start_service); // 取得Button实例 stopService = (Button) super.findViewById(R.id.stop_service); // 取得Button实例 startService.setOnClickListener(this); // 监控Button,注册按钮事件 stopService.setOnClickListener(this); // 监控Button,注册按钮时间 } public void onClick(View v) { switch (v.getId()) { case R.id.start_service: Intent startIntent = new Intent(this, MyService.class); startService(startIntent); // 启动服务 break; case R.id.stop_service: Intent stopIntent = new Intent(this, MyService.class); stopService(stopIntent); // 停止服务 break; default: break; } }}上面的代码很简单,主要作了以下工作:(1)取得 startService 和 stopService 两个按钮实例,并且注册了点击事件;(2)通过 Intent 对象,调用 Activity 的 startService() 和 stopService() 方法来启动和停止服务。【Notice】这里的活动的启动和停止完全是由活动本身控制的,如果我们 start 了服务,但是没有点击 stop,那么服务会一直处于运行状态,此时服务如何让自己停止下来?只需要在 MyService 的任何一个位置调用 stopSelf() 这个方法就能让服务停下来!Log测试添加Log,查看Service是如何运作的:public class MyService extends Service{ private static final String TAG = “MyService”; @Nullable @Override public IBinder onBind(Intent intent) { // 这是一个抽象方法,那么子类是必须要重写的 return null; } public void onCreate() { // 服务创建时调用 super.onCreate(); Log.d(TAG, “onCreate executed”); } public int onStartCommand(Intent intent, int flags, int startId) { // 服务启动时调用 Log.d(TAG, “onStartCommand executed”); return super.onStartCommand(intent, flags, startId); } public void onDestroy() { // 服务销毁时调用 super.onDestroy(); Log.d(TAG, “onDestroy executed”); }}添加了3行Log,目的就是看:在我们点击两个按钮的时候,整个Service什么时候创建,什么时候启动,什么时候毁灭!我们来看一下执行结果,运行程序,查看Logcat中的打印日志:(1)第一次点击 StartService按钮 后,MyService中的 onCreate() 和 onStartCommand() 方法都执行了,图中黄色箭头所示!此时,我们可以在 手机 –> 设置 –> 应用 –> 运行中 可以看到这个服务:(2)然后我们点击 stopService按钮 后,MyService中的 onDestory() 方法被执行,图中蓝色箭头所示!(3)此时可能你会有一个疑问?当我们点击了 startService按钮 以后,onCreate() 和 onStartCommand()方法同时被执行,这两个方法有什么区别?图中的 红色箭头 给了我们答案:onCreat() 方法是在服务第一次创建的时候调用的,而 onStartCommand() 方法则在每次启动服务的时候都会被调用。当我们在 服务未启动 的时候,点击 startService 按钮,则此时会 执行两个方法;但是 服务启动完成 之后,再次点击(随便你点几次)startService按钮,你会发现 只有onStartCommand()方法被执行。Service生命周期上面介绍完 Service 的使用方法,接下来看看 Service 的 生命周期 :跟Activity相比,Service的生命周期很简单:onCreate()->onStart()->onDestroy()我们以如下的方式展开这章节的讨论工作!【主题】:Activity 与 Service之间的 Communication【问题】:由上贴我们知道,当我们点击 START SERVICE 按钮后,服务的 onCreate() 和 onStartCommand() 方法会得到执行,此后 Service 是一直存在于后台运行的,Activity 无法控制 Service 中具体的逻辑运行,那么这样 Activity 只相当于起到一个通知的作用,除了告诉 Service 你可以开始工作了。那么这样显然会分离两者之间的关联性,这也不是我们需要的结果!【后果】:如果出现以上的问题,那么在我们平时的项目开发过程中,一直存在的 Service 很有可能会引起功耗的问题,可能影响手机的运行效率!【要求】:我们能否将 Activity 与 Service 建立一种联系,当 Activity 终结之时,Service 也销毁,也就是有没有办法让 Activity 和 Service 能够“不求同生,但求共死”? 答案是肯定的!这就涉及到 Service 的另一个重要知识点:绑定 与 解绑!还是以代码为例:MyServiceMyService.javapublic class MyService extends Service{ private static final String TAG = “MyService”; private DownloadBinder mBinder = new DownloadBinder(); // 定义一个 DownloadBinder 类 class DownloadBinder extends Binder { // 让 DownloadBinder 成为 Binder 的子类 public void startDownload() { // 定义开始下载的方法 Log.d(TAG, “startDownload executed”); } public int getProgress() { // 定义一个查看下载进度的方法 Log.d(TAG, “getProgress executed”); return 0; } } @Nullable @Override public IBinder onBind(Intent intent) { // onBind()方法,这个方法将在绑定后调用 return mBinder; // 返回 IBinder 的实例 –> DownloadBinder 类的实例 } public void onCreate() { super.onCreate(); Log.d(TAG, “onCreate executed”); } public int onStartCommand(Intent intent, int flags, int startId) { Log.d(TAG, “onStartCommand executed”); return super.onStartCommand(intent, flags, startId); } public void onDestroy() { super.onDestroy(); Log.d(TAG, “onDestroy executed”); }}BIND SERVICE / UNBIND SERVICE我们在Layout中添加两个按钮 BIND SERVICE 和 UNBIND SERVICE:MainActivity.javapublic class MainActivity extends Activity implements View.OnClickListener{ private ServiceConnection connection = new ServiceConnection() { // 创建一个 ServiceConnection 的匿名内部类 @Override public void onServiceConnected(ComponentName name, IBinder service) { // 重写 onServiceConnected() 方法 MyService.DownloadBinder downloadBinder = (MyService.DownloadBinder) service; // 向下转型取得 downloadBinder 实例 downloadBinder.startDownload(); // 在 Activity 中调用 Service 的方法 downloadBinder.getProgress(); // 在 Activity 中调用 Service 的方法 } @Override public void onServiceDisconnected(ComponentName name) { // 重写onServiceDisconnected()方法 } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button startService = (Button) super.findViewById(R.id.start_service); Button stopService = (Button) super.findViewById(R.id.stop_service); Button bindService = (Button) super.findViewById(R.id.bind_service); Button unbindService = (Button) super.findViewById(R.id.unbind_service); startService.setOnClickListener(this); stopService.setOnClickListener(this); bindService.setOnClickListener(this); unbindService.setOnClickListener(this); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.start_service: Intent startIntent = new Intent(this, MyService.class); // START 服务 –> onCreate() –> onStartCommand() startService(startIntent); break; case R.id.stop_service: Intent stopIntent = new Intent(this, MyService.class); // STOP 服务 –> onDestroy() stopService(stopIntent); break; case R.id.bind_service: // 绑定 –> ? Intent bindIntent = new Intent(this, MyService.class); bindService(bindIntent, connection, BIND_AUTO_CREATE); // ???? ???? ???? ???? 重点分析 break; case R.id.unbind_service: // 解绑 –> ? unbindService(connection); break; default: break; } }}bindService看一下 bindService(bindIntent, connection, BIND_AUTO_CREATE) 这个方法:bindService 接收了 3 个参数:bindIntent:这个参数传入的就是我们的 intent,目的就是调用 MyService 这个服务。connection:这个参数传入的就是创建好的 ServiceConnection 的实例,这个参数代表着我们的 Activity 是要和 Service 绑定在一起的!BIND_AUTO_CREATE:这是一个 FLAG,表示在活动和服务进行绑定后 自动创建服务。注意!是自动创建服务,也就是说 MyService 会执行 onCreate() 方法,但是不会执行 onStartCommand() 方法!接下来,直接看代码最终的效果:</left> <left>通过排列组合,对按钮进行点击,Log分 3 种情况:START SERVICE + STOP SERVICE:1、当我们先点击 START SERVICE :此时服务启动,调用 onCreat() 和 onStartCommand() 方法;2、当我们后点击 STOP SERVICE :此时,服务被销毁,调用 onDestroy() 方法。BIND SERVICE + UNBIND SERVICE:1、当我们先点击 BIND SERVICE :此时服务仅仅是创建,并未启动!所以调用的只是 onCreate() 方法。此时 Activity 与 Service 绑定,会同时调用 onBind() 方法,此时 onServiceConnected() 方法会被执行,还记的 onBind() 方法的返回类型不?我们通过 Log 可以很明显发现,Activity 调用了服务内部的两个自定义方法。2、当我们后点击 UNBIND SERVICE :由于服务还未启动,而 BIND SERVICE 只是将服务创建好并与活动进行绑定,那么解绑后,势必会销毁这个 Service,所以 onDestroy() 被执行!START SERVICE + BIND SERVICE + UNBIND SERVICE + STOP SERVICE:1、我们先点击 START SERVICE :onCreat() 和 onStartCommand() 方法被执行,这个就不用多说了; 2、然后点击 BIND SERVICE :这个时候其实活动已经在后台运行了,我们此时将活动和服务绑定,那么 onCreate() 不会再执行,只会执行 onServiceConnected() 方法,Log 里面打出来看的很清楚。3、此时你如果手贱,想 STOP SERVICE:那么恭喜你,毫无反应!为什么?因为你都没解绑,你怎么销毁?4、OK,那我们先解绑,我们点击 UNBIND SERVICE :此时一个奇怪的现象发生了,LOG 日志没有打印出 Destroy() 这个方法啊?没有被执行啊!不是说 bind 了 Service 之后,unbind 就会销毁这个服务吗?这跟我们之前分析的不符合啊。5、好吧,我们来看看为什么。其实原因很简单:我们先 start 了 Service,那么此时服务已经在后台运行了,这个时候你 bind,让 Service 和 Activity 绑定,其实是没有什么意义的。但是既然绑定了,你如果不解绑,那么 Destroy() 毫无用武,所以,这种情况和(2)中分析的还是有区别的,此是解绑完后,服务还是舒舒服服的在后台运行,所以,要想干掉这个服务,你必须要 STOP SERVICE。6、那我们解绑后,再 STOP SERVICE :这个时候 Service 就被枪毙了!Service 两个实用小技巧Forground Service服务几乎都是在后台运行的,一直一来它都是默默地做着辛苦的工作。但是服务的系统优先级还是比较低的,当系统出现内存不足的情况时,就有可能会回收掉正在后台运行的服务。如果你希望服务可以一直保持运行状态,而不是由于系统内存不足的原因导致被回收掉,就可以考虑使用前台服务。前台服务和普通服务最大的区别就在于,它会一直有一个正在运行的图标在系统的状态栏显示,下拉状态栏后可以看到更加详细的信息,非常类似于通知的效果。当然有时候你也可能不仅仅是为了防止服务被回收掉才使用前台服务,有些项目由于特殊的需求会要求必须使用前台服务,比如说天气类软件,它的服务在后台更新天气数据的同时,还会在系统状态栏一直显示当前的天气信息。【问题】:我们都知道服务是运行在后台的,如果系统出现内存不足的情况,那么此时,系统就可能回收后台的服务,那么我们如何保证服务可以一直运行?【解决】:在服务中,有一个 前台服务 的概念,调用 startForground() 方法可以实现。如何创建一个前台服务,看代码:public class MyService extends Service{ …… @Override public void onCreate() { super.onCreate(); Log.d(“MyService”, “onCreate executed”); Intent intent = new Intent(this, MainActivity.class); PendingIntent pi = PendingIntent.getActivity(this, 0, intent, 0); Notification notification = new Notification.Builder(this) // 启动服务后,在前台添加一个Notification .setContentTitle(“This is a content title”) .setContentText(“This is content text”) .setWhen(System.currentTimeMillis()) .setSmallIcon(R.mipmap.ic_launcher) .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher)) .setContentIntent(pi) .build(); startForeground(1, notification); } … …}以上的代码是在 Service 的创建中添加了一个 Notification,调用 startForground() 就可以保证:只要服务一直存在,那么在前台就会一直显示这个 Notification。如果我们在 onDestroy() 中调用 stopForground() 方法,会销毁这个 Notification,但是 Service 还是存活的,此时 Service 就会面临被 System 干掉的风险。如果直接 STOP SERVICE,那么 Notification 和 Service 都会销毁。 IntentService【问题】:我们知道服务的代码逻辑是在主线程中执行的,如果我们在主线程中需要执行一些耗时的操作,那么很有可能出现ANR(程序暂无响应)的状况。这个时候,我们可以采用 Android 的多线程编程的方式,我们应该在服务的每个具体的方法里开启一个子线程,然后在这里去处理那些耗时的逻辑。所以,一个比较标准的服务就可以写成如下形式:public class MyService extends Service{ @Nullable @Override public IBinder onBind(Intent intent) { return null; } @Override public int onStartCommand(Intent intent, int flags, int startId) { new Thread(new Runnable() { // 开启一个线程处理耗时操作 @Override public void run() { // 处理具体的逻辑 } }).start(); return super.onStartCommand(intent, flags, startId); }}现在,服务可以启动起来了。但是如果不调用 StopService() 或 stopSelf() 方法,服务会一直运行,所以我们需要修改一下代码:public class MyService extends Service{ @Nullable @Override public IBinder onBind(Intent intent) { return null; } @Override public int onStartCommand(Intent intent, int flags, int startId) { new Thread(new Runnable() { @Override public void run() { // 处理具体的逻辑 // 开启一个线程处理耗时操作 stopSelf(); // 让服务执行完逻辑后自行停止 } }).start(); return super.onStartCommand(intent, flags, startId); }}上面的代码就是一个标准的 Service 的书写形式,主要包含两个知识点:Thread子线程的创建 和 stopSelf() 方法的调用。虽说这种写法并不复杂,但是总会有人忘记开启线程,或者忘记调用 stopSelf(),那么有没有更好的办法能够实现上面两个需求呢?【解决】:在 Android 中,专门提供了一个 IntentService 类(android.app.IntentService),这个类就能很好的满足我们的需求!我们直接通过代码来看:新建一个 MyIntentService 类继承自 IntentService,代码如下:public class MyIntentService extends IntentService{ public MyIntentService() { super(“MyIntentService”); // 调用父类的有参构造函数 } @Override protected void onHandleIntent(Intent intent) { // 打印当前线程的 id Log.d(“MyIntentService”, “MyIntentServiceThread id is " + Thread.currentThread().getId()); } @Override public void onDestroy() { super.onDestroy(); Log.d(“MyIntentService”, “onDestroy executed”); }}以上代码做了几件事:1、提供了一个无参的构造方法,并且调用了父类的有参构造函数;2、子类实现父类的 onHandleIntent() 抽象方法,这个方法好就好在,它是一个已经运行在子线程中的方法。也就是说,服务调用了它,那么执行的逻辑就如同 Thread 子线程;3、根据 IntentService 的特性,这个服务在运行结束后应该是会自动停止的,所以我们又重写了 onDestroy()方法,在这里也打印一行日志,以证实服务是不是停止掉了。我们在 xml 文件中,创建一个 MyIntentService 服务按钮:<Button android:id=”@+id/start_intent_service" android:layout_width=“match_parent” android:layout_height=“wrap_content” android:text="@string/intent_service"/>然后修改 MainActivity 中的代码:public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); … … Button startIntentService = (Button) super.findViewById(R.id.start_intent_service); startIntentService.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Log.d(“MyIntentService”, “MainActivity Thread id is " + Thread.currentThread().getId()); // 查看主线程的id Intent intentService = new Intent(getBaseContext(), MyIntentService.class); startService(intentService); } }); }}最后,在AndroidMainfest中注册服务:<service android:name=".MyIntentService” />【结果】:从打出的LOG可以看出:1、MyIntentService 和 MainActivity 所在进程的 id是不一样的 ;2、onHandleIntent() 方法在执行完逻辑后确实销毁了服务,效果等同于 stopSelf()。从上面的分析可以看出 onHandleIntent() 方法确实相当的好用! ...

December 3, 2018 · 6 min · jiezi

Android 8.1 源码_核心篇 -- 深入研究 PMS 系列(6)之 APK 安装流程(PMS)

开篇核心源码关键类路径PackageInstallerSession.javaframeworks/base/services/core/java/com/android/server/pm/PackageInstallerSession.javaPackageManagerService.javaframeworks/base/services/core/java/com/android/server/pm/PackageManagerService.java前言在本系列上一篇文章 【深入研究 PackageManagerService 系列(5)之 PackageInstaller - APK 安装流程】 中,我们了解了 PackageInstaller 安装 APK 的流程,最后会将 APK 的信息交由 PMS 处理。那么 PMS 是如何处理的?这就是我们这篇文章需要分析的。PackageHandlercommitLocked在前一篇文章末尾,我们讲过 commitLocked 方法,我们回顾下: private final PackageManagerService mPm; private void commitLocked() throws PackageManagerException { … … /** * commitLocked 方法很长,我们主要关注这一行代码 * 调用 PackageManagerService 的 installStage 方法 * 这样安装 APK 的代码逻辑就进入了 PackageManagerService 中 / mPm.installStage(mPackageName, stageDir, stageCid, localObserver, params, mInstallerPackageName, mInstallerUid, user, mCertificates); }installStage正式进入 PMS 源码分析流程,我们看看 installStage 方法: void installStage(String packageName, File stagedDir, String stagedCid, IPackageInstallObserver2 observer, PackageInstaller.SessionParams sessionParams, String installerPackageName, int installerUid, UserHandle user, Certificate[][] certificates) { … … // 创建类型为 INIT_COPY 的消息 final Message msg = mHandler.obtainMessage(INIT_COPY); final int installReason = fixUpInstallReason(installerPackageName, installerUid, sessionParams.installReason); // 创建 InstallParams,它对应于包的安装数据 final InstallParams params = new InstallParams(origin, null, observer, sessionParams.installFlags, installerPackageName, sessionParams.volumeUuid, verificationInfo, user, sessionParams.abiOverride, sessionParams.grantedRuntimePermissions, certificates, installReason); params.setTraceMethod(“installStage”).setTraceCookie(System.identityHashCode(params)); msg.obj = params; … … // 将 InstallParams 通过消息发送出去 mHandler.sendMessage(msg); }handleMessage因为 PackageHandler 继承 Handler ,所以我们来看下 PackageHandler 的 HandlerMessage 方法: public void handleMessage(Message msg) { try { doHandleMessage(msg); } finally { // 设置了线程的优先级为后台线程 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); } }INIT_COPY接下来看下 INIT_COPY 消息的处理流程: class PackageHandler extends Handler { private boolean mBound = false; final ArrayList<HandlerParams> mPendingInstalls = new ArrayList<HandlerParams>(); … … // 用于处理各个类型的消息 void doHandleMessage(Message msg) { switch (msg.what) { case INIT_COPY: { // 取出 InstallParams HandlerParams params = (HandlerParams) msg.obj; // idx 为当前需要安装的 APK 个数,mPendingInstalls 里面保存所有需要安装的 APK 解析出来的 HandlerParams 参数 int idx = mPendingInstalls.size(); if (DEBUG_INSTALL) Slog.i(TAG, “init_copy idx=” + idx + “: " + params); // mBound 用于标识是否绑定了服务(DefaultContainerService), // 如果已经绑定了,则 mBound 为true,如果是第一次调用 mBound 为 false,默认值为 false。 if (!mBound) { Trace.asyncTraceBegin(TRACE_TAG_PACKAGE_MANAGER, “bindingMCS”, System.identityHashCode(mHandler)); // 如果没有绑定服务,重新绑定,connectToService 方法内部如果绑定成功会将 mBound 置为 true if (!connectToService()) { Slog.e(TAG, “Failed to bind to media container service”); params.serviceError(); Trace.asyncTraceEnd(TRACE_TAG_PACKAGE_MANAGER, “bindingMCS”, System.identityHashCode(mHandler)); if (params.traceMethod != null) { Trace.asyncTraceEnd(TRACE_TAG_PACKAGE_MANAGER, params.traceMethod, params.traceCookie); } // 绑定服务失败则 return return; } else { // 绑定服务成功,将请求添加到 ArrayList 类型的 mPendingInstalls 中,等待处理 mPendingInstalls.add(idx, params); } } else { // 已经绑定服务 mPendingInstalls.add(idx, params); if (idx == 0) { // 如果是第一个安装请求,则直接发送事件 MCS_BOUND 触发处理流程 mHandler.sendEmptyMessage(MCS_BOUND); } } break; } … … }connectToService假设我们是第一次走流程,还没有绑定服务,则会调用 connectToService() 方法,我们看下流程: class PackageHandler extends Handler { private boolean connectToService() { if (DEBUG_SD_INSTALL) Log.i(TAG, “Trying to bind to” + " DefaultContainerService”); Intent service = new Intent().setComponent(DEFAULT_CONTAINER_COMPONENT); Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT); /* * bindServiceAsUser 方法会传入 mDefContainerConn, * bindServiceAsUser 方法的处理逻辑和我们调用 bindService 是类似的, * 服务建立连接后,会调用 onServiceConnected / if (mContext.bindServiceAsUser(service, mDefContainerConn, Context.BIND_AUTO_CREATE, UserHandle.SYSTEM)) { Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); // 如果绑定 DefaultContainerService 成功,mBound 会置为 ture mBound = true; return true; } Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); return false; }这里可以看到 bind 到了一个 service ,这个 service 的 ComponentName 是 “DEFAULT_CONTAINER_COMPONENT” 这个常量,那我们就来看下这个 ComponentName。 static final ComponentName DEFAULT_CONTAINER_COMPONENT = new ComponentName( DEFAULT_CONTAINER_PACKAGE, “com.android.defcontainer.DefaultContainerService”);所以我们知道 bind 的 service 是 DefaultContainerService 。绑定 DefaultContainerService 之后,设定进程的优先级为 THREAD_PRIORITY_DEFAULT。然后等 bindServiceAsUser 这个方法执行完则又把线程的优先级设为 THREAD_PRIORITY_BACKGROUND。我们这边要重点提到一个 mDefContainerConn 变量,研究一下: final private DefaultContainerConnection mDefContainerConn = new DefaultContainerConnection();这下我们知道了 mDefContainerConn 的类型是 DefaultContainerConnection ,那我们来看下 DefaultContainerConnection 这个类。DefaultContainerConnection // DefaultContainerConnection 实现了 ServiceConnection,所以在连接成功的时候会调用 onServiceConnected 方法 class DefaultContainerConnection implements ServiceConnection { public void onServiceConnected(ComponentName name, IBinder service) { if (DEBUG_SD_INSTALL) Log.i(TAG, “onServiceConnected”); final IMediaContainerService imcs = IMediaContainerService.Stub .asInterface(Binder.allowBlocking(service)); // 发送了 MCS_BOUND 类型的消息 mHandler.sendMessage(mHandler.obtainMessage(MCS_BOUND, imcs)); } public void onServiceDisconnected(ComponentName name) { if (DEBUG_SD_INSTALL) Log.i(TAG, “onServiceDisconnected”); } }上文我们提及到 mContext.bindServiceAsUser(service, mDefContainerConn, Context.BIND_AUTO_CREATE, UserHandle.SYSTEM) 方法,其实就是"绑定" DefaultContainerService。我们知道 bind 一个 Service ,其中负责通信的 ServiceConnection,而本方法中负责通信的就是 mDefContainerConn。所以一旦绑定成功会执行 mDefContainerConn 的 onServiceConnected 方法。而现实是当绑定成功后在 onServiceConnected 中将一个 IBinder 转换成了一个 IMediaContainerService。这个就是 onServiceConnected 回调函数中根据参数传进来的 IMediaContainerService.Stub 的对象引用创建的一个远程代理对象,后面 PacakgeManagerServic 通过该代理对象访问 DefaultContainerService 服务。我们简单梳理一下以上代码所做的工作: ✨ mBound 用于标识是否绑定了 DefaultContainerService,默认值为 false。 ✨ DefaultContainerService 是用于检查和复制可移动文件的服务,这是一个比较耗时的操作,因此 DefaultContainerService 没有和 PMS 运行在同一进程中,它运行在 com.android.defcontainer 进程,通过 IMediaContainerService 和 PMS 进行 IPC 通信。彼此之间的 IPC 通信如下图所示: ✨ connectToService 方法用来绑定 DefaultContainerService。 ✨ mHandler.sendEmptyMessage(MCS_BOUND):发送 MCS_BOUND 类型的消息,触发处理第一个安装请求。不知道你是否发现,有两个发送 MCS_BOUND 类型消息的方法:// PackageHandler.doHandleMessage(已绑定服务)mHandler.sendEmptyMessage(MCS_BOUND); // 不带参数// DefaultContainerConnection(未绑定服务 - 绑定服务)mHandler.sendMessage(mHandler.obtainMessage(MCS_BOUND, imcs)); // 带参数MCS_BOUND 分析不带参数case MCS_BOUND: { if (DEBUG_INSTALL) Slog.i(TAG, “mcs_bound”); // 不带参数,则此条件不满足 if (msg.obj != null) { mContainerService = (IMediaContainerService) msg.obj; Trace.asyncTraceEnd(TRACE_TAG_PACKAGE_MANAGER, “bindingMCS”, System.identityHashCode(mHandler)); } // 走这边的逻辑 if (mContainerService == null) { // 服务没有绑定,则走这边,但是之前我们讲解过,发送 MCS_BOUND 时,已经绑定了服务,这显然是不正常的 if (!mBound) { // Something seriously wrong since we are not bound and we are not // waiting for connection. Bail out. Slog.e(TAG, “Cannot bind to media container service”); for (HandlerParams params : mPendingInstalls) { // 负责处理服务发生错误的情况 params.serviceError(); Trace.asyncTraceEnd(TRACE_TAG_PACKAGE_MANAGER, “queueInstall”, System.identityHashCode(params)); if (params.traceMethod != null) { Trace.asyncTraceEnd(TRACE_TAG_PACKAGE_MANAGER, params.traceMethod, params.traceCookie); } return; } // 绑定失败,清空安装请求队列 mPendingInstalls.clear(); } else { // 继续等待绑定服务 Slog.w(TAG, “Waiting to connect to media container service”); } } else if (mPendingInstalls.size() > 0) { … … } else { // Should never happen ideally. Slog.w(TAG, “Empty queue”); } break;}带参数case MCS_BOUND: { if (DEBUG_INSTALL) Slog.i(TAG, “mcs_bound”); if (msg.obj != null) { mContainerService = (IMediaContainerService) msg.obj; Trace.asyncTraceEnd(TRACE_TAG_PACKAGE_MANAGER, “bindingMCS”, System.identityHashCode(mHandler)); } // 带参数,此条件不满足 if (mContainerService == null) { … … // 走这边的逻辑,安装请求队列不为空 } else if (mPendingInstalls.size() > 0) { // 得到安装请求队列第一个请求 HandlerParams HandlerParams params = mPendingInstalls.get(0); if (params != null) { Trace.asyncTraceEnd(TRACE_TAG_PACKAGE_MANAGER, “queueInstall”, System.identityHashCode(params)); Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, “startCopy”); // 如果 HandlerParams 不为 null 就会调用 HandlerParams 的 startCopy 方法,用于开始复制 APK 的流程 if (params.startCopy()) { if (DEBUG_SD_INSTALL) Log.i(TAG, “Checking for more work or unbind…”); // 如果 APK 安装成功,删除本次安装请求 if (mPendingInstalls.size() > 0) { mPendingInstalls.remove(0); } if (mPendingInstalls.size() == 0) { if (mBound) { // 如果没有安装请求了,发送解绑服务的请求 if (DEBUG_SD_INSTALL) Log.i(TAG, “Posting delayed MCS_UNBIND”); removeMessages(MCS_UNBIND); Message ubmsg = obtainMessage(MCS_UNBIND); sendMessageDelayed(ubmsg, 10000); } } else { if (DEBUG_SD_INSTALL) Log.i(TAG, “Posting MCS_BOUND for next work”); // 如果还有其他的安装请求,接着发送 MCS_BOUND 消息继续处理剩余的安装请求 mHandler.sendEmptyMessage(MCS_BOUND); } } Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); } // 如果安装请求数不大于 0 就会打印 “Empty queue” } else { // Should never happen ideally. Slog.w(TAG, “Empty queue”); } break;}上面的流程其实很简单,我们根据是否传入了 ims 这个参数,走两条流程,核心的方法就是最终的 startCopy()。复制 APK上面我们提过,Copy APK 的操作是调用 HandlerParams 的 startCopy 方法。HandlerParams 是 PMS 中的抽象类,它的实现类为 PMS 的内部类 InstallParams。startCopy private abstract class HandlerParams { private static final int MAX_RETRIES = 4; /* * Number of times startCopy() has been attempted and had a non-fatal * error. / private int mRetries = 0; … … final boolean startCopy() { boolean res; try { if (DEBUG_INSTALL) Slog.i(TAG, “startCopy " + mUser + “: " + this); /* * mRetries 用于记录 startCopy 方法调用的次数,调用 startCopy 方法时会先自动加 1 * startCopy 方法尝试的次数超过了 4 次,就放弃这个安装请求 / if (++mRetries > MAX_RETRIES) { Slog.w(TAG, “Failed to invoke remote methods on default container service. Giving up”); // 发送 MCS_GIVE_UP 类型消息,将第一个安装请求(本次安装请求)从安装请求队列 mPendingInstalls 中移除掉 mHandler.sendEmptyMessage(MCS_GIVE_UP); handleServiceError(); return false; } else { handleStartCopy(); // ???? ???? ???? 重点方法 ???? ???? ???? res = true; } } catch (RemoteException e) { if (DEBUG_INSTALL) Slog.i(TAG, “Posting install MCS_RECONNECT”); mHandler.sendEmptyMessage(MCS_RECONNECT); res = false; } // 调用 handleReturnCode 抽象方法,这个方法会在 handleStartCopy 执行完拷贝相关行为之后,根据 handleStartCopy 做进一步的处理,主要返回状态码 handleReturnCode(); return res; } … … abstract void handleStartCopy() throws RemoteException; abstract void handleServiceError(); abstract void handleReturnCode(); }这边我们还是简单的看一下 MCS_GIVE_UP 和 MCS_RECONNECT 两种 message 的处理流程,逻辑相当简单:MCS_RECONNECT case MCS_RECONNECT: { if (DEBUG_INSTALL) Slog.i(TAG, “mcs_reconnect”); if (mPendingInstalls.size() > 0) { if (mBound) { disconnectService(); } if (!connectToService()) { Slog.e(TAG, “Failed to bind to media container service”); for (HandlerParams params : mPendingInstalls) { // Indicate service bind error params.serviceError(); Trace.asyncTraceEnd(TRACE_TAG_PACKAGE_MANAGER, “queueInstall”, System.identityHashCode(params)); } mPendingInstalls.clear(); } } break; }判断安装请求队列 mPendingInstalls 是否还有元素,如果有元素先断开绑定,则再次重新调用 connectToService 方法,我们知道 connectToService() 内部会再次执行绑定 DefaultContainerService,而在绑定成功后会再次发送一个 what 值为 MCS_BOUND 的 Message,从而又回到了 startCopy 里面。MCS_GIVE_UP case MCS_GIVE_UP: { if (DEBUG_INSTALL) Slog.i(TAG, “mcs_giveup too many retries”); HandlerParams params = mPendingInstalls.remove(0); Trace.asyncTraceEnd(TRACE_TAG_PACKAGE_MANAGER, “queueInstall”, System.identityHashCode(params)); break; }直接删除了安装请求队列 mPendingInstalls 里面下标为 0 的元素,即取消本次安装请求。handleStartCopy我们发现 handleStartCopy 也是一个抽象的方法,那么它在哪实现?前面我们说过:HandlerParams 是 PMS 中的抽象类,它的实现类为 PMS 的内部类 InstallParams。 class InstallParams extends HandlerParams { / * Invoke remote method to get package information and install * location values. Override install location based on default * policy if needed and then create install arguments based * on the install location. / public void handleStartCopy() throws RemoteException { int ret = PackageManager.INSTALL_SUCCEEDED; … … /* * 确定 APK 的安装位置 * onSd: 安装到 SD 卡 * onInt: 内部存储即 Data 分区 * ephemeral:安装到临时存储 / final boolean onSd = (installFlags & PackageManager.INSTALL_EXTERNAL) != 0; final boolean onInt = (installFlags & PackageManager.INSTALL_INTERNAL) != 0; final boolean ephemeral = (installFlags & PackageManager.INSTALL_INSTANT_APP) != 0; PackageInfoLite pkgLite = null; // APK 不能同时安装在 SD 卡和 Data 分区 if (onInt && onSd) { // Check if both bits are set. Slog.w(TAG, “Conflicting flags specified for installing on both internal and external”); ret = PackageManager.INSTALL_FAILED_INVALID_INSTALL_LOCATION; // 安装标志冲突,Instant Apps 不能安装到 SD 卡中 } else if (onSd && ephemeral) { Slog.w(TAG, “Conflicting flags specified for installing ephemeral on external”); ret = PackageManager.INSTALL_FAILED_INVALID_INSTALL_LOCATION; } else { /* * 获取 APK 的少量的信息 * 通过 IMediaContainerService 跨进程调用 DefaultContainerService 的 getMinimalPackageInfo 方法, * 该方法轻量解析 APK 并得到 APK 的少量信息, * 轻量解析的原因是这里不需要得到 APK 的全部信息,APK 的少量信息会封装到 PackageInfoLite 中。 / pkgLite = mContainerService.getMinimalPackageInfo(origin.resolvedPath, installFlags, packageAbiOverride); if (DEBUG_EPHEMERAL && ephemeral) { Slog.v(TAG, “pkgLite for install: " + pkgLite); } … .. } if (ret == PackageManager.INSTALL_SUCCEEDED) { // 判断安装的位置 int loc = pkgLite.recommendedInstallLocation; if (loc == PackageHelper.RECOMMEND_FAILED_INVALID_LOCATION) { ret = PackageManager.INSTALL_FAILED_INVALID_INSTALL_LOCATION; } else if (loc == PackageHelper.RECOMMEND_FAILED_ALREADY_EXISTS) { … … } else { installFlags = sPmsExt.customizeInstallPkgFlags(installFlags, pkgLite, mSettings.mPackages, getUser()); loc = installLocationPolicy(pkgLite); … … } } /* * 根据 InstallParams 创建 InstallArgs 对象 * InstallArgs 是一个抽象类,定义了 APK 的安装逻辑,比如"复制"和"重命名” APK 等 * * abstract int copyApk(IMediaContainerService imcs, boolean temp) throws RemoteException; * * InstallArgs 有 3 个子类,都被定义在 PMS 中: * FileInstallArgs:用于处理安装到非 ASEC 的存储空间的 APK ,也就是内部存储空间(Data分区) * AsecInstallArgs:用于处理安装到 ASEC 中(mnt/asec)即 SD 卡中的 APK * MoveInstallArgs:用于处理已安装 APK 的移动的逻辑 / final InstallArgs args = createInstallArgs(this); mArgs = args; if (ret == PackageManager.INSTALL_SUCCEEDED) { … … if (!origin.existing && requiredUid != -1 && isVerificationEnabled( verifierUser.getIdentifier(), installFlags, installerUid)) { … … } else { // 对 APK 进行检查后就会调用 InstallArgs 的 copyApk 方法进行安装 ret = args.copyApk(mContainerService, true); } } mRet = ret; } … … }FileInstallArgsOK,我们知道 InstallParams 有三个子类,不同的 InstallArgs 子类会有着不同的处理,那我们现在以 FileInstallArgs 为例跟踪学习一下具体的流程:copyApk /* * Logic to handle installation of non-ASEC applications, including copying * and renaming logic. / class FileInstallArgs extends InstallArgs { private File codeFile; … … int copyApk(IMediaContainerService imcs, boolean temp) throws RemoteException { Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, “copyApk”); try { return doCopyApk(imcs, temp); } finally { Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); } }doCopyApk调用了 doCopyApk 方法: private int doCopyApk(IMediaContainerService imcs, boolean temp) throws RemoteException { … … try { final boolean isEphemeral = (installFlags & PackageManager.INSTALL_INSTANT_APP) != 0; // 用于创建临时存储目录,比如 /data/app/vmdl18300388.tmp ,其中 18300388 是安装的 sessionId final File tempDir = mInstallerService.allocateStageDirLegacy(volumeUuid, isEphemeral); codeFile = tempDir; resourceFile = tempDir; } catch (IOException e) { Slog.w(TAG, “Failed to create copy file: " + e); return PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE; } … … int ret = PackageManager.INSTALL_SUCCEEDED; /* * 通过 IMediaContainerService 跨进程调用 DefaultContainerService 的 copyPackage 方法, * 这个方法会在 DefaultContainerService 所在的进程中将 APK 复制到临时存储目录, * 比如 /data/app/vmdl18300388.tmp/base.apk 。 / ret = imcs.copyPackage(origin.file.getAbsolutePath(), target); // 真正的文件拷贝 … … return ret; }安装 APKhandleReturnCode我们回到 APK 的复制调用链的头部方法:HandlerParams 的 startCopy 方法,在最后 调用了 handleReturnCode 方法,进行 APK 的安装。 private abstract class HandlerParams { private static final int MAX_RETRIES = 4; private int mRetries = 0; … … final boolean startCopy() { boolean res; try { if (++mRetries > MAX_RETRIES) { … … } else { handleStartCopy(); res = true; } } catch (RemoteException e) { … … } // 处理复制 APK 后的安装 APK 逻辑 handleReturnCode(); // ???? ???? ???? ???? ???? ???? return res; } … … abstract void handleReturnCode(); }handleReturnCode 也是一个抽象方法,那么在哪里实现?同样,它的实现在 InstallParams 中。 @Override void handleReturnCode() { // If mArgs is null, then MCS couldn’t be reached. When it // reconnects, it will try again to install. At that point, this // will succeed. if (mArgs != null) { // “装载代码"的入口是 processPendingInstall(InstallArgs,int) 方法 processPendingInstall(mArgs, mRet); } }我们发现调用了 processPendingInstall 方法,继续跟!processPendingInstall private void processPendingInstall(final InstallArgs args, final int currentStatus) { mHandler.post(new Runnable() { public void run() { mHandler.removeCallbacks(this); PackageInstalledInfo res = new PackageInstalledInfo(); res.setReturnCode(currentStatus); res.uid = -1; res.pkg = null; res.removedInfo = null; if (res.returnCode == PackageManager.INSTALL_SUCCEEDED) { /* * 安装前处理 * 用于检查 APK 的状态的,在安装前确保安装环境的可靠,如果不可靠会清除复制的 APK 文件 args.doPreInstall(res.returnCode); synchronized (mInstallLock) { installPackageTracedLI(args, res); // ???? ???? ???? ???? ???? ???? } /** * 安装后收尾 * 用于处理安装后的收尾操作,如果安装不成功,删除掉安装相关的目录与文件 args.doPostInstall(res.returnCode, res.uid); } … … } }); }installPackageTracedLI private void installPackageTracedLI(InstallArgs args, PackageInstalledInfo res) { try { Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, “installPackage”); installPackageLI(args, res); } finally { Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); } }installPackageLI private void installPackageLI(InstallArgs args, PackageInstalledInfo res) { … … PackageParser pp = new PackageParser(); pp.setSeparateProcesses(mSeparateProcesses); pp.setDisplayMetrics(mMetrics); pp.setCallback(mPackageParserCallback); Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, “parsePackage”); final PackageParser.Package pkg; try { // 解析 APK pkg = pp.parsePackage(tmpPackageFile, parseFlags); } catch (PackageParserException e) { res.setError(“Failed parse during installPackageLI”, e); return; } finally { Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); } … … // Get rid of all references to package scan path via parser. pp = null; String oldCodePath = null; boolean systemApp = false; synchronized (mPackages) { // 检查 APK 是否存在 if ((installFlags & PackageManager.INSTALL_REPLACE_EXISTING) != 0) { // 获取没被改名前的包名 String oldName = mSettings.getRenamedPackageLPr(pkgName); if (pkg.mOriginalPackages != null && pkg.mOriginalPackages.contains(oldName) && mPackages.containsKey(oldName)) { pkg.setPackageName(oldName); pkgName = pkg.packageName; // 设置标志位表示是替换安装 replace = true; if (DEBUG_INSTALL) Slog.d(TAG, “Replacing existing renamed package: oldName=” + oldName + " pkgName=” + pkgName); } … … } PackageSetting ps = mSettings.mPackages.get(pkgName); // 查看 Settings 中是否存有要安装的 APK 的信息,如果有就获取签名信息 if (ps != null) { if (DEBUG_INSTALL) Slog.d(TAG, “Existing package: " + ps); PackageSetting signatureCheckPs = ps; if (pkg.applicationInfo.isStaticSharedLibrary()) { SharedLibraryEntry libraryEntry = getLatestSharedLibraVersionLPr(pkg); if (libraryEntry != null) { signatureCheckPs = mSettings.getPackageLPr(libraryEntry.apk); } } // 检查签名的正确性 if (shouldCheckUpgradeKeySetLP(signatureCheckPs, scanFlags)) { if (!checkUpgradeKeySetLP(signatureCheckPs, pkg)) { res.setError(INSTALL_FAILED_UPDATE_INCOMPATIBLE, “Package " + pkg.packageName + " upgrade keys do not match the " + “previously installed version”); return; } } … … } int N = pkg.permissions.size(); for (int i = N-1; i >= 0; i–) { // 遍历每个权限,对权限进行处理 PackageParser.Permission perm = pkg.permissions.get(i); BasePermission bp = mSettings.mPermissions.get(perm.info.name); … … } } if (systemApp || sPmsExt.isOperatorApp(mPackages, mSettings.mPackages, pkgName)) { if (onExternal) { // 系统APP不能在SD卡上替换安装 res.setError(INSTALL_FAILED_INVALID_INSTALL_LOCATION, “Cannot install updates to system apps on sdcard”); return; } else if (instantApp) { // 系统 APP 不能被 Instant App 替换 res.setError(INSTALL_FAILED_INSTANT_APP_INVALID, “Cannot update a system app with an instant app”); return; } } … … // 重命名临时文件 if (!args.doRename(res.returnCode, pkg, oldCodePath)) { res.setError(INSTALL_FAILED_INSUFFICIENT_STORAGE, “Failed rename”); return; } if (!instantApp) { startIntentFilterVerifications(args.user.getIdentifier(), replace, pkg); } else { if (DEBUG_DOMAIN_VERIFICATION) { Slog.d(TAG, “Not verifying instant app install for app links: " + pkgName); } } try (PackageFreezer freezer = freezePackageForInstall(pkgName, installFlags, “installPackageLI”)) { if (replace) { // 替换安装 if (pkg.applicationInfo.isStaticSharedLibrary()) { PackageParser.Package existingPkg = mPackages.get(pkg.packageName); if (existingPkg != null && existingPkg.mVersionCode != pkg.mVersionCode) { res.setError(INSTALL_FAILED_DUPLICATE_PACKAGE, “Packages declaring " + “static-shared libs cannot be updated”); return; } } replacePackageLIF(pkg, parseFlags, scanFlags | SCAN_REPLACING, args.user, installerPackageName, res, args.installReason); } else { // 安装新的 APK installNewPackageLIF(pkg, parseFlags, scanFlags | SCAN_DELETE_DATA_ON_FAILURES, args.user, installerPackageName, volumeUuid, res, args.installReason); } } // 更新应用程序所属的用户 synchronized (mPackages) { final PackageSetting ps = mSettings.mPackages.get(pkgName); if (ps != null) { res.newUsers = ps.queryInstalledUsers(sUserManager.getUserIds(), true); ps.setUpdateAvailable(false /updateAvailable/); } … … } }installPackageLI 方法的代码很长,这里截取主要的部分,主要做了几件事: ✨ 1、创建 PackageParser 解析 APK 。 ✨ 2、检查 APK 是否存在,如果存在就获取此前没被改名前的包名,赋值给 PackageParser.Package 类型的 pkg ,将标志位 replace 置为 true 表示是替换安装。 ✨ 3、如果 Settings 中保存有要安装的 APK 的信息,说明此前安装过该 APK ,则需要校验 APK 的签名信息,确保安全的进行替换。 ✨ 4、将临时文件重新命名,比如前面提到的 /data/app/vmdl18300388.tmp/base.apk ,重命名为 /data/app/包名-1/base.apk 。这个新命名的包名会带上一个数字后缀 1,每次升级一个已有的 App ,这个数字会不断的累加。 ✨ 5、系统 APP 的更新安装会有两个限制,一个是系统 APP 不能在 SD 卡上替换安装,另一个是系统 APP 不能被 Instant App 替换。 ✨ 6、根据 replace 来做区分,如果是替换安装就会调用 replacePackageLIF 方法,其方法内部还会对系统 APP 和非系统 APP 进行区分处理,如果是新安装 APK 会调用 installNewPackageLIF 方法。installNewPackageLIF我们以安装新 APK 为例,查看 installNewPackageLIF 的源码: /* * Install a non-existing package. / private void installNewPackageLIF(PackageParser.Package pkg, final int policyFlags, int scanFlags, UserHandle user, String installerPackageName, String volumeUuid, PackageInstalledInfo res, int installReason) { Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, “installNewPackage”); … … try { // 扫描 APK PackageParser.Package newPackage = scanPackageTracedLI(pkg, policyFlags, scanFlags, System.currentTimeMillis(), user); // 更新 Settings 信息 updateSettingsLI(newPackage, installerPackageName, null, res, user, installReason); if (res.returnCode == PackageManager.INSTALL_SUCCEEDED) { // 安装成功后,为新安装的应用程序准备数据 prepareAppDataAfterInstallLIF(newPackage); } else { // 安装失败则删除 APK deletePackageLIF(pkgName, UserHandle.ALL, false, null, PackageManager.DELETE_KEEP_DATA, res.removedInfo, true, null); } } catch (PackageManagerException e) { res.setError(“Package couldn’t be installed in " + pkg.codePath, e); } Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); }installNewPackageLIF 主要做了以下 3 件事: ✨ 1、扫描 APK,将 APK 的信息存储在 PackageParser.Package 类型的 newPackage 中,一个 Package 的信息包含了 1 个 base APK 以及 0 个或者多个 split APK 。 ✨ 2、更新该 APK 对应的 Settings 信息,Settings 用于保存所有包的动态设置。 ✨ 3、如果安装成功就为新安装的应用程序准备数据,安装失败就删除APK。scanPackageTracedLI调用 scanPackageTracedLI() 进行安装 : public PackageParser.Package scanPackageTracedLI(File scanFile, final int parseFlags, int scanFlags, long currentTime, UserHandle user) throws PackageManagerException { Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, “scanPackage [” + scanFile.toString() + “]”); try { return scanPackageLI(scanFile, parseFlags, scanFlags, currentTime, user); } finally { Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); } }scanPackageLI - 01scanPackageTracedLI() 调用了 scanPackageLI() 方法: /* * Scans a package and returns the newly parsed package. * Returns {@code null} in case of errors and the error code is stored in mLastScanError */ private PackageParser.Package scanPackageLI(File scanFile, int parseFlags, int scanFlags, long currentTime, UserHandle user) throws PackageManagerException { if (DEBUG_INSTALL) Slog.d(TAG, “Parsing: " + scanFile); PackageParser pp = new PackageParser(); pp.setSeparateProcesses(mSeparateProcesses); pp.setOnlyCoreApps(mOnlyCore); pp.setDisplayMetrics(mMetrics); pp.setCallback(mPackageParserCallback); … … return scanPackageLI(pkg, scanFile, parseFlags, scanFlags, currentTime, user); }scanPackageLI - 02 private PackageParser.Package scanPackageLI(PackageParser.Package pkg, final int policyFlags, int scanFlags, long currentTime, @Nullable UserHandle user) throws PackageManagerException { boolean success = false; try { // scanPackageDirtyLI 实际安装 package 的方法 final PackageParser.Package res = scanPackageDirtyLI(pkg, policyFlags, scanFlags, currentTime, user); success = true; return res; } finally { if (!success && (scanFlags & SCAN_DELETE_DATA_ON_FAILURES) != 0) { // DELETE_DATA_ON_FAILURES is only used by frozen paths destroyAppDataLIF(pkg, UserHandle.USER_ALL, StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE); destroyAppProfilesLIF(pkg, UserHandle.USER_ALL); } } }总结本文主要讲解了 PMS 是如何处理 APK 安装的流程,主要有几个步骤: ✨ PackageInstaller 安装 APK 时会将 APK 的信息交由 PMS 处理,PMS 通过向 PackageHandler 发送消息来驱动 APK 的复制和安装工作。 ✨ PMS 发送 INIT_COPY 和 MCS_BOUND 类型的消息,控制 PackageHandler 来绑定 DefaultContainerService ,完成复制 APK 等工作。 ✨ 复制 APK 完成后,会开始进行安装 APK 的流程,包括安装前的检查、安装 APK 和安装后的收尾工作。参考 01. http://liuwangshu.cn/framewor... 02. https://www.jianshu.com/p/c43… ...

November 30, 2018 · 13 min · jiezi

Android 8.1 源码_核心篇 -- 深入研究 PackageManagerService 系列(2)

开篇核心源码关键类路径SystemServer.javaframeworks/base/services/java/com/android/server/SystemServer.javaPackageManagerService.javaframeworks/base/services/core/java/com/android/server/pm/PackageManagerService.javaProcess.javaframeworks/base/core/java/android/os/Process.javaSystemConfig.javaframeworks/base/core/java/com/android/server/SystemConfig.javaSettings.javaframeworks/base/services/core/java/com/android/server/pm/Settings.java简介PackageManagerService(PMS)是 SystemServer 启动后的第一个核心服务,也是 Android 系统中最常用的服务之一。它负责系统中 Package 的管理,应用程序的安装、卸载、信息查询等。如果你是面向 Android 系统开发的工程师,基础概念我也不需要再多赘述,我们直接跟源码。构造函数分析 - 扫描PackagePMS 构造函数第二阶段的工作就是扫描系统中的 APK 了。由于需要逐个扫描文件,因此手机上装的程序越多,PMS 的工作量就越大,系统启动速度也就越慢,这就是为什么你的手机启动速度有快慢的原因。系统库的 dex 优化接着上面的 PMS 构造函数继续分析源码: public PackageManagerService(Context context, Installer installer, boolean factoryTest, boolean onlyCore) { … … // DEX 优化 mPackageDexOptimizer = new PackageDexOptimizer(installer, mInstallLock, context, “dexopt”); mDexManager = new DexManager(this, mPackageDexOptimizer, installer, mInstallLock); synchronized (mPackages) { mHandlerThread = new ServiceThread(TAG, Process.THREAD_PRIORITY_BACKGROUND, true /allowIo/); mHandlerThread.start(); mHandler = new PackageHandler(mHandlerThread.getLooper()); mProcessLoggingHandler = new ProcessLoggingHandler(); Watchdog.getInstance().addThread(mHandler, WATCHDOG_TIMEOUT); mDefaultPermissionPolicy = new DefaultPermissionGrantPolicy(this); mInstantAppRegistry = new InstantAppRegistry(this); // 为 data/ 目录下的某些子目录生成File实例 File dataDir = Environment.getDataDirectory(); // data/app 存放第三方应用 mAppInstallDir = new File(dataDir, “app”); mAppLib32InstallDir = new File(dataDir, “app-lib”); mAsecInternalPath = new File(dataDir, “app-asec”).getPath(); // data/app-private 存放 drm 保护的应用 mDrmAppPrivateInstallDir = new File(dataDir, “app-private”); sUserManager = new UserManagerService(context, this, new UserDataPreparer(mInstaller, mInstallLock, mContext, mOnlyCore), mPackages); // 获取 SystemConfig 中解析到的 <permission> 标签标识的 permission 信息,保存到 Settings::mPermissions ArrayMap<String, SystemConfig.PermissionEntry> permConfig = systemConfig.getPermissions(); for (int i=0; i<permConfig.size(); i++) { SystemConfig.PermissionEntry perm = permConfig.valueAt(i); BasePermission bp = mSettings.mPermissions.get(perm.name); if (bp == null) { bp = new BasePermission(perm.name, “android”, BasePermission.TYPE_BUILTIN); mSettings.mPermissions.put(perm.name, bp); } if (perm.gids != null) { bp.setGids(perm.gids, perm.perUser); } } // 得到除 framework 之外的系统中的共享库列表,从 SystemConfig 获取解析到的数据 ArrayMap<String, String> libConfig = systemConfig.getSharedLibraries(); final int builtInLibCount = libConfig.size(); for (int i = 0; i < builtInLibCount; i++) { String name = libConfig.keyAt(i); String path = libConfig.valueAt(i); addSharedLibraryLPw(path, null, name, SharedLibraryInfo.VERSION_UNDEFINED, SharedLibraryInfo.TYPE_BUILTIN, PLATFORM_PACKAGE_NAME, 0); } mFoundPolicyFile = SELinuxMMAC.readInstallPolicy(); // 读取packages.xml的内容,并对mSettings::mPackages等成员进行赋值;packages.xml文件中的内容是上一次扫描apk目录的结果; // 当前这一次扫描的结果是保存在PackageManagerService::mPackages列表中; // 对比上次扫描的结果来检查本次扫描到的应用中是否有被升级包覆盖的系统应用,如果有则从PackageManagerService::mPackages中移除; // 这样,PackageManagerService::mPackages的记录就和mSettings::mPackages的一致了; // 系统最终会将本次apk扫描的结果重新写入packages.xml中 mFirstBoot = !mSettings.readLPw(sUserManager.getUsers(false)); // Clean up orphaned packages for which the code path doesn’t exist // and they are an update to a system app - caused by bug/32321269 final int packageSettingCount = mSettings.mPackages.size(); // 清理那些代码路径不存在的异常 package for (int i = packageSettingCount - 1; i >= 0; i–) { PackageSetting ps = mSettings.mPackages.valueAt(i); if (!isExternal(ps) && (ps.codePath == null || !ps.codePath.exists()) && mSettings.getDisabledSystemPkgLPr(ps.name) != null) { mSettings.mPackages.removeAt(i); mSettings.enableSystemPackageLPw(ps.name); } } if (mFirstBoot) { requestCopyPreoptedFiles(); } // 设置模块来代替 framework-res.apk 中缺省的 ResolverActivity String customResolverActivity = Resources.getSystem().getString( R.string.config_customResolverActivity); if (TextUtils.isEmpty(customResolverActivity)) { customResolverActivity = null; } else { mCustomResolverComponentName = ComponentName.unflattenFromString( customResolverActivity); } long startTime = SystemClock.uptimeMillis(); // 记录扫描开始的时间 // 需要系统提前加载的一些 jar final String bootClassPath = System.getenv(“BOOTCLASSPATH”); final String systemServerClassPath = System.getenv(“SYSTEMSERVERCLASSPATH”); if (bootClassPath == null) { Slog.w(TAG, “No BOOTCLASSPATH found!”); } if (systemServerClassPath == null) { Slog.w(TAG, “No SYSTEMSERVERCLASSPATH found!”); }扫描系统 Package清空 cache 文件后,PMS 终于进入重点段了。接下来看 PMS 第二阶段工作的核心内容,即扫描 Package,相关代码如下:// PackageManagerService.java // 定义 frameworkDir 指向 /system/frameworks 目录 File frameworkDir = new File(Environment.getRootDirectory(), “framework”); final VersionInfo ver = mSettings.getInternalVersion(); mIsUpgrade = !Build.FINGERPRINT.equals(ver.fingerprint); // when upgrading from pre-M, promote system app permissions from install to runtime mPromoteSystemApps = mIsUpgrade && ver.sdkVersion <= Build.VERSION_CODES.LOLLIPOP_MR1; // When upgrading from pre-N, we need to handle package extraction like first boot, // as there is no profiling data available. mIsPreNUpgrade = mIsUpgrade && ver.sdkVersion < Build.VERSION_CODES.N; mIsPreNMR1Upgrade = mIsUpgrade && ver.sdkVersion < Build.VERSION_CODES.N_MR1; // save off the names of pre-existing system packages prior to scanning; we don’t // want to automatically grant runtime permissions for new system apps // 是否需要提升权限 if (mPromoteSystemApps) { Iterator<PackageSetting> pkgSettingIter = mSettings.mPackages.values().iterator(); while (pkgSettingIter.hasNext()) { PackageSetting ps = pkgSettingIter.next(); if (isSystemApp(ps)) { // 遍历Settings::mPackages集合,将系统APP加入到PackageManagerService::mExistingSystemPackages mExistingSystemPackages.add(ps.name); } } } mCacheDir = preparePackageParserCache(mIsUpgrade); // 定义扫描参数 int scanFlags = SCAN_BOOTING | SCAN_INITIAL; if (mIsUpgrade || mFirstBoot) { scanFlags = scanFlags | SCAN_FIRST_BOOT_OR_UPGRADE; } // 先扫描 /vendor/overlay 目录 scanDirTracedLI(new File(VENDOR_OVERLAY_DIR), mDefParseFlags | PackageParser.PARSE_IS_SYSTEM | PackageParser.PARSE_IS_SYSTEM_DIR | PackageParser.PARSE_TRUSTED_OVERLAY, scanFlags | SCAN_TRUSTED_OVERLAY, 0); // 调用 scanDirTracedLI 函数扫描 /system/frameworks 目录 scanDirTracedLI(frameworkDir, mDefParseFlags | PackageParser.PARSE_IS_SYSTEM | PackageParser.PARSE_IS_SYSTEM_DIR | PackageParser.PARSE_IS_PRIVILEGED, scanFlags | SCAN_NO_DEX, 0); // Collected privileged system packages. final File privilegedAppDir = new File(Environment.getRootDirectory(), “priv-app”); // 扫描 /system/priv-app 下的 package scanDirTracedLI(privilegedAppDir, mDefParseFlags | PackageParser.PARSE_IS_SYSTEM | PackageParser.PARSE_IS_SYSTEM_DIR | PackageParser.PARSE_IS_PRIVILEGED, scanFlags, 0); // Collect ordinary system packages. final File systemAppDir = new File(Environment.getRootDirectory(), “app”); // 扫描 /system/app 下的 package scanDirTracedLI(systemAppDir, mDefParseFlags | PackageParser.PARSE_IS_SYSTEM | PackageParser.PARSE_IS_SYSTEM_DIR, scanFlags, 0); // Collect all vendor packages. File vendorAppDir = new File("/vendor/app"); try { vendorAppDir = vendorAppDir.getCanonicalFile(); } catch (IOException e) { // failed to look up canonical path, continue with original one } // 扫描 /vendor/app 下的 package scanDirTracedLI(vendorAppDir, mDefParseFlags | PackageParser.PARSE_IS_SYSTEM | PackageParser.PARSE_IS_SYSTEM_DIR, scanFlags, 0); // Collect all OEM packages. final File oemAppDir = new File(Environment.getOemDirectory(), “app”); // 扫描 OEM 的 Package scanDirTracedLI(oemAppDir, mDefParseFlags | PackageParser.PARSE_IS_SYSTEM | PackageParser.PARSE_IS_SYSTEM_DIR, scanFlags, 0);由以上代码可知,PMS 将扫描以下几个目录(仅列出重点): ✨ /system/frameworks :该目录中的文件都是系统库,例如:framework.jar、services.jar、framework-res.apk。不过 scanDirTracedLI 只扫描 APK 文件,所以 framework-res.apk 是该目录中唯一“受宠”的文件。 ✨ /system/app :该目录下全是默认的系统应用。例如:Browser.apk、SettingsProvider.apk 等。 ✨ /vendor/app :该目录中的文件由厂商提供,即全是厂商特定的 APK 文件,目前市面上的厂商都把自己的应用放在 /system/app 目录下。scanDirTracedLIPMS 调用 scanDirTracedLI 函数进行扫描,下面分析此函数: public void scanDirTracedLI(File dir, final int parseFlags, int scanFlags, long currentTime) { try { scanDirLI(dir, parseFlags, scanFlags, currentTime); // 调用 scanDirLI 函数 } finally { Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); } }我们可以看出,实际是调用了 scanDirLI 函数进行扫描工作!scanDirLI下面的重点就是来关注 scanDirLI 函数了: private void scanDirLI(File dir, int parseFlags, int scanFlags, long currentTime) { // 列举该目录下的文件 final File[] files = dir.listFiles(); if (ArrayUtils.isEmpty(files)) { Log.d(TAG, “No files in app dir " + dir); return; } ParallelPackageParser parallelPackageParser = new ParallelPackageParser( mSeparateProcesses, mOnlyCore, mMetrics, mCacheDir, mParallelPackageParserCallback); int fileCount = 0; for (File file : files) { final boolean isPackage = (isApkFile(file) || file.isDirectory()) && !PackageInstallerService.isStageName(file.getName()); if (!isPackage) { // Ignore entries which are not packages continue; } parallelPackageParser.submit(file, parseFlags); fileCount++; } // Process results one by one for (; fileCount > 0; fileCount–) { ParallelPackageParser.ParseResult parseResult = parallelPackageParser.take(); Throwable throwable = parseResult.throwable; int errorCode = PackageManager.INSTALL_SUCCEEDED; if (throwable == null) { // Static shared libraries have synthetic package names if (parseResult.pkg.applicationInfo.isStaticSharedLibrary()) { renameStaticSharedLibraryPackage(parseResult.pkg); } try { if (errorCode == PackageManager.INSTALL_SUCCEEDED) { // 调用 scanPackageLI 函数扫描一个特定的文件,返回值是 PackageParser 的内部类 Package,该类的实例代表一个 APK 文件,所以它就是和 APK 文件对应的数据结构 scanPackageLI(parseResult.pkg, parseResult.scanFile, parseFlags, scanFlags, currentTime, null); } … … } // Delete invalid userdata apps if ((parseFlags & PackageParser.PARSE_IS_SYSTEM) == 0 && errorCode == PackageManager.INSTALL_FAILED_INVALID_APK) { // 非系统 Package 扫描失败,删除文件 removeCodePathLI(parseResult.scanFile); } } parallelPackageParser.close(); }scanPackageLI - 01PMS 中有三处 scanPackageLI,我们后面会一一分析到,先来看第一个也是最先碰到的 sanPackageLI 函数。 private PackageParser.Package scanPackageLI(PackageParser.Package pkg, File scanFile, final int policyFlags, int scanFlags, long currentTime, @Nullable UserHandle user) throws PackageManagerException { if ((scanFlags & SCAN_CHECK_ONLY) == 0) { if (pkg.childPackages != null && pkg.childPackages.size() > 0) { scanFlags |= SCAN_CHECK_ONLY; } } else { scanFlags &= ~SCAN_CHECK_ONLY; } // Scan the parent PackageParser.Package scannedPkg = scanPackageInternalLI(pkg, scanFile, policyFlags, scanFlags, currentTime, user); // Scan the children final int childCount = (pkg.childPackages != null) ? pkg.childPackages.size() : 0; for (int i = 0; i < childCount; i++) { PackageParser.Package childPackage = pkg.childPackages.get(i); scanPackageInternalLI(childPackage, scanFile, policyFlags, scanFlags, currentTime, user); } if ((scanFlags & SCAN_CHECK_ONLY) != 0) { return scanPackageLI(pkg, scanFile, policyFlags, scanFlags, currentTime, user); } return scannedPkg; }scanPackageInternalLI调用 scanPackageInternalLI(): private PackageParser.Package scanPackageInternalLI(PackageParser.Package pkg, File scanFile, int policyFlags, int scanFlags, long currentTime, @Nullable UserHandle user) throws PackageManagerException { PackageSetting ps = null; PackageSetting updatedPkg; // 判断系统 APP 是否需要更新 synchronized (mPackages) { // 查看是否已经有该安装包,通过 mSetting 查找 String oldName = mSettings.getRenamedPackageLPr(pkg.packageName); if (pkg.mOriginalPackages != null && pkg.mOriginalPackages.contains(oldName)) { // 如果存在同一个包名的老的安装包,且已经改回原始名称了 ps = mSettings.getPackageLPr(oldName); } // 如果没有原始包,则使用真实包名 if (ps == null) { ps = mSettings.getPackageLPr(pkg.packageName); } // 查这个包是否是一个隐藏或者可以更新的系统包 updatedPkg = mSettings.getDisabledSystemPkgLPr(ps != null ? ps.name : pkg.packageName); // If this is a package we don’t know about on the system partition, we // may need to remove disabled child packages on the system partition // or may need to not add child packages if the parent apk is updated // on the data partition and no longer defines this child package. if ((policyFlags & PackageParser.PARSE_IS_SYSTEM) != 0) { // If this is a parent package for an updated system app and this system // app got an OTA update which no longer defines some of the child packages // we have to prune them from the disabled system packages. PackageSetting disabledPs = mSettings.getDisabledSystemPkgLPr(pkg.packageName); if (disabledPs != null) { final int scannedChildCount = (pkg.childPackages != null) ? pkg.childPackages.size() : 0; final int disabledChildCount = disabledPs.childPackageNames != null ? disabledPs.childPackageNames.size() : 0; for (int i = 0; i < disabledChildCount; i++) { String disabledChildPackageName = disabledPs.childPackageNames.get(i); boolean disabledPackageAvailable = false; for (int j = 0; j < scannedChildCount; j++) { PackageParser.Package childPkg = pkg.childPackages.get(j); if (childPkg.packageName.equals(disabledChildPackageName)) { disabledPackageAvailable = true; break; } } if (!disabledPackageAvailable) { mSettings.removeDisabledSystemPackageLPw(disabledChildPackageName); } } } } } … … // Note that we invoke the following method only if we are about to unpack an application // 调用第二个 scanPackageLI 函数 PackageParser.Package scannedPkg = scanPackageLI(pkg, policyFlags, scanFlags | SCAN_UPDATE_SIGNATURE, currentTime, user); … … }scanPackageLI - 02 private PackageParser.Package scanPackageLI(File scanFile, int parseFlags, int scanFlags, long currentTime, UserHandle user) throws PackageManagerException { // 创建一个 PackageParser 对象,用于解析包 PackageParser pp = new PackageParser(); // 设置 PackageParse 的三个属性 pp.setSeparateProcesses(mSeparateProcesses); pp.setOnlyCoreApps(mOnlyCore); pp.setDisplayMetrics(mMetrics); pp.setCallback(mPackageParserCallback); // 判断扫描模式 if ((scanFlags & SCAN_TRUSTED_OVERLAY) != 0) { parseFlags |= PackageParser.PARSE_TRUSTED_OVERLAY; } // 解析APK获取对应PackageParser.Package对象 pkg final PackageParser.Package pkg; // 调用 PackageParser 的 parsePackage 函数解析 APK 文件 try { // ???? ???? ???? ???? ???? ???? 真正的解析 pkg = pp.parsePackage(scanFile, parseFlags); } catch (PackageParserException e) { throw PackageManagerException.from(e); } finally { Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); } // Static shared libraries have synthetic package names if (pkg.applicationInfo.isStaticSharedLibrary()) { renameStaticSharedLibraryPackage(pkg); } return scanPackageLI(pkg, scanFile, parseFlags, scanFlags, currentTime, user); }PackageParserPackageParser 主要负责 APK 文件的解析,即解析 APK 文件中的 AndroidManifest.xml。这边我们要重点分析 PackageParser 类!!!官方解释Android 安装一个 APK 的时候首先会解析 APK,而解析 APK 则需要用到一个工具类,这个工具类就是 PackageParser。/** * Parser for package files (APKs) on disk. This supports apps packaged either * as a single “monolithic” APK, or apps packaged as a “cluster” of multiple * APKs in a single directory. * <p> * Apps packaged as multiple APKs always consist of a single “base” APK (with a * {@code null} split name) and zero or more “split” APKs (with unique split * names). Any subset of those split APKs are a valid install, as long as the * following constraints are met: * <ul> * <li>All APKs must have the exact same package name, version code, and signing * certificates. * <li>All APKs must have unique split names. * <li>All installations must contain a single base APK. * </ul> * * @hide /public class PackageParser {解释如下:解析磁盘上的APK安装包文件。它既能解析一个"单一"APK文件,也能解析一个"集群"APK文件(即一个APK文件里面包含多个APK文件)。一个"集群"APK有一个"基准"APK(base APK)组成和其他一些"分割"APK(“split” APKs)构成,其中这些"分割"APK用一些数字来分割。这些"分割"APK的必须都是有效的安装,同时必须满足下面的几个条件: ✨ 所有的APK必须具有完全相同的软件包名称,版本代码和签名证书 ✨ 所有的APK必须具有唯一的拆分名称 ✨ 所有安装必须包含一个单一的APK。解析步骤所以我们知道PackageParse类,它主要用来解析手机上的APK文件(支持Single APK和MultipleAPK),解析一个APK主要是分为两个步骤: ✨ 1、将APK解析成Package:即解析APK文件为Package对象的过程。 ✨ 2、将Package转化为PackageInfo:即由Package对象生成PackageInfo的过程。我们接下来看看 parsePackage 方法。parsePackage /* * Parse the package at the given location. Automatically detects if the * package is a monolithic style (single APK file) or cluster style * (directory of APKs). * <p> * This performs sanity checking on cluster style packages, such as * requiring identical package name and version codes, a single base APK, * and unique split names. * <p> * Note that this <em>does not</em> perform signature verification; that * must be done separately in {@link #collectCertificates(Package, int)}. * * If {@code useCaches} is true, the package parser might return a cached * result from a previous parse of the same {@code packageFile} with the same * {@code flags}. Note that this method does not check whether {@code packageFile} * has changed since the last parse, it’s up to callers to do so. * * @see #parsePackageLite(File, int) / public Package parsePackage(File packageFile, int flags) throws PackageParserException { return parsePackage(packageFile, flags, false / useCaches /); } public Package parsePackage(File packageFile, int flags, boolean useCaches) throws PackageParserException { Package parsed = useCaches ? getCachedResult(packageFile, flags) : null; if (parsed != null) { return parsed; } long parseTime = LOG_PARSE_TIMINGS ? SystemClock.uptimeMillis() : 0; if (packageFile.isDirectory()) { // 集群APK parsed = parseClusterPackage(packageFile, flags); // 获得一个 XML 资源解析对象 } else { // 单一APK parsed = parseMonolithicPackage(packageFile, flags); // 获得一个 XML 资源解析对象 } long cacheTime = LOG_PARSE_TIMINGS ? SystemClock.uptimeMillis() : 0; cacheResult(packageFile, flags, parsed); if (LOG_PARSE_TIMINGS) { parseTime = cacheTime - parseTime; cacheTime = SystemClock.uptimeMillis() - cacheTime; if (parseTime + cacheTime > LOG_PARSE_TIMINGS_THRESHOLD_MS) { Slog.i(TAG, “Parse times for ‘” + packageFile + “’: parse=” + parseTime + “ms, update_cache=” + cacheTime + " ms”); } } return parsed; }parseMonolithicPackage这边我们以单一APK为例去跟踪源码流程: /* * Parse the given APK file, treating it as as a single monolithic package. * <p> * Note that this <em>does not</em> perform signature verification; that * must be done separately in {@link #collectCertificates(Package, int)}. * * @deprecated external callers should move to * {@link #parsePackage(File, int)}. Eventually this method will * be marked private. */ @Deprecated public Package parseMonolithicPackage(File apkFile, int flags) throws PackageParserException { final AssetManager assets = newConfiguredAssetManager(); // ???? ???? ???? ???? ???? ???? 重点分析 final PackageLite lite = parseMonolithicPackageLite(apkFile, flags); if (mOnlyCoreApps) { if (!lite.coreApp) { throw new PackageParserException(INSTALL_PARSE_FAILED_MANIFEST_MALFORMED, “Not a coreApp: " + apkFile); } } try { // ???? ???? ???? ???? ???? ???? 重点分析 // 调用parseBaseApk()方法解析一个apk并生成一个Package对象 final Package pkg = parseBaseApk(apkFile, assets, flags); pkg.setCodePath(apkFile.getAbsolutePath()); pkg.setUse32bitAbi(lite.use32bitAbi); return pkg; } finally { IoUtils.closeQuietly(assets); } }【说明】:未分析完,由于篇幅字数限制,剩下代码的分析见【Android 8.1 源码_核心篇 – 深入研究 PackageManagerService 系列(3)】 ...

October 26, 2018 · 10 min · jiezi

Android 8.1 源码_核心篇 -- 深入研究 PackageManagerService 系列(3)

开篇核心源码关键类路径SystemServer.javaframeworks/base/services/java/com/android/server/SystemServer.javaPackageManagerService.javaframeworks/base/services/core/java/com/android/server/pm/PackageManagerService.javaProcess.javaframeworks/base/core/java/android/os/Process.javaSystemConfig.javaframeworks/base/core/java/com/android/server/SystemConfig.javaSettings.javaframeworks/base/services/core/java/com/android/server/pm/Settings.java简介PackageManagerService(PMS)是 SystemServer 启动后的第一个核心服务,也是 Android 系统中最常用的服务之一。它负责系统中 Package 的管理,应用程序的安装、卸载、信息查询等。如果你是面向 Android 系统开发的工程师,基础概念我也不需要再多赘述,我们直接跟源码。PackageParserparseMonolithicPackageLite private static PackageLite parseMonolithicPackageLite(File packageFile, int flags) throws PackageParserException { final ApkLite baseApk = parseApkLite(packageFile, flags); final String packagePath = packageFile.getAbsolutePath(); return new PackageLite(packagePath, baseApk, null, null, null, null, null, null); }这个方法就是调用parseApkLite()方法来获取一个ApkLite对象,然后用这个ApkLite对象构造一个PackageLite对象。parseApkLite-01 /** * Utility method that retrieves lightweight details about a single APK * file, including package name, split name, and install location. * * @param apkFile path to a single APK * @param flags optional parse flags, such as * {@link #PARSE_COLLECT_CERTIFICATES} / public static ApkLite parseApkLite(File apkFile, int flags) throws PackageParserException { final String apkPath = apkFile.getAbsolutePath(); // 创建 AssetManager 对象 AssetManager assets = null; XmlResourceParser parser = null; try { assets = newConfiguredAssetManager(); int cookie = assets.addAssetPath(apkPath); if (cookie == 0) { throw new PackageParserException(INSTALL_PARSE_FAILED_NOT_APK, “Failed to parse " + apkPath); } final DisplayMetrics metrics = new DisplayMetrics(); metrics.setToDefaults(); parser = assets.openXmlResourceParser(cookie, ANDROID_MANIFEST_FILENAME); final Signature[] signatures; final Certificate[][] certificates; // 设置签名 if ((flags & PARSE_COLLECT_CERTIFICATES) != 0) { // TODO: factor signature related items out of Package object final Package tempPkg = new Package((String) null); Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, “collectCertificates”); try { collectCertificates(tempPkg, apkFile, flags); } finally { Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); } signatures = tempPkg.mSignatures; certificates = tempPkg.mCertificates; } else { signatures = null; certificates = null; } final AttributeSet attrs = parser; return parseApkLite(apkPath, parser, attrs, signatures, certificates); } catch (XmlPullParserException | IOException | RuntimeException e) { Slog.w(TAG, “Failed to parse " + apkPath, e); throw new PackageParserException(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION, “Failed to parse " + apkPath, e); } finally { // 关闭资源管理器与解析器 IoUtils.closeQuietly(parser); IoUtils.closeQuietly(assets); } }parseApkLite-02 private static ApkLite parseApkLite(String codePath, XmlPullParser parser, AttributeSet attrs, Signature[] signatures, Certificate[][] certificates) throws IOException, XmlPullParserException, PackageParserException { // 调用了parsePackageSplitNames()方法来获取packageSplit final Pair<String, String> packageSplit = parsePackageSplitNames(parser, attrs); int installLocation = PARSE_DEFAULT_INSTALL_LOCATION; int versionCode = 0; int revisionCode = 0; boolean coreApp = false; boolean debuggable = false; boolean multiArch = false; boolean use32bitAbi = false; boolean extractNativeLibs = true; boolean isolatedSplits = false; boolean isFeatureSplit = false; String configForSplit = null; String usesSplitName = null; // 遍历属性,并获取相应的值 for (int i = 0; i < attrs.getAttributeCount(); i++) { final String attr = attrs.getAttributeName(i); if (attr.equals(“installLocation”)) { installLocation = attrs.getAttributeIntValue(i, PARSE_DEFAULT_INSTALL_LOCATION); } else if (attr.equals(“versionCode”)) { versionCode = attrs.getAttributeIntValue(i, 0); } else if (attr.equals(“revisionCode”)) { revisionCode = attrs.getAttributeIntValue(i, 0); } else if (attr.equals(“coreApp”)) { coreApp = attrs.getAttributeBooleanValue(i, false); } else if (attr.equals(“isolatedSplits”)) { isolatedSplits = attrs.getAttributeBooleanValue(i, false); } else if (attr.equals(“configForSplit”)) { configForSplit = attrs.getAttributeValue(i); } else if (attr.equals(“isFeatureSplit”)) { isFeatureSplit = attrs.getAttributeBooleanValue(i, false); } } // Only search the tree when the tag is directly below <manifest> int type; final int searchDepth = parser.getDepth() + 1; final List<VerifierInfo> verifiers = new ArrayList<VerifierInfo>(); // private static final String TAG_PACKAGE_VERIFIER = “package-verifier” // 继续解析package-verifier节点 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT && (type != XmlPullParser.END_TAG || parser.getDepth() >= searchDepth)) { if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { continue; } if (parser.getDepth() != searchDepth) { continue; } if (TAG_PACKAGE_VERIFIER.equals(parser.getName())) { final VerifierInfo verifier = parseVerifier(attrs); if (verifier != null) { verifiers.add(verifier); } } else if (TAG_APPLICATION.equals(parser.getName())) { for (int i = 0; i < attrs.getAttributeCount(); ++i) { final String attr = attrs.getAttributeName(i); if (“debuggable”.equals(attr)) { debuggable = attrs.getAttributeBooleanValue(i, false); } if (“multiArch”.equals(attr)) { multiArch = attrs.getAttributeBooleanValue(i, false); } if (“use32bitAbi”.equals(attr)) { use32bitAbi = attrs.getAttributeBooleanValue(i, false); } if (“extractNativeLibs”.equals(attr)) { extractNativeLibs = attrs.getAttributeBooleanValue(i, true); } } } else if (TAG_USES_SPLIT.equals(parser.getName())) { if (usesSplitName != null) { Slog.w(TAG, “Only one <uses-split> permitted. Ignoring others.”); continue; } usesSplitName = attrs.getAttributeValue(ANDROID_RESOURCES, “name”); if (usesSplitName == null) { throw new PackageParserException( PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED, “<uses-split> tag requires ‘android:name’ attribute”); } } } //利用解析出来的数据去构造一个ApkLite对象 return new ApkLite(codePath, packageSplit.first, packageSplit.second, isFeatureSplit, configForSplit, usesSplitName, versionCode, revisionCode, installLocation, verifiers, signatures, certificates, coreApp, debuggable, multiArch, use32bitAbi, extractNativeLibs, isolatedSplits); }parsePackageSplitNames// 这个方法主要是解析manifest private static Pair<String, String> parsePackageSplitNames(XmlPullParser parser, AttributeSet attrs) throws IOException, XmlPullParserException, PackageParserException { int type; while ((type = parser.next()) != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT) { } if (type != XmlPullParser.START_TAG) { throw new PackageParserException(INSTALL_PARSE_FAILED_MANIFEST_MALFORMED, “No start tag found”); } if (!parser.getName().equals(TAG_MANIFEST)) { throw new PackageParserException(INSTALL_PARSE_FAILED_MANIFEST_MALFORMED, “No <manifest> tag”); } final String packageName = attrs.getAttributeValue(null, “package”); if (!“android”.equals(packageName)) { // 这个方法主要就是检测是否是数字、字母、下划线和点分隔符,这也是取包名的规则, // 比如是字母数字下划线加点分隔符,否则都不是合法的应用包名。并且合法的包名至少包含一个点分隔符。 final String error = validateName(packageName, true, true); if (error != null) { throw new PackageParserException(INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME, “Invalid manifest package: " + error); } } String splitName = attrs.getAttributeValue(null, “split”); if (splitName != null) { if (splitName.length() == 0) { splitName = null; } else { final String error = validateName(splitName, false, false); if (error != null) { throw new PackageParserException(INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME, “Invalid manifest split: " + error); } } } return Pair.create(packageName.intern(), (splitName != null) ? splitName.intern() : splitName); }parseBaseApk private Package parseBaseApk(File apkFile, AssetManager assets, int flags) throws PackageParserException { final String apkPath = apkFile.getAbsolutePath(); String volumeUuid = null; if (apkPath.startsWith(MNT_EXPAND)) { final int end = apkPath.indexOf(’/’, MNT_EXPAND.length()); volumeUuid = apkPath.substring(MNT_EXPAND.length(), end); } mParseError = PackageManager.INSTALL_SUCCEEDED; mArchiveSourcePath = apkFile.getAbsolutePath(); if (DEBUG_JAR) Slog.d(TAG, “Scanning base APK: " + apkPath); final int cookie = loadApkIntoAssetManager(assets, apkPath, flags); Resources res = null; XmlResourceParser parser = null; try { res = new Resources(assets, mMetrics, null); parser = assets.openXmlResourceParser(cookie, ANDROID_MANIFEST_FILENAME); final String[] outError = new String[1]; final Package pkg = parseBaseApk(apkPath, res, parser, flags, outError); if (pkg == null) { throw new PackageParserException(mParseError, apkPath + " (at " + parser.getPositionDescription() + “): " + outError[0]); } pkg.setVolumeUuid(volumeUuid); pkg.setApplicationVolumeUuid(volumeUuid); pkg.setBaseCodePath(apkPath); pkg.setSignatures(null); return pkg; } catch (PackageParserException e) { throw e; } catch (Exception e) { throw new PackageParserException(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION, “Failed to read manifest from " + apkPath, e); } finally { IoUtils.closeQuietly(parser); } }parseBaseApk private Package parseBaseApk(String apkPath, Resources res, XmlResourceParser parser, int flags, String[] outError) throws XmlPullParserException, IOException { final String splitName; final String pkgName; try { Pair<String, String> packageSplit = parsePackageSplitNames(parser, parser); pkgName = packageSplit.first; splitName = packageSplit.second; if (!TextUtils.isEmpty(splitName)) { outError[0] = “Expected base APK, but found split " + splitName; mParseError = PackageManager.INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME; return null; } } catch (PackageParserException e) { mParseError = PackageManager.INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME; return null; } if (mCallback != null) { String[] overlayPaths = mCallback.getOverlayPaths(pkgName, apkPath); if (overlayPaths != null && overlayPaths.length > 0) { for (String overlayPath : overlayPaths) { res.getAssets().addOverlayPath(overlayPath); } } } // 用包名构造一个Package final Package pkg = new Package(pkgName); // 获取资源数组 TypedArray sa = res.obtainAttributes(parser, com.android.internal.R.styleable.AndroidManifest); // 初始化pkg的属性mVersionCode、baseRevisionCode和mVersionName pkg.mVersionCode = pkg.applicationInfo.versionCode = sa.getInteger( com.android.internal.R.styleable.AndroidManifest_versionCode, 0); pkg.baseRevisionCode = sa.getInteger( com.android.internal.R.styleable.AndroidManifest_revisionCode, 0); pkg.mVersionName = sa.getNonConfigurationString( com.android.internal.R.styleable.AndroidManifest_versionName, 0); if (pkg.mVersionName != null) { pkg.mVersionName = pkg.mVersionName.intern(); } // 判断是不是核心app pkg.coreApp = parser.getAttributeBooleanValue(null, “coreApp”, false); sa.recycle(); // 解析AndroidManifest下面的每一个节点 return parseBaseApkCommon(pkg, null, res, parser, flags, outError); }parseBaseApkCommon private Package parseBaseApkCommon(Package pkg, Set<String> acceptedTags, Resources res, XmlResourceParser parser, int flags, String[] outError) throws XmlPullParserException, IOException { … … / Set the global “forward lock” flag / if ((flags & PARSE_FORWARD_LOCK) != 0) { pkg.applicationInfo.privateFlags |= ApplicationInfo.PRIVATE_FLAG_FORWARD_LOCK; } / Set the global “on SD card” flag / // 是否要安装在SD卡上 if ((flags & PARSE_EXTERNAL_STORAGE) != 0) { pkg.applicationInfo.flags |= ApplicationInfo.FLAG_EXTERNAL_STORAGE; } // Resource boolean are -1, so 1 means we don’t know the value. int supportsSmallScreens = 1; int supportsNormalScreens = 1; int supportsLargeScreens = 1; int supportsXLargeScreens = 1; int resizeable = 1; int anyDensity = 1; int outerDepth = parser.getDepth(); while ((type = parser.next()) != XmlPullParser.END_DOCUMENT && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { continue; } … … // 解析<application> 标签 if (tagName.equals(TAG_APPLICATION)) { if (foundApp) { if (RIGID_PARSER) { outError[0] = “<manifest> has more than one <application>”; mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; return null; } else { Slog.w(TAG, “<manifest> has more than one <application>”); XmlUtils.skipCurrentTag(parser); continue; } } foundApp = true; if (!parseBaseApplication(pkg, res, parser, flags, outError)) { return null; } // 解析<overlay> 标签 } else if (tagName.equals(TAG_OVERLAY)) { sa = res.obtainAttributes(parser, com.android.internal.R.styleable.AndroidManifestResourceOverlay); pkg.mOverlayTarget = sa.getString( com.android.internal.R.styleable.AndroidManifestResourceOverlay_targetPackage); pkg.mOverlayPriority = sa.getInt( com.android.internal.R.styleable.AndroidManifestResourceOverlay_priority, 0); pkg.mIsStaticOverlay = sa.getBoolean( com.android.internal.R.styleable.AndroidManifestResourceOverlay_isStatic, false); final String propName = sa.getString( com.android.internal.R.styleable .AndroidManifestResourceOverlay_requiredSystemPropertyName); final String propValue = sa.getString( com.android.internal.R.styleable .AndroidManifestResourceOverlay_requiredSystemPropertyValue); sa.recycle(); if (pkg.mOverlayTarget == null) { outError[0] = “<overlay> does not specify a target package”; mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; return null; } if (pkg.mOverlayPriority < 0 || pkg.mOverlayPriority > 9999) { outError[0] = “<overlay> priority must be between 0 and 9999”; mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; return null; } // check to see if overlay should be excluded based on system property condition if (!checkOverlayRequiredSystemProperty(propName, propValue)) { Slog.i(TAG, “Skipping target and overlay pair " + pkg.mOverlayTarget + " and " + pkg.baseCodePath+ “: overlay ignored due to required system property: " + propName + " with value: " + propValue); return null; } XmlUtils.skipCurrentTag(parser); // 解析<key-sets> 标签 } else if (tagName.equals(TAG_KEY_SETS)) { if (!parseKeySets(pkg, res, parser, outError)) { return null; } // 解析<permission-group> 标签 } else if (tagName.equals(TAG_PERMISSION_GROUP)) { if (!parsePermissionGroup(pkg, flags, res, parser, outError)) { return null; } // 解析<permission> 标签 } else if (tagName.equals(TAG_PERMISSION)) { if (!parsePermission(pkg, res, parser, outError)) { return null; } // 解析<permission-tree> 标签 } else if (tagName.equals(TAG_PERMISSION_TREE)) { if (!parsePermissionTree(pkg, res, parser, outError)) { return null; } // 解析<uses-permission> 标签 } else if (tagName.equals(TAG_USES_PERMISSION)) { if (!parseUsesPermission(pkg, res, parser)) { return null; } // 解析<uses-permission-sdk-m> 标签或者 <uses-permission-sdk-23> 标签 } else if (tagName.equals(TAG_USES_PERMISSION_SDK_M) || tagName.equals(TAG_USES_PERMISSION_SDK_23)) { if (!parseUsesPermission(pkg, res, parser)) { return null; } // 解析<uses-configuration>标签 } else if (tagName.equals(TAG_USES_CONFIGURATION)) { ConfigurationInfo cPref = new ConfigurationInfo(); sa = res.obtainAttributes(parser, com.android.internal.R.styleable.AndroidManifestUsesConfiguration); cPref.reqTouchScreen = sa.getInt( com.android.internal.R.styleable.AndroidManifestUsesConfiguration_reqTouchScreen, Configuration.TOUCHSCREEN_UNDEFINED); cPref.reqKeyboardType = sa.getInt( com.android.internal.R.styleable.AndroidManifestUsesConfiguration_reqKeyboardType, Configuration.KEYBOARD_UNDEFINED); if (sa.getBoolean( com.android.internal.R.styleable.AndroidManifestUsesConfiguration_reqHardKeyboard, false)) { cPref.reqInputFeatures |= ConfigurationInfo.INPUT_FEATURE_HARD_KEYBOARD; } cPref.reqNavigation = sa.getInt( com.android.internal.R.styleable.AndroidManifestUsesConfiguration_reqNavigation, Configuration.NAVIGATION_UNDEFINED); if (sa.getBoolean( com.android.internal.R.styleable.AndroidManifestUsesConfiguration_reqFiveWayNav, false)) { cPref.reqInputFeatures |= ConfigurationInfo.INPUT_FEATURE_FIVE_WAY_NAV; } sa.recycle(); pkg.configPreferences = ArrayUtils.add(pkg.configPreferences, cPref); XmlUtils.skipCurrentTag(parser); // 解析<uses-feature>标签 } else if (tagName.equals(TAG_USES_FEATURE)) { FeatureInfo fi = parseUsesFeature(res, parser); pkg.reqFeatures = ArrayUtils.add(pkg.reqFeatures, fi); if (fi.name == null) { ConfigurationInfo cPref = new ConfigurationInfo(); cPref.reqGlEsVersion = fi.reqGlEsVersion; pkg.configPreferences = ArrayUtils.add(pkg.configPreferences, cPref); } XmlUtils.skipCurrentTag(parser); // 解析<feature-group>标签 } else if (tagName.equals(TAG_FEATURE_GROUP)) { FeatureGroupInfo group = new FeatureGroupInfo(); ArrayList<FeatureInfo> features = null; final int innerDepth = parser.getDepth(); while ((type = parser.next()) != XmlPullParser.END_DOCUMENT && (type != XmlPullParser.END_TAG || parser.getDepth() > innerDepth)) { if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { continue; } final String innerTagName = parser.getName(); if (innerTagName.equals(“uses-feature”)) { FeatureInfo featureInfo = parseUsesFeature(res, parser); // FeatureGroups are stricter and mandate that // any <uses-feature> declared are mandatory. featureInfo.flags |= FeatureInfo.FLAG_REQUIRED; features = ArrayUtils.add(features, featureInfo); } else { Slog.w(TAG, “Unknown element under <feature-group>: " + innerTagName + " at " + mArchiveSourcePath + " " + parser.getPositionDescription()); } XmlUtils.skipCurrentTag(parser); } if (features != null) { group.features = new FeatureInfo[features.size()]; group.features = features.toArray(group.features); } pkg.featureGroups = ArrayUtils.add(pkg.featureGroups, group); // 解析<uses-sdk>标签 } else if (tagName.equals(TAG_USES_SDK)) { if (SDK_VERSION > 0) { sa = res.obtainAttributes(parser, com.android.internal.R.styleable.AndroidManifestUsesSdk); int minVers = 1; String minCode = null; int targetVers = 0; String targetCode = null; TypedValue val = sa.peekValue( com.android.internal.R.styleable.AndroidManifestUsesSdk_minSdkVersion); if (val != null) { if (val.type == TypedValue.TYPE_STRING && val.string != null) { targetCode = minCode = val.string.toString(); } else { // If it’s not a string, it’s an integer. targetVers = minVers = val.data; } } val = sa.peekValue( com.android.internal.R.styleable.AndroidManifestUsesSdk_targetSdkVersion); if (val != null) { if (val.type == TypedValue.TYPE_STRING && val.string != null) { targetCode = val.string.toString(); if (minCode == null) { minCode = targetCode; } } else { // If it’s not a string, it’s an integer. targetVers = val.data; } } sa.recycle(); final int minSdkVersion = PackageParser.computeMinSdkVersion(minVers, minCode, SDK_VERSION, SDK_CODENAMES, outError); if (minSdkVersion < 0) { mParseError = PackageManager.INSTALL_FAILED_OLDER_SDK; return null; } final int targetSdkVersion = PackageParser.computeTargetSdkVersion(targetVers, targetCode, SDK_VERSION, SDK_CODENAMES, outError); if (targetSdkVersion < 0) { mParseError = PackageManager.INSTALL_FAILED_OLDER_SDK; return null; } pkg.applicationInfo.minSdkVersion = minSdkVersion; pkg.applicationInfo.targetSdkVersion = targetSdkVersion; } XmlUtils.skipCurrentTag(parser); // 解析<supports-screens>标签 } else if (tagName.equals(TAG_SUPPORT_SCREENS)) { sa = res.obtainAttributes(parser, com.android.internal.R.styleable.AndroidManifestSupportsScreens); pkg.applicationInfo.requiresSmallestWidthDp = sa.getInteger( com.android.internal.R.styleable.AndroidManifestSupportsScreens_requiresSmallestWidthDp, 0); pkg.applicationInfo.compatibleWidthLimitDp = sa.getInteger( com.android.internal.R.styleable.AndroidManifestSupportsScreens_compatibleWidthLimitDp, 0); pkg.applicationInfo.largestWidthLimitDp = sa.getInteger( com.android.internal.R.styleable.AndroidManifestSupportsScreens_largestWidthLimitDp, 0); // This is a trick to get a boolean and still able to detect // if a value was actually set. supportsSmallScreens = sa.getInteger( com.android.internal.R.styleable.AndroidManifestSupportsScreens_smallScreens, supportsSmallScreens); supportsNormalScreens = sa.getInteger( com.android.internal.R.styleable.AndroidManifestSupportsScreens_normalScreens, supportsNormalScreens); supportsLargeScreens = sa.getInteger( com.android.internal.R.styleable.AndroidManifestSupportsScreens_largeScreens, supportsLargeScreens); supportsXLargeScreens = sa.getInteger( com.android.internal.R.styleable.AndroidManifestSupportsScreens_xlargeScreens, supportsXLargeScreens); resizeable = sa.getInteger( com.android.internal.R.styleable.AndroidManifestSupportsScreens_resizeable, resizeable); anyDensity = sa.getInteger( com.android.internal.R.styleable.AndroidManifestSupportsScreens_anyDensity, anyDensity); sa.recycle(); XmlUtils.skipCurrentTag(parser); // 解析<protected-broadcast>标签 } else if (tagName.equals(TAG_PROTECTED_BROADCAST)) { sa = res.obtainAttributes(parser, com.android.internal.R.styleable.AndroidManifestProtectedBroadcast); // Note: don’t allow this value to be a reference to a resource // that may change. String name = sa.getNonResourceString( com.android.internal.R.styleable.AndroidManifestProtectedBroadcast_name); sa.recycle(); if (name != null && (flags&PARSE_IS_SYSTEM) != 0) { if (pkg.protectedBroadcasts == null) { pkg.protectedBroadcasts = new ArrayList<String>(); } if (!pkg.protectedBroadcasts.contains(name)) { pkg.protectedBroadcasts.add(name.intern()); } } XmlUtils.skipCurrentTag(parser); // 解析<instrumentation>标签 } else if (tagName.equals(TAG_INSTRUMENTATION)) { if (parseInstrumentation(pkg, res, parser, outError) == null) { return null; } // 解析<original-package>标签 } else if (tagName.equals(TAG_ORIGINAL_PACKAGE)) { sa = res.obtainAttributes(parser, com.android.internal.R.styleable.AndroidManifestOriginalPackage); String orig =sa.getNonConfigurationString( com.android.internal.R.styleable.AndroidManifestOriginalPackage_name, 0); if (!pkg.packageName.equals(orig)) { if (pkg.mOriginalPackages == null) { pkg.mOriginalPackages = new ArrayList<String>(); pkg.mRealPackage = pkg.packageName; } pkg.mOriginalPackages.add(orig); } sa.recycle(); XmlUtils.skipCurrentTag(parser); // 解析<adopt-permissions>标签 } else if (tagName.equals(TAG_ADOPT_PERMISSIONS)) { sa = res.obtainAttributes(parser, com.android.internal.R.styleable.AndroidManifestOriginalPackage); String name = sa.getNonConfigurationString( com.android.internal.R.styleable.AndroidManifestOriginalPackage_name, 0); sa.recycle(); if (name != null) { if (pkg.mAdoptPermissions == null) { pkg.mAdoptPermissions = new ArrayList<String>(); } pkg.mAdoptPermissions.add(name); } XmlUtils.skipCurrentTag(parser); // 解析<uses-gl-texture>标签 } else if (tagName.equals(TAG_USES_GL_TEXTURE)) { // Just skip this tag XmlUtils.skipCurrentTag(parser); continue; // 解析<compatible-screens>标签 } else if (tagName.equals(TAG_COMPATIBLE_SCREENS)) { // Just skip this tag XmlUtils.skipCurrentTag(parser); continue; // 解析<supports-input>标签 } else if (tagName.equals(TAG_SUPPORTS_INPUT)) {// XmlUtils.skipCurrentTag(parser); continue; // 解析<eat-comment>标签 } else if (tagName.equals(TAG_EAT_COMMENT)) { // Just skip this tag XmlUtils.skipCurrentTag(parser); continue; } else if (tagName.equals(TAG_PACKAGE)) { if (!MULTI_PACKAGE_APK_ENABLED) { XmlUtils.skipCurrentTag(parser); continue; } if (!parseBaseApkChild(pkg, res, parser, flags, outError)) { // If parsing a child failed the error is already set return null; } } else if (tagName.equals(TAG_RESTRICT_UPDATE)) { if ((flags & PARSE_IS_SYSTEM_DIR) != 0) { sa = res.obtainAttributes(parser, com.android.internal.R.styleable.AndroidManifestRestrictUpdate); final String hash = sa.getNonConfigurationString( com.android.internal.R.styleable.AndroidManifestRestrictUpdate_hash, 0); sa.recycle(); pkg.restrictUpdateHash = null; if (hash != null) { final int hashLength = hash.length(); final byte[] hashBytes = new byte[hashLength / 2]; for (int i = 0; i < hashLength; i += 2){ hashBytes[i/2] = (byte) ((Character.digit(hash.charAt(i), 16) << 4) + Character.digit(hash.charAt(i + 1), 16)); } pkg.restrictUpdateHash = hashBytes; } } XmlUtils.skipCurrentTag(parser); } else if (RIGID_PARSER) { outError[0] = “Bad element under <manifest>: " + parser.getName(); mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; return null; } else { Slog.w(TAG, “Unknown element under <manifest>: " + parser.getName() + " at " + mArchiveSourcePath + " " + parser.getPositionDescription()); XmlUtils.skipCurrentTag(parser); continue; } }parseBaseApplication private boolean parseBaseApplication(Package owner, Resources res, XmlResourceParser parser, int flags, String[] outError) throws XmlPullParserException, IOException { // 获取ApplicationInfo对象ai final ApplicationInfo ai = owner.applicationInfo; // 获取包名 final String pkgName = owner.applicationInfo.packageName; // 从资源里面获取AndroidManifest的数组 TypedArray sa = res.obtainAttributes(parser, com.android.internal.R.styleable.AndroidManifestApplication); if (!parsePackageItemInfo(owner, ai, outError, “<application>”, sa, false /nameRequired/, com.android.internal.R.styleable.AndroidManifestApplication_name, com.android.internal.R.styleable.AndroidManifestApplication_label, com.android.internal.R.styleable.AndroidManifestApplication_icon, com.android.internal.R.styleable.AndroidManifestApplication_roundIcon, com.android.internal.R.styleable.AndroidManifestApplication_logo, com.android.internal.R.styleable.AndroidManifestApplication_banner)) { sa.recycle(); mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; return false; } if (ai.name != null) { ai.className = ai.name; } // 在AndroidManifest里面是否设置了android:manageSpaceActivity属性, // 如果设置了则manageSpaceActivity不为空,没有设置manageSpaceActivity为空 String manageSpaceActivity = sa.getNonConfigurationString( com.android.internal.R.styleable.AndroidManifestApplication_manageSpaceActivity, Configuration.NATIVE_CONFIG_VERSION); if (manageSpaceActivity != null) { // 如果设置了,则添加类名 ai.manageSpaceActivityName = buildClassName(pkgName, manageSpaceActivity, outError); } // 是否设置了androidMannifest.xml文件中android:allowBackup属性; boolean allowBackup = sa.getBoolean( com.android.internal.R.styleable.AndroidManifestApplication_allowBackup, true); // 如果设置了允许备份 if (allowBackup) { ai.flags |= ApplicationInfo.FLAG_ALLOW_BACKUP; // backupAgent, killAfterRestore, fullBackupContent, backupInForeground, // and restoreAnyVersion are only relevant if backup is possible for the // given application. // 获取backupAgent,如果在AndroidManifest里面设置了android:backupAgent属性, // 则backupAgent不为空,否则backupAgent为空 String backupAgent = sa.getNonConfigurationString( com.android.internal.R.styleable.AndroidManifestApplication_backupAgent, Configuration.NATIVE_CONFIG_VERSION); // 设置了backupAgent,这构建类名 if (backupAgent != null) { ai.backupAgentName = buildClassName(pkgName, backupAgent, outError); if (DEBUG_BACKUP) { Slog.v(TAG, “android:backupAgent = " + ai.backupAgentName + " from " + pkgName + “+” + backupAgent); } if (sa.getBoolean( com.android.internal.R.styleable.AndroidManifestApplication_killAfterRestore, true)) { ai.flags |= ApplicationInfo.FLAG_KILL_AFTER_RESTORE; } if (sa.getBoolean( com.android.internal.R.styleable.AndroidManifestApplication_restoreAnyVersion, false)) { ai.flags |= ApplicationInfo.FLAG_RESTORE_ANY_VERSION; } if (sa.getBoolean( com.android.internal.R.styleable.AndroidManifestApplication_fullBackupOnly, false)) { ai.flags |= ApplicationInfo.FLAG_FULL_BACKUP_ONLY; } if (sa.getBoolean( com.android.internal.R.styleable.AndroidManifestApplication_backupInForeground, false)) { ai.privateFlags |= ApplicationInfo.PRIVATE_FLAG_BACKUP_IN_FOREGROUND; } } TypedValue v = sa.peekValue( com.android.internal.R.styleable.AndroidManifestApplication_fullBackupContent); if (v != null && (ai.fullBackupContent = v.resourceId) == 0) { if (DEBUG_BACKUP) { Slog.v(TAG, “fullBackupContent specified as boolean=” + (v.data == 0 ? “false” : “true”)); } // “false” => -1, “true” => 0 ai.fullBackupContent = (v.data == 0 ? -1 : 0); } if (DEBUG_BACKUP) { Slog.v(TAG, “fullBackupContent=” + ai.fullBackupContent + " for " + pkgName); } } ai.theme = sa.getResourceId( com.android.internal.R.styleable.AndroidManifestApplication_theme, 0); ai.descriptionRes = sa.getResourceId( com.android.internal.R.styleable.AndroidManifestApplication_description, 0); if ((flags&PARSE_IS_SYSTEM) != 0) { if (sa.getBoolean( com.android.internal.R.styleable.AndroidManifestApplication_persistent, false)) { // Check if persistence is based on a feature being present final String requiredFeature = sa.getNonResourceString( com.android.internal.R.styleable. AndroidManifestApplication_persistentWhenFeatureAvailable); if (requiredFeature == null || mCallback.hasFeature(requiredFeature)) { ai.flags |= ApplicationInfo.FLAG_PERSISTENT; } } } if (sa.getBoolean( com.android.internal.R.styleable.AndroidManifestApplication_requiredForAllUsers, false)) { owner.mRequiredForAllUsers = true; } String restrictedAccountType = sa.getString(com.android.internal.R.styleable .AndroidManifestApplication_restrictedAccountType); if (restrictedAccountType != null && restrictedAccountType.length() > 0) { owner.mRestrictedAccountType = restrictedAccountType; } String requiredAccountType = sa.getString(com.android.internal.R.styleable .AndroidManifestApplication_requiredAccountType); if (requiredAccountType != null && requiredAccountType.length() > 0) { owner.mRequiredAccountType = requiredAccountType; } if (sa.getBoolean( com.android.internal.R.styleable.AndroidManifestApplication_debuggable, false)) { ai.flags |= ApplicationInfo.FLAG_DEBUGGABLE; } if (sa.getBoolean( com.android.internal.R.styleable.AndroidManifestApplication_vmSafeMode, false)) { ai.flags |= ApplicationInfo.FLAG_VM_SAFE_MODE; } owner.baseHardwareAccelerated = sa.getBoolean( com.android.internal.R.styleable.AndroidManifestApplication_hardwareAccelerated, owner.applicationInfo.targetSdkVersion >= Build.VERSION_CODES.ICE_CREAM_SANDWICH); if (owner.baseHardwareAccelerated) { ai.flags |= ApplicationInfo.FLAG_HARDWARE_ACCELERATED; } if (sa.getBoolean( com.android.internal.R.styleable.AndroidManifestApplication_hasCode, true)) { ai.flags |= ApplicationInfo.FLAG_HAS_CODE; } if (sa.getBoolean( com.android.internal.R.styleable.AndroidManifestApplication_allowTaskReparenting, false)) { ai.flags |= ApplicationInfo.FLAG_ALLOW_TASK_REPARENTING; } if (sa.getBoolean( com.android.internal.R.styleable.AndroidManifestApplication_allowClearUserData, true)) { ai.flags |= ApplicationInfo.FLAG_ALLOW_CLEAR_USER_DATA; } // The parent package controls installation, hence specify test only installs. if (owner.parentPackage == null) { if (sa.getBoolean( com.android.internal.R.styleable.AndroidManifestApplication_testOnly, false)) { ai.flags |= ApplicationInfo.FLAG_TEST_ONLY; } } if (sa.getBoolean( com.android.internal.R.styleable.AndroidManifestApplication_largeHeap, false)) { ai.flags |= ApplicationInfo.FLAG_LARGE_HEAP; } if (sa.getBoolean( com.android.internal.R.styleable.AndroidManifestApplication_usesCleartextTraffic, true)) { ai.flags |= ApplicationInfo.FLAG_USES_CLEARTEXT_TRAFFIC; } if (sa.getBoolean( com.android.internal.R.styleable.AndroidManifestApplication_supportsRtl, false / default is no RTL support*/)) { ai.flags |= ApplicationInfo.FLAG_SUPPORTS_RTL; } if (sa.getBoolean( com.android.internal.R.styleable.AndroidManifestApplication_multiArch, false)) { ai.flags |= ApplicationInfo.FLAG_MULTIARCH; } if (sa.getBoolean( com.android.internal.R.styleable.AndroidManifestApplication_extractNativeLibs, true)) { ai.flags |= ApplicationInfo.FLAG_EXTRACT_NATIVE_LIBS; } if (sa.getBoolean( R.styleable.AndroidManifestApplication_defaultToDeviceProtectedStorage, false)) { ai.privateFlags |= ApplicationInfo.PRIVATE_FLAG_DEFAULT_TO_DEVICE_PROTECTED_STORAGE; } if (sa.getBoolean( R.styleable.AndroidManifestApplication_directBootAware, false)) { ai.privateFlags |= ApplicationInfo.PRIVATE_FLAG_DIRECT_BOOT_AWARE; } if (sa.hasValueOrEmpty(R.styleable.AndroidManifestApplication_resizeableActivity)) { if (sa.getBoolean(R.styleable.AndroidManifestApplication_resizeableActivity, true)) { ai.privateFlags |= PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_RESIZEABLE; } else { ai.privateFlags |= PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_UNRESIZEABLE; } } else if (owner.applicationInfo.targetSdkVersion >= Build.VERSION_CODES.N) { ai.privateFlags |= PRIVATE_FLAG_ACTIVITIES_RESIZE_MODE_RESIZEABLE_VIA_SDK_VERSION; } ai.maxAspectRatio = sa.getFloat(R.styleable.AndroidManifestApplication_maxAspectRatio, 0); ai.networkSecurityConfigRes = sa.getResourceId( com.android.internal.R.styleable.AndroidManifestApplication_networkSecurityConfig, 0); ai.category = sa.getInt( com.android.internal.R.styleable.AndroidManifestApplication_appCategory, ApplicationInfo.CATEGORY_UNDEFINED); String str; str = sa.getNonConfigurationString( com.android.internal.R.styleable.AndroidManifestApplication_permission, 0); ai.permission = (str != null && str.length() > 0) ? str.intern() : null; if (owner.applicationInfo.targetSdkVersion >= Build.VERSION_CODES.FROYO) { str = sa.getNonConfigurationString( com.android.internal.R.styleable.AndroidManifestApplication_taskAffinity, Configuration.NATIVE_CONFIG_VERSION); } else { // Some older apps have been seen to use a resource reference // here that on older builds was ignored (with a warning). We // need to continue to do this for them so they don’t break. str = sa.getNonResourceString( com.android.internal.R.styleable.AndroidManifestApplication_taskAffinity); } ai.taskAffinity = buildTaskAffinityName(ai.packageName, ai.packageName, str, outError); if (outError[0] == null) { CharSequence pname; if (owner.applicationInfo.targetSdkVersion >= Build.VERSION_CODES.FROYO) { pname = sa.getNonConfigurationString( com.android.internal.R.styleable.AndroidManifestApplication_process, Configuration.NATIVE_CONFIG_VERSION); } else { // Some older apps have been seen to use a resource reference // here that on older builds was ignored (with a warning). We // need to continue to do this for them so they don’t break. pname = sa.getNonResourceString( com.android.internal.R.styleable.AndroidManifestApplication_process); } ai.processName = buildProcessName(ai.packageName, null, pname, flags, mSeparateProcesses, outError); ai.enabled = sa.getBoolean( com.android.internal.R.styleable.AndroidManifestApplication_enabled, true); if (sa.getBoolean( com.android.internal.R.styleable.AndroidManifestApplication_isGame, false)) { ai.flags |= ApplicationInfo.FLAG_IS_GAME; } if (false) { if (sa.getBoolean( com.android.internal.R.styleable.AndroidManifestApplication_cantSaveState, false)) { ai.privateFlags |= ApplicationInfo.PRIVATE_FLAG_CANT_SAVE_STATE; // A heavy-weight application can not be in a custom process. // We can do direct compare because we intern all strings. if (ai.processName != null && ai.processName != ai.packageName) { outError[0] = “cantSaveState applications can not use custom processes”; } } } } ai.uiOptions = sa.getInt( com.android.internal.R.styleable.AndroidManifestApplication_uiOptions, 0); ai.classLoaderName = sa.getString( com.android.internal.R.styleable.AndroidManifestApplication_classLoader); if (ai.classLoaderName != null && !ClassLoaderFactory.isValidClassLoaderName(ai.classLoaderName)) { outError[0] = “Invalid class loader name: " + ai.classLoaderName; } sa.recycle(); if (outError[0] != null) { mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; return false; } final int innerDepth = parser.getDepth(); // IMPORTANT: These must only be cached for a single <application> to avoid components // getting added to the wrong package. final CachedComponentArgs cachedArgs = new CachedComponentArgs(); int type; // 开始解析 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT && (type != XmlPullParser.END_TAG || parser.getDepth() > innerDepth)) { if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { continue; } String tagName = parser.getName(); // 解析activity if (tagName.equals(“activity”)) { Activity a = parseActivity(owner, res, parser, flags, outError, cachedArgs, false, owner.baseHardwareAccelerated); if (a == null) { mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; return false; } owner.activities.add(a); // 解析receiver } else if (tagName.equals(“receiver”)) { Activity a = parseActivity(owner, res, parser, flags, outError, cachedArgs, true, false); if (a == null) { mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; return false; } owner.receivers.add(a); // 解析service } else if (tagName.equals(“service”)) { Service s = parseService(owner, res, parser, flags, outError, cachedArgs); if (s == null) { mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; return false; } owner.services.add(s); // 解析provider } else if (tagName.equals(“provider”)) { Provider p = parseProvider(owner, res, parser, flags, outError, cachedArgs); if (p == null) { mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; return false; } owner.providers.add(p); … … } else { if (!RIGID_PARSER) { Slog.w(TAG, “Unknown element under <application>: " + tagName + " at " + mArchiveSourcePath + " " + parser.getPositionDescription()); XmlUtils.skipCurrentTag(parser); continue; } else { outError[0] = “Bad element under <application>: " + tagName; mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED; return false; } } } // Must be ran after the entire {@link ApplicationInfo} has been fully processed and after // every activity info has had a chance to set it from its attributes. setMaxAspectRatio(owner); PackageBackwardCompatibility.modifySharedLibraries(owner); // 检查IntentFilter之一是否包含DEFAULT / VIEW和HTTP / HTTPS数据URI if (hasDomainURLs(owner)) { owner.applicationInfo.privateFlags |= ApplicationInfo.PRIVATE_FLAG_HAS_DOMAIN_URLS; } else { owner.applicationInfo.privateFlags &= ~ApplicationInfo.PRIVATE_FLAG_HAS_DOMAIN_URLS; } return true; }其实这个方法就是解析了application节点下的所有信息,比如activity、service、receiver、provider、library、users-librayry等信息,同时将解析后的每一个属性生成相应的对象,添加到传入的package里面,这些信息最后都会在PackageManagerService中用到。扫描非系统 Package if (!mOnlyCore) { // mOnlyCore 用于控制是否扫描非系统 Package // do this first before mucking with mPackages for the “expecting better” case final Iterator<PackageParser.Package> pkgIterator = mPackages.values().iterator(); while (pkgIterator.hasNext()) { final PackageParser.Package pkg = pkgIterator.next(); if (pkg.isStub) { stubSystemApps.add(pkg.packageName); } } final Iterator<PackageSetting> psit = mSettings.mPackages.values().iterator(); while (psit.hasNext()) { PackageSetting ps = psit.next(); /* * If this is not a system app, it can’t be a * disable system app. / if ((ps.pkgFlags & ApplicationInfo.FLAG_SYSTEM) == 0) { /// M: Operator apps are belong to system domain, therefore, need prune. /// We should also consider OTA from old version without mtkFlag if (sPmsExt.isNotOperatorApp(ps)) continue; } / * If the package is scanned, it’s not erased. / final PackageParser.Package scannedPkg = mPackages.get(ps.name); if (scannedPkg != null) { / * If the system app is both scanned and in the * disabled packages list, then it must have been * added via OTA. Remove it from the currently * scanned package so the previously user-installed * application can be scanned. / if (mSettings.isDisabledSystemPackageLPr(ps.name)) { logCriticalInfo(Log.WARN, “Expecting better updated system app for " + ps.name + “; removing system app. Last known codePath=” + ps.codePathString + “, installStatus=” + ps.installStatus + “, versionCode=” + ps.versionCode + “; scanned versionCode=” + scannedPkg.mVersionCode); removePackageLI(scannedPkg, true); mExpectingBetter.put(ps.name, ps.codePath); } continue; } if (!mSettings.isDisabledSystemPackageLPr(ps.name)) { psit.remove(); logCriticalInfo(Log.WARN, “System package " + ps.name + " no longer exists; it’s data will be wiped”); // Actual deletion of code and data will be handled by later // reconciliation step } else { // we still have a disabled system package, but, it still might have // been removed. check the code path still exists and check there’s // still a package. the latter can happen if an OTA keeps the same // code path, but, changes the package name. final PackageSetting disabledPs = mSettings.getDisabledSystemPkgLPr(ps.name); if (disabledPs.codePath == null || !disabledPs.codePath.exists() || disabledPs.pkg == null) { possiblyDeletedUpdatedSystemApps.add(ps.name); } } } } if (!mOnlyCore) { scanDirTracedLI(mAppInstallDir, 0, scanFlags | SCAN_REQUIRE_KNOWN, 0); scanDirTracedLI(mDrmAppPrivateInstallDir, mDefParseFlags | PackageParser.PARSE_FORWARD_LOCK, scanFlags | SCAN_REQUIRE_KNOWN, 0); // Remove disable package settings for updated system apps that were // removed via an OTA. If the update is no longer present, remove the // app completely. Otherwise, revoke their system privileges. for (String deletedAppName : possiblyDeletedUpdatedSystemApps) { PackageParser.Package deletedPkg = mPackages.get(deletedAppName); mSettings.removeDisabledSystemPackageLPw(deletedAppName); final String msg; if (deletedPkg == null) { // should have found an update, but, we didn’t; remove everything msg = “Updated system package " + deletedAppName + " no longer exists; removing its data”; // Actual deletion of code and data will be handled by later // reconciliation step } else { // found an update; revoke system privileges msg = “Updated system package + " + deletedAppName + " no longer exists; revoking system privileges”; // Don’t do anything if a stub is removed from the system image. If // we were to remove the uncompressed version from the /data partition, // this is where it’d be done. final PackageSetting deletedPs = mSettings.mPackages.get(deletedAppName); deletedPkg.applicationInfo.flags &= ~ApplicationInfo.FLAG_SYSTEM; deletedPs.pkgFlags &= ~ApplicationInfo.FLAG_SYSTEM; /// M: [Operator] Revoke operator permissions for the original operator sPmsExt.clearExtFlags(deletedPkg, deletedPs); } logCriticalInfo(Log.WARN, msg); } / * Make sure all system apps that we expected to appear on * the userdata partition actually showed up. If they never * appeared, crawl back and revive the system version. */ for (int i = 0; i < mExpectingBetter.size(); i++) { final String packageName = mExpectingBetter.keyAt(i); if (!mPackages.containsKey(packageName)) { final File scanFile = mExpectingBetter.valueAt(i); logCriticalInfo(Log.WARN, “Expected better " + packageName + " but never showed up; reverting to system”); int reparseFlags = mDefParseFlags; if (FileUtils.contains(privilegedAppDir, scanFile)) { reparseFlags = PackageParser.PARSE_IS_SYSTEM | PackageParser.PARSE_IS_SYSTEM_DIR | PackageParser.PARSE_IS_PRIVILEGED; } else if (FileUtils.contains(systemAppDir, scanFile)) { reparseFlags = PackageParser.PARSE_IS_SYSTEM | PackageParser.PARSE_IS_SYSTEM_DIR; } else if (FileUtils.contains(vendorAppDir, scanFile)) { reparseFlags = PackageParser.PARSE_IS_SYSTEM | PackageParser.PARSE_IS_SYSTEM_DIR; } else if (FileUtils.contains(oemAppDir, scanFile)) { reparseFlags = PackageParser.PARSE_IS_SYSTEM | PackageParser.PARSE_IS_SYSTEM_DIR; } else { Slog.e(TAG, “Ignoring unexpected fallback path " + scanFile); continue; } mSettings.enableSystemPackageLPw(packageName); try { scanPackageTracedLI(scanFile, reparseFlags, scanFlags, 0, null); } catch (PackageManagerException e) { Slog.e(TAG, “Failed to parse original system package: " + e.getMessage()); } } } // Uncompress and install any stubbed system applications. // This must be done last to ensure all stubs are replaced or disabled. decompressSystemApplications(stubSystemApps, scanFlags); final int cachedNonSystemApps = PackageParser.sCachedPackageReadCount.get() - cachedSystemApps; final long dataScanTime = SystemClock.uptimeMillis() - systemScanTime - startTime; final int dataPackagesCount = mPackages.size() - systemPackagesCount; Slog.i(TAG, “Finished scanning non-system apps. Time: " + dataScanTime + " ms, packageCount: " + dataPackagesCount + " , timePerPackage: " + (dataPackagesCount == 0 ? 0 : dataScanTime / dataPackagesCount) + " , cached: " + cachedNonSystemApps); if (mIsUpgrade && dataPackagesCount > 0) { MetricsLogger.histogram(null, “ota_package_manager_data_app_avg_scan_time”, ((int) dataScanTime) / dataPackagesCount); } }构造函数分析 - 扫尾工作这部分任务比较简单,就是将第二阶段手机的信息再集中整理一次,可自行研究。总结第二阶段分析就此结束! ...

October 26, 2018 · 20 min · jiezi

Android 四大组件 -- Service 解析(用法篇)

开篇服务是什么?服务(Service)是 Android 中实现程序后台运行的解决方案,它非常适合去执行那些不需要和用户交互而且还需要长期进行的任务。服务的运行不依赖于任何用户界面,即使程序被切换到后台,或者用户打开了另外一个应用程序,服务仍然能够保持正常运行。不过需要注意的是,服务并不是运行在一个独立的进程当中的,而是依赖于创建服务时所在的应用程序进程。当某个应用程序进程被杀掉时,所有依赖于该进程的服务也会停止运行。Service 使用方法定义一个服务首先,我们定义一个 MyService.java 类,当然作为一个服务类,必须要继承 Service(android.app.Service),看代码:// 源码路径:frameworks/base/core/java/android/app/Service.javapublic abstract class Service extends ContextWrapper implements ComponentCallbacks2 { private static final String TAG = “Service”; … … @Nullable public abstract IBinder onBind(Intent intent); … …}Service 定义了一个抽象方法 onBind,子类继承它,必须复写此方法。public class MyService extends Service{ @Nullable @Override public IBinder onBind(Intent intent) { // 这是一个抽象方法,那么子类是必须要重写的 return null; }}服务既然已经定义好了,自然应该在服务中去处理一些事情,那处理事情的逻辑应该写在哪里?我们需要在服务里重写 Service 中的另一些常用的方法:public class MyService extends Service{ @Nullable @Override public IBinder onBind(Intent intent) { // 这是一个抽象方法,那么子类是必须要重写的 return null; } @Override public void onCreate() { // 服务创建时调用 super.onCreate(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { // 服务启动时调用 return super.onStartCommand(intent, flags, startId); } @Override public void onDestroy() { // 服务销毁时调用 super.onDestroy(); }}和添加 Activity 一样,我们添加了一个服务,那么在 AndroidManifest.xml 文件中必须进行注册才能生效!<?xml version=“1.0” encoding=“utf-8”?><manifest xmlns:android=“http://schemas.android.com/apk/res/android" package=“com.example.xin02ma.myapplication”> <application android:allowBackup=“true” android:icon="@mipmap/ic_launcher” …… <activity android:name=".MainActivity"> …… </activity> <service android:name=".MyService" android:enabled=“true” android:exported=“true” /> </application></manifest>启动和停止服务服务定义好了,接下来就应该考虑如何去启动以及停止这个服务了。(1)先添加两个Button(activity_main.xml)<Button android:id="@+id/start_service" android:layout_width=“match_parent” android:layout_height=“wrap_content” android:text="@string/start_service" /><Button android:id="@+id/stop_service" android:layout_width=“match_parent” android:layout_height=“wrap_content” android:text="@string/stop_service" /><center></center> (2)接下来,修改主函数 MainActivity 的代码:public class MainActivity extends Activity implements View.OnClickListener{ private Button startService; private Button stopService; protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 采用布局 startService = (Button) super.findViewById(R.id.start_service); // 取得Button实例 stopService = (Button) super.findViewById(R.id.stop_service); // 取得Button实例 startService.setOnClickListener(this); // 监控Button,注册按钮事件 stopService.setOnClickListener(this); // 监控Button,注册按钮时间 } public void onClick(View v) { switch (v.getId()) { case R.id.start_service: Intent startIntent = new Intent(this, MyService.class); startService(startIntent); // 启动服务 break; case R.id.stop_service: Intent stopIntent = new Intent(this, MyService.class); stopService(stopIntent); // 停止服务 break; default: break; } }}上面的代码很简单,主要作了以下工作:(1)取得 startService 和 stopService 两个按钮实例,并且注册了点击事件;(2)通过 Intent 对象,调用 Activity 的 startService() 和 stopService() 方法来启动和停止服务。 【Notice】 这里的活动的启动和停止完全是由活动本身控制的,如果我们 start 了服务,但是没有点击 stop,那么服务会一直处于运行状态,此时服务如何让自己停止下来?只需要在 MyService 的任何一个位置调用 stopSelf() 这个方法就能让服务停下来!Log测试添加Log,查看Service是如何运作的:public class MyService extends Service{ private static final String TAG = “MyService”; @Nullable @Override public IBinder onBind(Intent intent) { // 这是一个抽象方法,那么子类是必须要重写的 return null; } public void onCreate() { // 服务创建时调用 super.onCreate(); Log.d(TAG, “onCreate executed”); } public int onStartCommand(Intent intent, int flags, int startId) { // 服务启动时调用 Log.d(TAG, “onStartCommand executed”); return super.onStartCommand(intent, flags, startId); } public void onDestroy() { // 服务销毁时调用 super.onDestroy(); Log.d(TAG, “onDestroy executed”); }}添加了3行Log,目的就是看:在我们点击两个按钮的时候,整个Service什么时候创建,什么时候启动,什么时候毁灭!我们来看一下执行结果,运行程序,查看Logcat中的打印日志:(1)第一次点击 StartService按钮 后,MyService中的 onCreate() 和 onStartCommand() 方法都执行了,图中黄色箭头所示!此时,我们可以在 手机 –> 设置 –> 应用 –> 运行中 可以看到这个服务:(2)然后我们点击 stopService按钮 后,MyService中的 onDestory() 方法被执行,图中蓝色箭头所示!(3)此时可能你会有一个疑问?当我们点击了 startService按钮 以后, onCreate() 和 onStartCommand() 方法同时被执行,这两个方法有什么区别?图中的 红色箭头 给了我们答案: onCreat() 方法是在服务第一次创建的时候调用的,而 onStartCommand() 方法则在每次启动服务的时候都会被调用。当我们在 服务未启动 的时候,点击 startService 按钮 ,则此时会 执行两个方法 ;但是 服务启动完成 之后,再次点击(随便你点几次) startService按钮 ,你会发现 只有onStartCommand()方法被执行 。Service生命周期上面介绍完 Service 的使用方法,接下来看看 Service 的 生命周期 :跟Activity相比,Service的生命周期很简单: onCreate()->onStart()->onDestroy() 我们以如下的方式展开这章节的讨论工作! 【主题】 :Activity 与 Service之间的 Communication 【问题】 :由上贴我们知道,当我们点击 START SERVICE 按钮后,服务的 onCreate() 和 onStartCommand() 方法会得到执行,此后 Service 是一直存在于后台运行的,Activity 无法控制 Service 中具体的逻辑运行,那么这样 Activity 只相当于起到一个通知的作用,除了告诉 Service 你可以开始工作了。那么这样显然会分离两者之间的关联性,这也不是我们需要的结果! 【后果】 :如果出现以上的问题,那么在我们平时的项目开发过程中,一直存在的 Service 很有可能会引起功耗的问题,可能影响手机的运行效率! 【要求】 :我们能否将 Activity 与 Service 建立一种联系,当 Activity 终结之时,Service 也销毁,也就是有没有办法让 Activity 和 Service 能够 “不求同生,但求共死” ? 答案是肯定的! 这就涉及到 Service 的另一个重要知识点: 绑定 与 解绑 !还是以代码为例:MyService MyService.java public class MyService extends Service{ private static final String TAG = “MyService”; private DownloadBinder mBinder = new DownloadBinder(); // 定义一个 DownloadBinder 类 class DownloadBinder extends Binder { // 让 DownloadBinder 成为 Binder 的子类 public void startDownload() { // 定义开始下载的方法 Log.d(TAG, “startDownload executed”); } public int getProgress() { // 定义一个查看下载进度的方法 Log.d(TAG, “getProgress executed”); return 0; } } @Nullable @Override public IBinder onBind(Intent intent) { // onBind()方法,这个方法将在绑定后调用 return mBinder; // 返回 IBinder 的实例 –> DownloadBinder 类的实例 } public void onCreate() { super.onCreate(); Log.d(TAG, “onCreate executed”); } public int onStartCommand(Intent intent, int flags, int startId) { Log.d(TAG, “onStartCommand executed”); return super.onStartCommand(intent, flags, startId); } public void onDestroy() { super.onDestroy(); Log.d(TAG, “onDestroy executed”); }}BIND SERVICE / UNBIND SERVICE我们在Layout中添加两个按钮 BIND SERVICE 和 UNBIND SERVICE : MainActivity.java public class MainActivity extends Activity implements View.OnClickListener{ private ServiceConnection connection = new ServiceConnection() { // 创建一个 ServiceConnection 的匿名内部类 @Override public void onServiceConnected(ComponentName name, IBinder service) { // 重写 onServiceConnected() 方法 MyService.DownloadBinder downloadBinder = (MyService.DownloadBinder) service; // 向下转型取得 downloadBinder 实例 downloadBinder.startDownload(); // 在 Activity 中调用 Service 的方法 downloadBinder.getProgress(); // 在 Activity 中调用 Service 的方法 } @Override public void onServiceDisconnected(ComponentName name) { // 重写onServiceDisconnected()方法 } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button startService = (Button) super.findViewById(R.id.start_service); Button stopService = (Button) super.findViewById(R.id.stop_service); Button bindService = (Button) super.findViewById(R.id.bind_service); Button unbindService = (Button) super.findViewById(R.id.unbind_service); startService.setOnClickListener(this); stopService.setOnClickListener(this); bindService.setOnClickListener(this); unbindService.setOnClickListener(this); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.start_service: Intent startIntent = new Intent(this, MyService.class); // START 服务 –> onCreate() –> onStartCommand() startService(startIntent); break; case R.id.stop_service: Intent stopIntent = new Intent(this, MyService.class); // STOP 服务 –> onDestroy() stopService(stopIntent); break; case R.id.bind_service: // 绑定 –> ? Intent bindIntent = new Intent(this, MyService.class); bindService(bindIntent, connection, BIND_AUTO_CREATE); // ???? ???? ???? ???? 重点分析 break; case R.id.unbind_service: // 解绑 –> ? unbindService(connection); break; default: break; } }}bindService看一下 bindService(bindIntent, connection, BIND_AUTO_CREATE) 这个方法: bindService 接收了 3 个参数: bindIntent :这个参数传入的就是我们的 intent,目的就是调用 MyService 这个服务。 connection :这个参数传入的就是创建好的 ServiceConnection 的实例,这个参数代表着我们的 Activity 是要和 Service 绑定在一起的! BIND_AUTO_CREATE :这是一个 FLAG,表示在活动和服务进行绑定后 自动创建服务 。注意!是自动创建服务,也就是说 MyService 会执行 onCreate() 方法,但是不会执行 onStartCommand() 方法! 接下来,直接看代码最终的效果: 通过排列组合,对按钮进行点击,Log分 3 种情况: START SERVICE + STOP SERVICE: 1、当我们先点击 START SERVICE :此时服务启动,调用 onCreat() 和 onStartCommand() 方法;2、当我们后点击 STOP SERVICE :此时,服务被销毁,调用 onDestroy() 方法。 BIND SERVICE + UNBIND SERVICE: 1、当我们先点击 BIND SERVICE :此时服务仅仅是创建,并未启动!所以调用的只是 onCreate() 方法。此时 Activity 与 Service 绑定,会同时调用 onBind() 方法,此时 onServiceConnected() 方法会被执行,还记的 onBind() 方法的返回类型不?我们通过 Log 可以很明显发现,Activity 调用了服务内部的两个自定义方法。2、当我们后点击 UNBIND SERVICE :由于服务还未启动,而 BIND SERVICE 只是将服务创建好并与活动进行绑定,那么解绑后,势必会销毁这个 Service,所以 onDestroy() 被执行! START SERVICE + BIND SERVICE + UNBIND SERVICE + STOP SERVICE: 1、我们先点击 START SERVICE :onCreat() 和 onStartCommand() 方法被执行,这个就不用多说了; 2、然后点击 BIND SERVICE :这个时候其实活动已经在后台运行了,我们此时将活动和服务绑定,那么 onCreate() 不会再执行,只会执行 onServiceConnected() 方法,Log 里面打出来看的很清楚。3、此时你如果手贱,想 STOP SERVICE:那么恭喜你,毫无反应!为什么?因为你都没解绑,你怎么销毁?4、OK,那我们先解绑,我们点击 UNBIND SERVICE :此时一个奇怪的现象发生了,LOG 日志没有打印出 Destroy() 这个方法啊?没有被执行啊!不是说 bind 了 Service 之后,unbind 就会销毁这个服务吗?这跟我们之前分析的不符合啊。5、好吧,我们来看看为什么。其实原因很简单:我们先 start 了 Service,那么此时服务已经在后台运行了,这个时候你 bind,让 Service 和 Activity 绑定,其实是没有什么意义的。但是既然绑定了,你如果不解绑,那么 Destroy() 毫无用武,所以,这种情况和(2)中分析的还是有区别的,此是解绑完后,服务还是舒舒服服的在后台运行,所以,要想干掉这个服务,你必须要 STOP SERVICE。6、那我们解绑后,再 STOP SERVICE :这个时候 Service 就被枪毙了!Service 两个实用小技巧Forground Service服务几乎都是在后台运行的,一直一来它都是默默地做着辛苦的工作。但是服务的系统优先级还是比较低的,当系统出现内存不足的情况时,就有可能会回收掉正在后台运行的服务。如果你希望服务可以一直保持运行状态,而不是由于系统内存不足的原因导致被回收掉,就可以考虑使用前台服务。前台服务和普通服务最大的区别就在于,它会一直有一个正在运行的图标在系统的状态栏显示,下拉状态栏后可以看到更加详细的信息,非常类似于通知的效果。当然有时候你也可能不仅仅是为了防止服务被回收掉才使用前台服务,有些项目由于特殊的需求会要求必须使用前台服务,比如说天气类软件,它的服务在后台更新天气数据的同时,还会在系统状态栏一直显示当前的天气信息。 【问题】 :我们都知道服务是运行在后台的,如果系统出现内存不足的情况,那么此时,系统就可能回收后台的服务,那么我们如何保证服务可以一直运行? 【解决】 :在服务中,有一个 前台服务 的概念,调用 startForground() 方法可以实现。如何创建一个前台服务,看代码:public class MyService extends Service{ …… @Override public void onCreate() { super.onCreate(); Log.d(“MyService”, “onCreate executed”); Intent intent = new Intent(this, MainActivity.class); PendingIntent pi = PendingIntent.getActivity(this, 0, intent, 0); Notification notification = new Notification.Builder(this) // 启动服务后,在前台添加一个Notification .setContentTitle(“This is a content title”) .setContentText(“This is content text”) .setWhen(System.currentTimeMillis()) .setSmallIcon(R.mipmap.ic_launcher) .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher)) .setContentIntent(pi) .build(); startForeground(1, notification); } … …}以上的代码是在 Service 的创建中添加了一个 Notification,调用 startForground() 就可以保证:只要服务一直存在,那么在前台就会一直显示这个 Notification。如果我们在 onDestroy() 中调用 stopForground() 方法,会销毁这个 Notification,但是 Service 还是存活的,此时 Service 就会面临被 System 干掉的风险。如果直接 STOP SERVICE,那么 Notification 和 Service 都会销毁。 IntentService 【问题】 :我们知道服务的代码逻辑是在主线程中执行的,如果我们在主线程中需要执行一些耗时的操作,那么很有可能出现ANR(程序暂无响应)的状况。这个时候,我们可以采用 Android 的多线程编程的方式,我们应该在服务的每个具体的方法里开启一个子线程,然后在这里去处理那些耗时的逻辑。所以,一个比较标准的服务就可以写成如下形式:public class MyService extends Service{ @Nullable @Override public IBinder onBind(Intent intent) { return null; } @Override public int onStartCommand(Intent intent, int flags, int startId) { new Thread(new Runnable() { // 开启一个线程处理耗时操作 @Override public void run() { // 处理具体的逻辑 } }).start(); return super.onStartCommand(intent, flags, startId); }}现在,服务可以启动起来了。但是如果不调用 StopService() 或 stopSelf() 方法,服务会一直运行,所以我们需要修改一下代码:public class MyService extends Service{ @Nullable @Override public IBinder onBind(Intent intent) { return null; } @Override public int onStartCommand(Intent intent, int flags, int startId) { new Thread(new Runnable() { @Override public void run() { // 处理具体的逻辑 // 开启一个线程处理耗时操作 stopSelf(); // 让服务执行完逻辑后自行停止 } }).start(); return super.onStartCommand(intent, flags, startId); }}上面的代码就是一个标准的 Service 的书写形式,主要包含两个知识点:Thread子线程的创建 和 stopSelf() 方法的调用。虽说这种写法并不复杂,但是总会有人忘记开启线程,或者忘记调用 stopSelf(),那么有没有更好的办法能够实现上面两个需求呢? 【解决】: 在 Android 中,专门提供了一个 IntentService 类(android.app.IntentService),这个类就能很好的满足我们的需求!我们直接通过代码来看:新建一个 MyIntentService 类继承自 IntentService,代码如下:public class MyIntentService extends IntentService{ public MyIntentService() { super(“MyIntentService”); // 调用父类的有参构造函数 } @Override protected void onHandleIntent(Intent intent) { // 打印当前线程的 id Log.d(“MyIntentService”, “MyIntentServiceThread id is " + Thread.currentThread().getId()); } @Override public void onDestroy() { super.onDestroy(); Log.d(“MyIntentService”, “onDestroy executed”); }} 以上代码做了几件事: 1、提供了一个无参的构造方法,并且调用了父类的有参构造函数;2、子类实现父类的 onHandleIntent() 抽象方法,这个方法好就好在,它是一个已经运行在子线程中的方法。也就是说,服务调用了它,那么执行的逻辑就如同 Thread 子线程;3、根据 IntentService 的特性,这个服务在运行结束后应该是会自动停止的,所以我们又重写了 onDestroy()方法,在这里也打印一行日志,以证实服务是不是停止掉了。我们在 xml 文件中,创建一个 MyIntentService 服务按钮:<Button android:id=”@+id/start_intent_service" android:layout_width=“match_parent” android:layout_height=“wrap_content” android:text="@string/intent_service"/>然后修改 MainActivity 中的代码:public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); … … Button startIntentService = (Button) super.findViewById(R.id.start_intent_service); startIntentService.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Log.d(“MyIntentService”, “MainActivity Thread id is " + Thread.currentThread().getId()); // 查看主线程的id Intent intentService = new Intent(getBaseContext(), MyIntentService.class); startService(intentService); } }); }}最后,在AndroidMainfest中注册服务:<service android:name=".MyIntentService” /> 【结果】 :从打出的LOG可以看出:1、MyIntentService 和 MainActivity 所在进程的 id是不一样的 ;2、onHandleIntent() 方法在执行完逻辑后确实销毁了服务,效果等同于 stopSelf()。从上面的分析可以看出 onHandleIntent() 方法确实相当的好用! ...

September 28, 2018 · 6 min · jiezi