Android中的SPI实现

Android中的SPI实现

SPI是JVM世界中的标准API,但在Android应用程序中并不常用。然而,它可以非常有用地实现插件架构。让我们探讨一下如何在Android中利用SPI。

问题

在Android中,不同的提供者为推送功能提供服务,而在大型项目中,使用单一实现是不可行的。以下是一些可用的提供者:

  • FCM(Firebase Cloud Messaging):主要的推送服务实现,但需要Google服务,可能无法在所有设备上使用。
  • ADM(Amazon Device Messaging):Amazon设备(Kindle设备)上的实现,仅在Amazon设备上运行。
  • HCM(Huawei Cloud Messaging):华为设备上的实现。
  • Baidu(Baidu Push SDK):主要用于中国的推送服务实现。

由于有如此多的服务,管理和初始化它们变得具有挑战性。

当我们需要为不同的应用程序构建提供不同的服务集时,问题变得更加困难。以下是一些示例:

  • Google Play控制台不允许发布包含百度服务的应用程序。因此,百度服务应仅包含在面向中国的构建中。
  • Amazon设备消息传递仅适用于Amazon设备,因此在仅针对Amazon应用商店的构建中包含它是有意义的。
  • 华为实现在面向华为商店的构建中是有意义的。

解决方案

为了解决这个问题,我们可以从创建推送服务实现的抽象层开始。这个抽象层应该放在一个单独的Gradle模块中,以便它可以轻松地作为其他实现模块的依赖项添加。

抽象层

我们可以通过创建以下通用接口来为推送服务定义抽象层:

package com.kurantsov.pushserviceimport android.content.Context/*** Interface used to provide push service implementation via SPI*/
interface PushService {/*** Type of the push service implementation*/val type: PushServiceType/*** Priority of the push service implementation*/val priority: PushServicePriority/*** Returns if the push service implementation is available on the device*/fun isAvailable(context: Context): Boolean/*** Initializes push service*/fun initialize(context: Context)
}/*** Describes type of the push service implementation*/
interface PushServiceType {val name: Stringval description: String
}sealed class PushServicePriority(val value: Int) {object High : PushServicePriority(0)object Medium : PushServicePriority(1)object Low : PushServicePriority(2)
}

实现

然后,我们可以基于推送服务提供者实现一个通用接口。

为此,我们可以为每个实现创建一个Gradle模块。

Firebase Cloud Messaging实现示例:

package com.kurantsov.pushservice.firebaseimport android.content.Context
import android.util.Log
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.firebase.ktx.Firebase
import com.google.firebase.messaging.ktx.messaging
import com.kurantsov.pushservice.PushService
import com.kurantsov.pushservice.PushServiceManager
import com.kurantsov.pushservice.PushServicePriority
import com.kurantsov.pushservice.PushServiceTypeclass FirebasePushService : PushService {override val type: PushServiceType = FirebasePushServiceTypeoverride val priority: PushServicePriority = PushServicePriority.Highoverride fun isAvailable(context: Context): Boolean {val availability =GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context)return availability == ConnectionResult.SUCCESS}override fun initialize(context: Context) {Firebase.messaging.token.addOnCompleteListener { task ->if (!task.isSuccessful) {Log.w(TAG, "Fetching FCM registration token failed", task.exception)}val token = task.resultPushServiceManager.setPushToken(token, FirebasePushServiceType)}}private companion object {const val TAG = "FirebasePushService"}
}object FirebasePushServiceType : PushServiceType {override val name: String = "FCM"override val description: String = "Firebase"
}

Amazon Device Messaging实现示例:

package com.kurantsov.pushservice.amazonimport android.content.Context
import com.amazon.device.messaging.ADM
import com.kurantsov.pushservice.PushService
import com.kurantsov.pushservice.PushServicePriority
import com.kurantsov.pushservice.PushServiceType/*** Amazon device messaging implementation of the push service*/
class AmazonPushService : PushService {override val type: PushServiceType = AmazonPushServiceTypeoverride val priority: PushServicePriority = PushServicePriority.Highoverride fun isAvailable(context: Context): Boolean {return isAmazonServicesAvailable}override fun initialize(context: Context) {val adm = ADM(context)adm.registrationId?.let { token ->handleRegistrationSuccess(token)} ?: run {adm.startRegister()}}
}object AmazonPushServiceType : PushServiceType {override val name: String = "ADM"override val description: String = "Amazon"
}/*** Returns if amazon device messaging is available on the device*/
val isAmazonServicesAvailable: Boolean by lazy {try {Class.forName("com.amazon.device.messaging.ADM")true} catch (e: ClassNotFoundException) {false}
}

实现注册

为了使实现通过SPI“可发现”,我们需要进行注册。这可以通过在META-INF/services/{接口的全限定名}中添加实现的完全限定名称来完成。这需要在提供接口实现的每个模块中完成。

Firebase实现文件示例内容:

com.kurantsov.pushservice.firebase.FirebasePushService
请注意,要将服务文件夹的完整路径包含在模块的结果AAR中,路径是:{模块路径}/src/main/resources/META-INF/services

Android Studio项目视图中的SPI注册示例

用法

最后一步是使用接口实现。以下是SPI使用示例:

import java.util.ServiceLoaderprivate fun listImplementations(context: Context) {//Loading push service implementationsval serviceLoader = ServiceLoader.load(PushService::class.java)//Logging implementationsserviceLoader.sortedBy { pusService -> pusService.priority.value }.forEach { pushService ->val isAvailable = pushService.isAvailable(context)Log.d(TAG, "Push service implementation - ${pushService.type.description}, " +"available - $isAvailable")}
}

示例输出如下:

Push service implementation - Firebase, available - true
Push service implementation - Amazon, available - false
Push service implementation - Huawei, available - true
Push service implementation - Baidu, available - true

完整代码请参考

https://github.com/ArtsemKurantsou/SPI4Android

额外内容

PushServiceManager

以下是一个更“真实”的示例,展示了PushServiceManager的用法:

package com.kurantsov.pushserviceimport android.content.Context
import android.util.Log
import java.util.ServiceLoader
import java.util.concurrent.CopyOnWriteArraySet
import java.util.concurrent.atomic.AtomicBooleanobject PushServiceManager {private const val TAG = "PushServiceManager"var pushToken: PushToken = PushToken.NotInitializedprivate setprivate val isInitialized: AtomicBoolean = AtomicBoolean(false)private val tokenChangedListeners: MutableSet<OnPushTokenChangedListener> =CopyOnWriteArraySet()private var selectedPushServiceType: PushServiceType? = nullfun initialize(context: Context) {if (isInitialized.get()) {Log.d(TAG, "Push service is initialized already")return}synchronized(this) {if (isInitialized.get()) {Log.d(TAG, "Push service is initialized already")return}performServiceInitialization(context)}}private fun performServiceInitialization(context: Context) {//Loading push service implementationsval serviceLoader = ServiceLoader.load(PushService::class.java)val selectedImplementation = serviceLoader.sortedBy { pusService -> pusService.priority.value }.firstOrNull { pushService ->val isAvailable = pushService.isAvailable(context)Log.d(TAG, "Checking push service - ${pushService.type.description}, " +"available - $isAvailable")isAvailable}if (selectedImplementation != null) {selectedImplementation.initialize(context)selectedPushServiceType = selectedImplementation.typeisInitialized.set(true)Log.i(TAG, "Push service initialized with ${selectedImplementation.type.description}")} else {Log.e(TAG, "Push service implementation failed. No implementations found!")throw IllegalStateException("No push service implementations found!")}}/*** Adds listener for the push token updates. Called immediately if token is available* already.*/fun addOnPushTokenChangedListener(listener: OnPushTokenChangedListener) {tokenChangedListeners.add(listener)val currentToken = pushTokenif (currentToken is PushToken.Initialized) {listener.onPushTokenChanged(currentToken)}}/*** Removes listener for the push token updates.*/fun removeOnPushTokenChangedListener(listener: OnPushTokenChangedListener) {tokenChangedListeners.remove(listener)}/*** Called by push service implementation to notify about push token change.*/fun setPushToken(token: String, serviceType: PushServiceType) {if (selectedPushServiceType != serviceType) {Log.w(TAG, "setPushToken called from unexpected implementation. " +"Selected implementation - ${selectedPushServiceType?.description}, " +"Called by - ${serviceType.description}")return}val initializedToken = PushToken.Initialized(token, serviceType)this.pushToken = initializedTokentokenChangedListeners.forEach { listener ->listener.onPushTokenChanged(initializedToken)}}/*** Called by push service implementation to notify about push message.*/fun processMessage(message: Map<String, String>, sender: String) {Log.d(TAG, "processMessage: sender - $sender, message - $message")}}

PushServiceInitializer

为了简化推送服务的最终集成,我们可以使用App启动库,这样“app”模块就不需要添加其他内容。

Initializer:

package com.kurantsov.pushserviceimport android.content.Context
import android.util.Log
import androidx.startup.Initializerclass PushServiceInitializer : Initializer<PushServiceManager> {override fun create(context: Context): PushServiceManager {runCatching {PushServiceManager.initialize(context)}.onFailure { e ->Log.e(TAG, "create: failed to initialize push service", e)}.onSuccess {Log.d(TAG, "create: Push service initialized successfully")}return PushServiceManager}override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()private companion object {const val TAG = "PushServiceInitializer"}
}

AndroidManifest:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"><application><providerandroid:name="androidx.startup.InitializationProvider"android:authorities="${applicationId}.androidx-startup"android:exported="false"tools:node="merge"><meta-dataandroid:name="com.kurantsov.pushservice.PushServiceInitializer"android:value="androidx.startup" /></provider></application>
</manifest>

编译时实现选择

由于使用了推送服务实现的SPI,我们有几个模块提供了实现。要将其添加到最终的apk中,我们只需要在实现模块上添加依赖关系。

有几种方法可以在编译时添加/删除依赖项。例如:

我们可以创建几个应用程序的构建变体,并使用基于变体的依赖关系(例如,如果我们有华为变体,我们可以使用huaweiImplementation而不是implementation;这样只会为中国变体添加依赖项)。
基于编译标志进行依赖项的添加。
以下是基于标志的方法示例( app/build.gradle.kts):

dependencies {implementation(project(":push-service:core"))implementation(project(":push-service:firebase"))if (getBooleanProperty("amazon")) {implementation(project(":push-service:amazon"))}if (getBooleanProperty("huawei")) {implementation(project(":push-service:huawei"))}if (getBooleanProperty("baidu")) {implementation(project(":push-service:baidu"))}
}fun getBooleanProperty(propertyName: String): Boolean {return properties[propertyName]?.toString()?.toBoolean() == true
}

然后,我们可以在编译过程中使用命令行中的-P{标志名称}={值}来添加这些标志。以下是添加所有实现的命令示例:

gradle :app:assemble -Pamazon=true -Phuawei=true -Pbaidu=true

aar/apk中的SPI实现

您可以使用Android Studio内置的apk资源管理器验证aar/apk文件中的SPI实现。

在aar文件中,META-INF/services文件夹位于classes.jar内部。Firebase实现aar示例:
Firebase 实现AAR 示例
在apk文件中,META-INF/services文件夹位于apk根目录中。以下是最终apk示例:
APK 示例

参考链接

https://github.com/ArtsemKurantsou/SPI4Android
https://en.wikipedia.org/wiki/Service_provider_interface
https://developer.android.com/topic/libraries/app-startup

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/411180.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

5文件操作

包含头文件<fstream> 操作文件三大类&#xff1a; ofstream : 写文件ifstream &#xff1a;读文件fstream : 读写文件 5.1文本文件 -文件以ascii的形式存储在计算机中 5.1.1写文件 步骤&#xff1a; 包含头文件 #include "fstream"创建流对象 ofs…

Vue 如何把computed里的逻辑提取出来

借用一下百度的ai 项目使用&#xff1a; vue 文件引入 <sidebar-itemv-for"route in routes":key"route.menuCode":item"route":base-path"route.path"click"onColor"/>import { handleroutes } from "./handle…

Base64编码原理解析

文章目录 一、Base64Base64编码的原理如下&#xff1a;以字符串"hello world"为例&#xff0c;它的ASCII码为&#xff08;下面&#x1f447;是ASCII码对照表&#xff09;&#xff1a;将这些ASCII码转换为二进制&#xff08;对照上表&#xff09;&#xff1a;将上述二…

Shiro框架:Shiro用户访问控制鉴权流程-内置过滤器方式源码解析

目录 1.配置访问控制鉴权流程的2种方式 2.Shiro内置过滤器 3.权限控制流程解析 3.1 权限&#xff08;Permissions&#xff09;配置和初始化流程 3.1.1 通过filterChainDefinitionMap配置url权限 3.1.1.1 应用层配置方式 3.1.1.2 配置解析过程 3.1.1.2.1 FilterChainMan…

UE4使用技巧

打开蓝图编辑器时不是打开一个新窗口&#xff0c;而是作为主窗口 适用于全部的打开新窗口的操作 蓝图编译时自动保存 开始游戏后立即捕获鼠标

华为路由设备DHCPV6配置

组网需求 如果大量的企业用户IPv6地址都是手动配置&#xff0c;那么网络管理员工作量大&#xff0c;而且可管理性很差。管理员希望实现公司用户IPv6地址和网络配置参数的自动获取&#xff0c;便于统一管理&#xff0c;实现IPv6的层次布局。 图1 DHCPv6服务器组网图 配置思路 …

Clickhouse: One table to rule them all!

前面几篇笔记我们讨论了存储海量行情数据的个人技术方案。它们之所以被称之为个人方案&#xff0c;并不是因为性能弱&#xff0c;而是指在这些方案中&#xff0c;数据都存储在本地&#xff0c;也只适合单机查询。 数据源很贵 – 在这个冬天&#xff0c;我们已经听说&#xff0…

Peter算法小课堂—并查集

我们先来看太戈编程467题 攀亲戚 题目描述&#xff1a; 最近你发现自己和古代一个皇帝长得很像&#xff1a;都有两个鼻子一个眼睛&#xff0c;你想知道这皇帝是不是你的远方亲戚&#xff0c;你是不是皇亲国戚。目前你能掌握的信息有m条&#xff0c;关于n个人&#xff1a;第i条…

Python二级中的进制转换:看这一篇就够了

在计算机中&#xff0c;不同的进制之间的转换是常见的问题&#xff0c;也是令很多朋友比较头疼的问题&#xff0c;这种题虽然在Python二级中分不多&#xff0c;有时甚至还不会出&#xff0c;但是必须要掌握它们&#xff0c;以备不时之需。 这里详细介绍如何在二进制、八进制、…

Unity之铰链关节和弹簧组件

《今天闪电侠他回来了&#xff0c;这一次他要拿回属于他的一切》 目录 &#x1f4d5;一、铰链关节组件HingeJoint 1. 实例 2. 铰链关节的坐标属性 ​3.铰链关节的马达属性Motor &#x1f4d5;二、弹簧组件 &#x1f4d5;三、杂谈 一、铰链关节组件HingeJoint 1. 实例 说…

Docker从入门到精通

系列文章目录 docker常见用法之镜像构建1 docker 系列文章目录一、镜像的分层结构二、容器的用法三、镜像的构建3.1docker commit 构建新镜像三部曲3.1.1运行容器并且修改容器3.1.2提交容器3.1.2删除docker镜像 3.2Dockerfile构建镜像 系列文章目录一、 Dockerfile写法详解1.1…

MySQL优化之SQL调优策略

首先以一张思维导图从全局上给大家分享以下几种SQL优化策略&#xff0c;再详细讲解 1、避免使用SELECT * 在阿里的编码规范中也强制了数据库查询不能使用SELECT *&#xff0c;因为SELECT *方式走的都是全表扫描&#xff0c;导致的结果就是查询效率非常低下&#xff0c;其原因为…

Redis数据结构学习笔记

图文主要参考小林Coding的图解redis数据结构 redis为什么快 除了它是内存数据库&#xff0c;使得所有的操作都在内存上进⾏之外&#xff0c;还有⼀个重要因素&#xff0c;它实现的数据结构&#xff0c;使 得我们对数据进⾏增删查改操作时&#xff0c;Redis 能⾼效的处理。 数…

1.机器学习-机器学习算法分类概述

机器学习-机器学习算法分类概述 个人简介机器学习算法分类&#xff1a;监督学习、无监督学习、强化学习一监督学习1. 监督学习分类任务举例&#xff1a;1.1 特征1.2 标签 二无监督学习1.关键特点2.应用示例3.常见的无监督学习算法 三强化学习1.定义2.示例场景 四机器学习开发流…

【Python3】【力扣题】389. 找不同

【力扣题】题目描述&#xff1a; 【Python3】代码&#xff1a; 1、解题思路&#xff1a;使用计数器分别统计字符串中的元素和出现次数&#xff0c;两个计数器相减&#xff0c;结果就是新添加的元素。 知识点&#xff1a;collections.Counter(...)&#xff1a;字典子类&#x…

【白皮书下载】GPU计算在汽车中的应用

驾驶舱域控制器 (CDC) 是汽车 GPU 的传统应用领域。在这里&#xff0c;它可以驱动仪表板上的图形&#xff0c;与车辆保持高度响应和直观的用户界面&#xff0c;甚至为乘客提供游戏体验。随着车辆屏幕数量的增加和分辨率的提高&#xff0c;对汽车 GPU 在 CDC 中进行图形处理的需…

Jxls 实现动态导出功能

目录 引言前端页面后端代码excel模板导出效果 引言 在实际做项目的过程中&#xff0c;导出报表时需要根据每个人所关注的点不一样&#xff0c;所需导出的字段也不一样&#xff0c;这时后端就需要根据每个所选的字段去相应的报表&#xff0c;这就是本文要讲的动态导出报表。 前端…

S/MIME电子邮件证书申请指南

近年来&#xff0c;邮件安全问题日益突出&#xff0c;电子邮件成为诈骗、勒索软件攻击的重灾区。恶意邮件的占比屡创新高&#xff0c;邮件泄密事件更是比比皆是。在如此严峻的网络安全形势下&#xff0c;使用S/MIME电子邮件证书进行邮件收发是当今最佳的邮件安全解决方案之一。…

生产消费者模型

生产消费者模型概念 生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯&#xff0c;而 通过阻塞队列来进行通讯&#xff0c;所以生产者生产完数据之后不用等待消费者处理&#xff0c;直接扔给阻塞队列&#xff0c;消费者不…

pygame学习(三)——支持多种类型的事件

大家好&#xff01;我是码银&#x1f970; 欢迎关注&#x1f970;&#xff1a; CSDN&#xff1a;码银 公众号&#xff1a;码银学编程 实时事件循环 为了保证程序的持续刷新、保持打开的状态&#xff0c;我们会创建一个无限循环&#xff0c;通常使用的是while语句&#xff0c;w…