利用 Android 系统原生 API 实现分享功能(2)

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了利用 Android 系统原生 API 实现分享功能(2)相关的知识,希望对你有一定的参考价值。

参考技术A

在之前的一篇文章 利用 android 系统原生 API 实现分享功能 中主要说了下实现流程,但具体实施起来其实还是有许多坑要面对。那这篇文章就是提供一个封装好的 Share2 库供大家参考。

GitHub 项目地址:Share2

看过上一篇文章的同学应该知道,要调用 Android 系统内建的分享功能,主要有三步流程:

更多相关内容请参考上一篇,这里就不再重复赘述了。

知道大致的实现流程后,其实只要解决下面几个问题后就可以具体实施了。

这其实是直接决定了最终的实现形态,我们知道常见的使用场景中,只是为了在应用间分享图片和一些文件,那对于那些只是分享文本的产品而言,两者实现起来要考虑的问题完全不同。

所以为了解决这个问题,我们可以预先定好支持的分享内容类型,针对不同类型可以进行不同的处理。

在 Share2 中,一共定义了5种类别的分享内容,基本能覆盖常见的使用场景。在调用分享接口时可以直接指定内容类型,比如像文本、图片、音视频、已经其他各种类型文件。

对于不同类别的内容,可能会有不同的来源。比如文本可能就只是一个字符串对象,而对于分享图片或其他文件,我们需要一个 Uri 来标识一个资源。这其实就引出来具体实施时的一个大问题,如何获取要分享文件的 Uri,并且这个 Uri 要能被接收分享内容的应用处理才行 。

那么,如何获取要分享内容文件的 Uri?如果处理才能让接收方也能够根据 Uri 获取到文件?

我们把文件 Uri 的来源划分为下面三种类型:

常见场景 :通过文件选择器获取一个文件的 Uri

通过这种方式获取到的 Uri 是由系统 ContentProvider 返回的,在 Android 4.4 之前的版本和之后的版本有较大的区别,我们后面再说怎么处理。只要先记住这种系统返回给我们的 Uri 就行了。

比如调用系统相机进行拍照或录制音视频,要传入一个生成目标文件的 Uri ,从 7.0 开始我们需要用到 FileProvider 来实现。

如果用到了 FileProvider 就要注意跟系统 ContentProvider 返回 Uri 的区别,比如我们在 Manifest 中对 FileProvider 配置 android:authorities="com.xx.xxx.fileProvider" 属性,那这时系统返回的 Uri 格式就变成了 : content://com.xx.xxx.fileProvider... ,对于这种类型的 Uri 我们姑且叫 自定义 FileProvider 返回的 Uri ,后面一并说怎么处理。

我们调用 new File 时需要传入指定的文件路径,这个绝对路径通常是: /storage/emulated/0/... 这种样式,我们要想调用分享时也要变成 Uri 的形式才可以,那么如何把文件路径变成一个文件 Uri ?这个问题下面也一并进行回答。

前面提到了文件 Uri 的三种分类,对应不同类型处理方式也不同,不然你最先遇到的问题就是:

这是由于对系统返回的 Uri 缺失访问权限导致,所以要对应用进行临时访问 Uri 的授权才行,不然会提示权限缺失。

对于要分享系统返回的 Uri 我们可以这样进行处理:

需要注意的是对于自定义 FileProvider 返回 Uri 的处理,即使是设置临时访问权限,但是分享到第三方应用也会无法识别该 Uri

典型的场景就是,我们如果把自定义 FileProvider 的返回的 Uri 设置分享到微信或 QQ 之类的第三方应用,会提示文件不存在,这是因为他们无法识别该 Uri。

关于这个问题的处理其实跟下面要说的把文件路径变成系统返回的 Uri 一样,我们只需要把自定义 FileProvider 返回的 Uri 变成第三方应用可以识别系统返回的 Uri 就行了。

创建 FileProvider 时需要传入一个 File 对象,所以直接可以知道文件路径,那就把问题都转换成了: 如何通过文件路径获取系统返回的 Uri

下面是根据传入的 File 对象和类型来查询系统 ContentProvider 来获取相应的 Uri,已经按照不同文件类型在不同系统版本下的进行了适配。

其中 forceGetFileUri 方法是通过反射实现的,处理 7.0 以上系统的特殊情况下的兼容性,一般情况下不会调用到。Android 7.0 开始不允许 file:// Uri 的方式在不同的 App 间共享文件,但是如果换成 FileProvider 的方式依然是无效的,我们可以通过反射把该检测干掉。

通过 File Path 转成 Uri 的方式,我们最终统一了调用系统分享时传入内容 Uri 的三种不同场景,最终全部转换为传递系统返回的 Uri,让第三方应用能够正常的获取到分享内容。

Share2 按照上述方法进行了具体实施,可以通过下面的方式进行集成:

分享图片到指定界面,比如分享到微信朋友圈

GitHub 项目地址:Share2

干货分享 | 浅析K8s API设计与实现


干货分享 | 浅析K8s API设计与实现

友情提示:全文7000多文字,预计阅读时间12分钟


导语

Kubernetes(以下简称K8s)是容器集群管理系统,是一个开源的平台,可以实现容器集群的自动化部署、自动扩缩容、维护等功能。K8s本身是一个依照云原生设计理念实现的优秀软件,我们可以深入分析并理解清楚其设计理念,并将此理念融入到自己的产品设计。关于K8s的设计理念,大致可以分为2类,API设计和控制器设计。本文主要从K8s的API声明式设计原则及实现进行深入的剖析,并提炼出一套API设计方法。本文的重点内容包括:什么是声明式API、K8s API实现原理剖析、基于云原生API设计方法。



01

什么是声明式API


任何的云平台都需要设计API,而API设计的合理性、兼容性等一般是需要在设计之初就要考虑的。每支持一项新的特性或者功能点,会引入对应API对象,支持对该功能的管理。当涉及到资源的更新、优化,通常对于基础服务API,比如K8s Apiserver服务,是项重大的考验,除了需要考虑新功能特性,更需要保证向前或者其它服务兼容的特性。


K8s的设计原则之首就是“所有的API应该都是声明式的”,这也是K8s能够“生存”的核心所在。那么什么是声明式API呢?


首先,相对于声明式,实现一套API最简单的方式就是命令式,step-by-step,准确的输入各参数,并根据命令执行过程,直到实现期望的结果,它注重的是过程(how)。


举个例子。我们知道Docker作为容器的典型开源实现,它的操作都是基于命令行的,比如:

#docker load -i ubuntu.tar #docker images# docker run -i -t -d ubuntu:latest


像这样3条命令,是在环境中启动了基于ubuntu镜像的容器,然后通过这个容器,可以访问自己的应用。而在K8s中,也可以实现同样的效果,流程是先编写好资源YAML文件,再通过定义好的资源文件(YAML格式),比如名称为ubuntu.yaml,内容如下:

kind: DeploymentapiVersion: extensions/v1beta1metadata: name: ubuntu namespace: test


我们通过在K8s中执行命令,如kubectl create -f ubuntu.yaml,启动一个容器应用。使用过K8s的同学,相信你们很熟悉YAML文件,它作为K8s声明式API的必备因素,那么kubectl create命令的操作过程,我们能否称之为声明式API的实现?


答案是:并不能。所谓声明式,简单理解就是提交一个定义好的API对象来进行“声明”,系统会去达到它期望的状态,它注重的是想要什么(what),而让系统想出如何做。我们再来理解一下上图中YAML文件中的内容,其中kind声明了类型;apiVersion声明了资源的版本等其它的参数,当然如果仅仅在K8s API中创建这个数据结构,这并不能称为完全意义上的声明式API,K8s还需要做到在系统中,尽可能去达到它期望的状态,对于示例中的Deployment资源,系统期望达到的状态是创建对应的pod(控制器实现),并提供服务。而这一切,如果仅仅通过API,用户是无法感知的,并且系统可以自主完成从“期望状态”到“实际状态”的调谐(Reconcile)过程。这里Reconcile的过程,在K8s中是通过控制器技术实现的。


如果需要引入新的特性,比如增加字段,可以采用API新版本的方式来实现。这样的API设计可以保证重复的操作效果是稳定的,更易被用户使用,系统向用户隐藏了实现的细节,这样就保证了系统未来优化、扩展的可能性。那么在K8s中,它究竟是怎么保证的呢?


答案是:在“apply”的过程中。那apply这个操作什么呢?


下面我们来看一下K8s另一个最常用的命令:kubectl apply -f ubuntu.yaml,这个命令是将文件配置应用于资源,根据配置文件里面列出来的内容,可以只更新现有的资源配置,即YAML文件的内容,可以只写需要升级的属性。

这种声明式的API(apply),与命令式的API过程最本质的区别就是,它一次可以处理多个写操作,并不会产生冲突,具备merge能力。所以K8s中的多数实现都采用apply的操作过程。

在apply过程中,K8s最重要的流程就是PATCH的实现,那么K8s是如何实现的呢?


下面我们用伪代码的形式,来梳理一下它最核心的过程。

{ //返回资源的结构对象  objectInfo := r.store.Newfunc() //根据资源key获取存储后端的resourceVersion resourceVersion := getCurrentResourceVersion() //获取当前的资源对象 currentObj := getCurrentObj() //将当前的资源对象,与待更新的资源结构内容进行合并 objToUpdate := r.applyPatchToCurrentObj() //资源的admission插件Admit过程objToUpdate := applyAdmission() //获取更新后的资源版本 resourceVersionUpdate := getUpdateObj() //对比版本信息,一致才可以创建 Err := restBeforeUpdate(version …) //根据合并后的结构更新结构 err := r.store.update(objToUpdate)}


(1)首先,获取资源的结构,然后根据请求中的key获取当前对象状态中的resource Version资源版本号,及资源数据内容。

(2)再将当前请求中的结构变化信息,和已存在的数据进行合并,生成新的资源结构。

(3)在后端更新这个新资源结构之前,还需要进行一系列插件admission的过滤与检测,比如认证、鉴权、pod的必要信息判定等等。只要通过检测,才能到后端更新新生成的对象。

(4)此外,还需要保证更新后的资源版本号,与之前系统的版本号要一致,否则不予更新,这很好的保证了资源的一致性。


经过这个流程,我们可以看到Patch请求中的数据,只是资源全量一部分,换句话说,它并不需要知道资源之前存在的完整内容,它只需要填入自己想要的一部分更新字段。这是声明式API能够处理多个写操作最重要的“根源”所在。而资源不同版本的API,它们各自定义了所需的资源格式、字段信息,因此在资源版本升级时候,原有的资源格式,只需要处理与原先定义格式匹配的字段,而后续合并的内容,并不影响它的处理逻辑。


02

K8s API实现原理剖析


在 Kubernetes 项目中,一个 API 对象在 Etcd 里的完整资源路径,是由:Group(API 组)、Version(API 版本)和 Resource(API 资源类型)三个部分组成的。

通过这样的结构,整个 Kubernetes 里的所有 API 对象,实际上就可以用如下的树形结构表示出来:

干货分享 | 浅析K8s API设计与实现


在这张图中,我们可以清晰看出K8s资源的组织方式,是层层递进的,所有资源的设计基本是一致的。那么K8s是如何来实现不同版本API的呢?资源的路由是如何实现的?


下面从实现层面来剖析一下。随着K8s的版本的迭代,API的设计也逐步趋向成熟化,实现的核心过程,如下结构简图所示。(本文只截取了API Server服务启动API安装、启动最核心的实现,更详细的实现细节,可以参考github上K8s的代码实现。)我们围绕这张图的实现细节贯穿说明,大体分为:资源不同版本安装与注册、资源GVR的实现、资源路由实现原理。


资源不同版本安装与注册




K8s的API服务在启动之前,首先需要安装API(InstallAPIs)服务,而这个过程,会针对各种storage provider(各种资源的REST storage factory),逐个获取对应资源的apiGroupInfo,包括资源所在的组、添加到scheme对象、VersionResourcesStorageMap对象等,其中VersionResourcesStorageMap是一个map[string]map[string]rest.Storage的对象,从它可以知道资源所拥有的版本,以及在各版本下,支持的资源类型,及各类型后端对应的REST存储工厂。在这个存储工厂中,可以映射资源的各种结构属性,以及通用的存储实现方法。

干货分享 | 浅析K8s API设计与实现

图1 API实现逻辑图


然后将上述得到的apiGroupInfo添加到数组对象apiGroupsInfo中,进入下一步安装API组(InstallAPIGroups),在这个过程中,最核心的调用是依次为每个apiGroupInfo,安装其对应的API资源(InstallAPIResources)。

干货分享 | 浅析K8s API设计与实现


资源GVR(Group、Version、Resource)的实现




在InstallAPIResources的过程中,需要关注apiGroupInfo的PrioritizedVersions属性,这个属性来自各资源,在初始化的时候,注册进来的。这里得明确知道,在K8s中任何资源都需要有如下一些属性,Group、Version、Kind,资源所属的组,所属的版本,所属的类型。而每一类资源初始化的时候,都会调用scheme.PrioritizedVersionsForGroup(group)的方法,而这个方法返回的是[]scheme.GroupVersion对象,比如[]{Group: “app”, Version:”v1”}。当然各资源的版本增加的时候,这个数组就会相应增加。apiGroupInfo的PrioritizedVersions属性就是上面的[]groupVersion数组,依次针对各groupVersion获取对应的APIGroupVersion对象,这个对象的属性包含了Root资源结构前缀、Storage指明了资源、Creater、Serializer、Typer(Typer指向apiGroupInfo Scheme)等。下面重要的部分是针对各APIGroupVersion进行REST安装(InstallREST)的调用,我们需要明确传递进去的参数GenericAPIServer.Handler.GoRestfulContainer的来源,回溯分析一下配置初始化的过程,这里的GenericAPIServer是从New()方法中建立的,有个NewAPIServerHandler()的方法,深入其中,看到了GoRestfulContainer属性,来自restful.NewContainer()方法。


下面简要分析一下NewContainer的过程,实质是初始化Container结构体的过程,包括了webServices、router等等,而webservice结构体中的字段包含typeNameHandleFunc,这个字段,说明处理请求对应的handler。到此处,我们终于看到了与路由、服务相关的内容,这离我们看到的API实现深处更近了一步。屏住呼吸,继续往下走。

干货分享 | 浅析K8s API设计与实现


继续回到InstallREST方法,在这个方法中,首先我们需要建立一个新的APIInstaller对象,它包括了prefix、minRequestTimeout、group等属性,其中prefix,我们可以理解为API的路径前缀,是由{Root}/{Group}/{Version}这类似的方式组成。minRequestTimeout表示请求的过期时间。group对应的是APIGroupVersion。然后通过对APIInstaller对象执行安装(Install),这个过程完成了各资源API的处理器的注册。

干货分享 | 浅析K8s API设计与实现


在Install方法中,它首先生成了webService的对象ws,返回的是一个restful.WebService对象,可以设置path、设置apiversion等。由于Install方法是来自APIInstaller对象,这个对象有group属性,我们可以根据不同的资源,找到它所在组及可以操作的Storage资源。将他们加入到paths路径中,用于下一步的注册资源处理器的过程。这里我们举例来说,比如“apps”下的组,对应的后端资源包括“deployments”、“daemonsets”等等,将这些资源加入到paths数组中。


根据上述设置的各group中的paths数组,依次为这些path,注册资源处理器,即registerResourceHandlers,当然这里需要将不同组下的所有的storage的各种资源,都注册一遍,并且同时将它注册到ws的服务中去。


资源路由实现原理




所以,到这里K8s的API最关键的方法,就是registerResourceHandlers,掌握了这个原理,就等于知道了资源、存储、后端处理器的关系。下面针对这个实现过程,进行进一步分析。这个函数,是及其之长。我们挑其中最关键的步骤,进行流程说明。


首先是Append Actions,可以理解为添加动作过程,当前区分namespaceScoped,比如node这类资源,是与namesapce无关,而deploy、pod这类资源与namespace强相关,因此针对这2种模式,需要有针对性的生成对应的actions组。其中的每一个action包括对应的Verb,即操作方法,比如POST、PUT等,还包括资源路径、资源参数等。


有了actions组之后,下面针对每一个action,进行资源路由的创建及添加(Append to route)。所以如何添加处理器到路由?是理解API最核心的环节,从图1这张简图的右半部分,我们可以看到Store是比较关键的对象,通过它可以实现各种方法对应的处理器,同时也可以访问底层的存储。那么Store是怎么来的?

干货分享 | 浅析K8s API设计与实现


通过上述的分析,我们回溯一下API的构建过程,我们发现在获得各资源的APIGroupInfo的时候,会针对各不同版本,初始化后端的storageMap,而这个storageMap实质对应的就是各资源的后端处理的各种方法,包括Create()、Update()、Delete()等,就是通用的genericregistry.Store{}的结构实现的这些方法。而得到这个对象外,还发现了一个比较重要的过程,store.CompleteWithOptions()方法。通过这个方法,将会得到Storage对象,这是资源操作最直接的结构对象,但是这个storage的对象是从RESTOptions.Decorator方法生成,如下部分截图所示:

干货分享 | 浅析K8s API设计与实现


经过同样方法分析,我们得知opts是来自RESTOptions结构的对象,因此,我们需要再次回溯API启动过程,初始化RESTOptions的过程,奔着这样的目的,终于在store.CompleteWithOptions()的方法中,看到了它的身影,有关键的一步:opts,  err := options.RESTOptions.GetRESTOptions()。在GetRESTOptions方法中,由于配置的存储后端是etcd,所以会调用etcd相关的实现方法,(f *StorageFactoryRestOptionsFactory) GetRESTOptions(resource schema.GroupResource),这是找到Decorator最关键的步骤,找到了Decorator就等于找到了存储的后端的直接实现。

干货分享 | 浅析K8s API设计与实现


我们简要分析一下GETRESTOptions的过程,发现了Decorator的由来,并且还区分了是否开启监听缓存,这似乎也找到了优化APIServer的一项机制,可以通过开启缓存的方式。以其中一种“不开启缓存”的模式来说明,它的流程是通过factory.Create()方法,生成对应的存储操作接口对象及销毁的接口对象。当然我们还看到了存储的后端,在K8s 1.18以上的版本中,默认使用V3版本的客户端进行连接。


通过factory.Create方法,可以到达基于V3版本的etcd集群的客户端连接,而这个连接相关联的结构对象,实现了诸多的方法,包括Create、Put、Get、Watch等方法。


继续回到上图1中的右半部分,我们此时不难理解,Storage对象,实际对应的是资源后端的存储逻辑,而它调用的Decorator过程,实际是操作etcd的各自实现方法。


通过上述复杂的过程,我们完成了API实现过程的剖析。可以看出K8s的声明式的API实现,没有过分的突出每一类资源的接口,而是将资源以可注册的方式,即等同于可插拔的方式,安装不同的版本,整个实现,版本之间不会相互依赖,也不会因为版本的更新,引发原始功能问题。新增加的版本,可以增加特定的新的属性,而这个属性的实现,可以在控制器逻辑中实现管控。


03

基于云原生API设计方法


通过对K8s的API实现分析,我们从中受到启发,声明式的API,比较适合基于云原生基础服务,尤其是API版本迭代比较频繁的场景。下面我们有针对性地总结一种基于云原生的API设计方法,支持API的无缝扩容、优化,并提供对不同版本的兼容性。它的整体结构设计如上如所示,API设计遵循REST风格,所有的资源处于特定的group模块,并通过group中的gvk结构注册到scheme center中,其中gvk表明group、version、kind,举例来说,“app”、“v1”、“Deployment”,这表明资源的模块属于app应用模块,v1是它的版本号,Deployment是它的资源类型。这样设计的好处是,可以动态实现资源的安装与注册、实现资源版本的动态优化、更新。并且注册中心采用scheme的模式,它有比较丰富的结构与方法,可以实现资源的统一纳管。

干货分享 | 浅析K8s API设计与实现


然后,通过对注册到scheme center中的所有资源,分别安装对应的handler,此处的handler用于处理数据,连接到特定的存储设备storage,比如etcd、mysql等。


基于这种结构,客户端的请求REST client在访问API服务之前,需要经过通用的插件流程,比如authentication认证模块,authorization鉴权模块等。通过这些模块后,再访问API服务。而API服务将对应的请求发到指定资源的handler上去,再由handler完成数据的CRUD。


到这里,这种API设计完成了声明式API设计的一大半工作。从用户侧来说,通过声明式的API规范,可以完成资源的创建。但是,服务端如何完成真正资源的操作,还需要对应的资源Controller完成。即在这种设计中,controller服务监听对应的资源,并按照资源的格式,完成它在后端的状态,即在后端实现这种数据结构的过程。


通过K8s API实现的深入剖析,我们从一定层面上了解了K8s ApiServer的工作原理,而这距离玩转K8s的框架,还有很长的路要走。下一步我们将针对控制器的设计及存储性能进行深入的研究。敬请期待!




往期精选


1、

2、

3、

以上是关于利用 Android 系统原生 API 实现分享功能(2)的主要内容,如果未能解决你的问题,请参考以下文章

Android利用系统原生BottomNavigationView实现底部导航

Android 高级开发进阶图谱

分享一个Mongodb PHP封装类

关于js实现微信,QQ,微博分享的总结

Android 原生的人脸识别Camera+FaceDetector示例

技术分享|一文看懂云原生