首页 Android 正文
  • 本文约7565字,阅读需38分钟
  • 58
  • 0

五种常见 Android 的 SDK 开发的方式

摘要

一、前言 本文介绍 5 种常见的 Android SDK 开发方式,以及对应使用场景,难度逐级递增,重点介绍后两种方式,涉及到插件化方式、思维和跨进程通信 AIDL 相关知识。 Android SDK 开发是一门艺术,只有都掌握了,才能轻松应对各种场景。SDK 开发的结果是标准产品,是对外输出的,提供给其他中小型公司或者商家用的,需要注意以下原则: 尽最大努...

一、前言

本文介绍 5 种常见的 Android SDK 开发方式,以及对应使用场景,难度逐级递增,重点介绍后两种方式,涉及到插件化方式、思维和跨进程通信 AIDL 相关知识。

五种常见 Android 的 SDK 开发的方式

Android SDK 开发是一门艺术,只有都掌握了,才能轻松应对各种场景。SDK 开发的结果是标准产品,是对外输出的,提供给其他中小型公司或者商家用的,需要注意以下原则:

尽最大努力做到稳定。

尽最大努力不依赖第三方库。

尽最大努力保证包的体积最小。

尽最大努力保证性能最佳。

尽最大努力保证好兼容性。

尽最大努力自己实现动态化可更新。

示例工程 github : https://github.com/wgllss/WX-SDK-Demo

二、Jar 包式 SDK

Jar 包式开发是最简单的,它的工程是建的 model lib 工程。

build.gradle下配置plugins {id 'com.android.library'}

    plugins {

        id 'com.android.library'

    }

编译出来在/intermediates/aar_main_jar/下面以classes.jar形式存在,可以重命名。

使用方将 JAR 文件复制到 Android 项目中的libs目录(若无则创建)。

打开build.gradle文件,在dependencies块中添加对该 JAR 文件的引用,例如implementation files('libs/yourlibrary.jar')

同步 Gradle,以便让它知道你已经添加了新的依赖。

常见的 jar 包:如日志库,json 解析库,网络库,像 okhttp 等。

三、SO 库式 SDK

So 库编写,需要涉及到 C 和 C++,有些还要交叉编译相关操作。

在 Android Studio 下自行编写 C/C++ 相关代码,涉及到 jni 相关知识,Cmake知识,早些时候是android.Mk相关知识。

编译出来的 SO 在/intermediates/stripped_native_libs/下面arm64 - v8aarmeabi - v7ax86x86_64都存在。

使用方将 so 拷贝到项目 libs 目录下,然后配置好 jni 的 lib,如sourceSets {main {jniLibs.srcDirs = ['libs']}}

    sourceSets {
        main {
          jniLibs.srcDirs = ['libs']
        }
    }

常见的 SO 库:游戏 Unity,音视频相关处理 FFmpeg 打包出来的库,扫码,直播等相关用 C++ 写好封装的库。

四、AAR 包式 SDK

AAR 包开发和 jar 的开发方式类似,只是包含了相关资源和配置 manifest,里面除了纯代码,android 相关四大组件、UI 的都可以。比如

支付 SDK,SDK 里面自带了界面,有 activity。

有些 UI 相关库有些纯 sdk 但是需要相关配置权限等。

甚至某些还包含 so、资源,全部都有的。

这些开发封装好的,可以直接发布到仓库让使用方调用,也可以直接将 aar 拷贝到项目工程直接使用,如

implementation 'com.scwang.smart:refresh-layout-kernel:2.0.0-alpha-1

又或者

implementation files(name: 'xxxxAAVV', ext: 'aar')

上面 jar、so 和 aar 都是直接接入项目工程,没法动态修改的,很多 sdk 发布越久版本迭代也会有很多,如 2.0 版本、3.0 版本等。

下面将介绍可以动态修改 SDK 写法。

五、Dex 插件式 SDK

此种方式不常见,需要有插件化基础和思维。

本实例以最简单的,用插件化方式写一个日志打印的 SDK。

先建一个model lib工程wx_sdk4

内部代码为一个简单的日志接口IWXLog

     interface IWXLog {

         fun e(tag: String, message: String)

         fun i(tag: String, message: String)

         fun v(tag: String, message: String)

     }

再建一个加载日志的接口IXLogLoader,包含isDownloadSuccessloadgetWXLog等方法。


         interface IXLogLoader {

             fun isDownloadSuccess(): Boolean

             fun load(context: Context)

             fun getWXLog(): IWXLog

         }

IXLogLoader本地默认实现类XLogLoaderImpl,包含模拟下载、判断版本等逻辑,通过DynamicManageUtils获取相关文件并进行操作,根据下载情况设置atomicBoolean的值,getWXLog方法中通过WXClassLoader获取接口实现。

    class XLogLoaderImpl private constructor() : IXLogLoader {

        private var atomicBoolean = AtomicBoolean(false)
        lateinit var context: Context

        companion object {

           val instance by lazy { XLogLoaderImpl() }

        }

        override fun isDownloadSuccess(): Boolean {
           return atomicBoolean.get()
        }

        override fun load(context: Context) {
           this.context = context

               //此处 模拟下载,怎么判断版本,怎么在启动优化中最快读取配置,判断要加载那个版本插件,是否还要下载插件等 前面文章有介绍过
               DynamicManageUtils.getDxFile(context, "d_dex", getDlfn("xlog_dex", 1000)).takeUnless { it.exists() }?.run {
                   val isSuccess = DynamicManageUtils.copyFileFromAssetsToSD(context, this, "xlog_dex")
                   atomicBoolean.set(isSuccess)
               }
           }

           override fun getWXLog(): IWXLog {
               if (!atomicBoolean.get()) {
                   // 如果没有下载成功或者 异常,可以给出相关提示异常
               }
               return WXClassLoader(DynamicManageUtils.getDxFile(context, "d_dex", getDlfn("xlog_dex", 1000)).absolutePath, null, context.classLoader).getInterface(IWXLog::class.java, "com.wx.iml.WXLog")
           }
        }  

再建一个实现全动态化策略切换用于在客户端接入时调用的WXLogger,根据本地文件是否存在来获取IXLogLoader实例。

   object WXLogger {

          fun getIXLogLoader(context: Context): IXLogLoader {
              //接入部分怎么动态插件化  注意此处,可以在IXLogLoader 内部实现 下载 IXLogLoader的插件实现 ,判断插件版本,可以做到全动态方式,插件版本可以搞个接口,本地包内默认实现,此类里面判断,然后可以做到版本 下载逻辑,IXLogLoader都全动态的,
              val localFile = DynamicManageUtils.getDxFile(context, "d_dex", getDlfn("IXLogLoade_dex", 1000))
              val xLogLoader = if (localFile.exists()) WXClassLoader(localFile.absolutePath, null, context.classLoader).getInterface(IXLogLoader::class.java, "IXLogLoader的插件式实现类全名")
              else XLogLoaderImpl.instance
              return xLogLoader
          }
        }

再建插件工程wx_sdk4_plugin_impl内部实现真正的日志 SDK,处理成插件 dex 文件。

class WXLog : IWXLog {
    override fun e(tag: String, message: String) {
    android.util.Log.e(tag, message)
    }

         override fun i(tag: String, message: String) {
             android.util.Log.i(tag, message)
         }

         override fun v(tag: String, message: String) {
             android.util.Log.v(tag, message)
         }
        }

接入端调用,在SampleApplicationattachBaseContext方法中加载日志,

    class SampleApplication : Application() {

         companion object {
             lateinit var xLogLoader: IXLogLoader
         }

         override fun attachBaseContext(base: Context?) {
             super.attachBaseContext(base)
             WXLogger.getIXLogLoader(this).also {
                 xLogLoader = it
             }.load(this)
         }
        }

在实际使用的地方通过textView.setOnClickListener等方式触发日志输出。

    textView.setOnClickListener {
    SampleApplication.xLogLoader.getWXLog().e("HomeFragment", "AAAAAAAAAAAA")
    }

六、Apk 安装式 SDK

此种方式以安装的 apk 形式存在,但是没有启动桌面图标,可以包含资源,so 等,或者不包含都可以,

此种方式需要设置层前台进程,保持和当前调用应用通信正常。这是跨进程通信方式 SDK,常用于智能设备,如 android 系统收银机,收银秤,平板,学习机,机器人,车机等设备。例如一个收银设备打印小票,打印条码,或者做个本地可以培训识别的 sdk,最典型的就是谷歌自己的文字转语音(TTS 输出)。

下面是个简单示例:

配置没有启动桌面图标,需要添加相关权限,如,并在标签内配置服务相关信息,如`标签及intent-filter`等。

//此种方式需要 前台进程,需要弹窗权限
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<application
    android:allowBackup="true"
    android:fullBackupContent="@xml/backup_rules"
    android:largeHeap="true"
    android:requestLegacyExternalStorage="true"
    android:resizeableActivity="false"
    android:supportsRtl="true"
    android:usesCleartextTraffic="true">
    <service
        android:name="com.wx.sdk5.MyService"
        android:enabled="true"
        android:exported="true"
        android:foregroundServiceType="mediaPlayback">
        <!--  foregroundServiceType 可以选择适当的类型          -->
        <intent-filter>
            <action android:name="com.wx.sdk5.aidl" />
        </intent-filter>
    </service>
</application>

新建个AIDL,定义接口方法basicTypes

interface IMyAidlInterface {

    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
            double aDouble, String aString);
}

apk 内部的MyService实现,在onCreate方法中显示通知,onBind方法返回binderIBinderImpl实现IMyAidlInterface.Stub接口,在basicTypes方法中进行相关操作输出日志。BXNotificationListener用于处理通知相关事件,控制前台服务的启动和停止。

class MyService : Service() {

    private val notificationManager by lazy { WXNotificationManager(this, BXNotificationListener()) }
    private var isForegroundService = false

    private val binder by lazy { IBinderImpl() }

    override fun onCreate() {
        super.onCreate()
        notificationManager.showNotification()
    }

    override fun onBind(intent: Intent): IBinder {
        return binder
    }

    private inner class IBinderImpl : IMyAidlInterface.Stub() {
        override fun basicTypes(anInt: Int, aLong: Long, aBoolean: Boolean, aFloat: Float, aDouble: Double, aString: String?) {
            android.util.Log.e("MyService", "在此包SDK 内可以做相关操作,如打印票据,识别,或者其他的等等")
        }
    }

    private inner class BXNotificationListener : WXNotificationListener {

        override fun onNotificationPosted(notificationId: Int, notification: Notification, ongoing: Boolean) {
            if (ongoing && !isForegroundService) {
                ContextCompat.startForegroundService(this@MyService, Intent(this@MyService, this@MyService.javaClass))
                startForeground(notificationId, notification)
                isForegroundService = true
            }
        }

        override fun onNotificationCancelled(notificationId: Int, dismissedByUser: Boolean) {
            android.util.Log.e("BackgroundService", "onNotificationCancelled")
            stopForeground(true)
            isForegroundService = false
            stopSelf()
        }
    }
}

在调用端,再次单独写成个 lib 工程,处理成 jar 让接入端直接使用 jar,该工程内部实现SDK5Impl类,包含绑定服务、解绑服务、调用服务方法等逻辑,通过ServiceConnection实现服务连接和断开的回调处理。

class SDK5Impl private constructor() {
  private val isBindService by lazy { AtomicBoolean(false) }
  private var remoteScaleService: IMyAidlInterface? = null

  companion object {
      val instance by lazy { SDK5Impl() }
  }

  private val connection = object : ServiceConnection {
      override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
          isBindService.set(true)
          try {
              remoteScaleService = IMyAidlInterface.Stub.asInterface(service)
          } catch (e: Exception) {

          }
      }

      override fun onServiceDisconnected(name: ComponentName?) {
          isBindService.set(false)
          remoteScaleService = null
      }
  }

  fun bindScaleService(context: Context) {
      try {
          context.packageManager.getApplicationInfo("com.wx.sdk5", 0).enabled
          val intent = Intent("com.wx.sdk5.aidl")
          intent.setPackage("com.wx.sdk5")
          context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
      } catch (e: Exception) {

      }
  }

  fun unBindScaleService(context: Context) {
      remoteScaleService?.takeIf {
          isBindService.get()
      }?.let {
          context.unbindService(connection)
      }
      isBindService.set(false)
  }

  fun basicTypes(anInt: Int, aLong: Long, aBoolean: Boolean, aFloat: Float, aDouble: Double, aString: String) {
      remoteScaleService?.basicTypes(anInt, aLong, aBoolean, aFloat, aDouble, aString)
  }
}

在接入端调用,包括绑定远端服务

SDK5Impl.instance.bindScaleService(this)  

解绑服务(在onDestroy方法中SDK5Impl.instance.unBindScaleService(this)

  override fun onDestroy() {
         super.onDestroy()
         SDK5Impl.instance.unBindScaleService(this)
   }

真正调用(如binding.textSdk5.setOnClickListener中调用SDK5Impl.instance.basicTypes方法)

binding.textSdk5.setOnClickListener {
      SDK5Impl.instance.basicTypes(1, 2L, true, 3.0f, 4.4, "AAA")
}

七、查看 SDK4,SDK5 结果

展示运行时相关的日志信息。

五种常见 Android 的 SDK 开发的方式

八、总结

本文对 5 种常见的 SDK 开发方式进行了总结:

最常见的有 jar、so 和 aar,这三种是没法动态自我更新的。

第 4 种:dex 形式 SDK,需要掌握好相关插件化开发知识,并且能理解插件化策略思想,才可以做到全动态化。

第 5 种:跨进程安装一个无桌面启动的 apk 方式,其实是可以在调用端第一次绑定时候,可以检查 sdk 是否有新的版本,这样以安装 apk 的方式里面,可以包含所有系统功能,so、资源、代码,都可以全部解耦出去,需要注意的是,在 aidl 跨进程通信时候,传输的数据不能太大,不要超过(1M - 8k)的数据(为什么是这么多,需要自行理解 binder 跨进程通信)。

标签:SDK

扫描二维码,在手机上阅读
    评论