2.2 游戏SDK的流程图

游戏SDK最核心的是登录和支付功能,其它的都是运营相关的,例如埋点、数据统计等等~

登录和支付的流程大概如下图:

图画的比较简陋,解释一下,上半部分是登录流程、下半部分是支付流程

流程还算比较简单的~

接下来说说游戏SDK开发的一些需要注意的点:

三、游戏SDK开发注意事项

3.1 少依赖

很多开发者都知道,作为SDK,应该尽量少使用开源库,或者说不用开源库,
而是通过手写网络框架,手写数据库等等,主要是考虑两个方面:

  • 减小SDK体积 ;
  • 避免第三方接入的时候发生依赖冲突

当然,依赖库并不是说不能用,有时候一些数据统计的库需要依赖第三方,那这种情况是没有办法避免的,可以在对接文档中提供一个解决依赖冲突的办法

3.2 解决依赖冲突

方式一

在app的build.gradle中添加类似配置如下:

configurations.all {
resolutionStrategy {
//解决v4包冲突,强制使用这个版本的v4包
force ‘com.android.support:support-v4:26.1.0’
}
}

方式二

exclude

implementation(“com.xxx.xxx:xx”) {
exclude group: ‘com.android.support’
}
复制代码

exclude是最常用的解决依赖冲突的方式,但如果多个依赖库引入不同版本的其它库,需要分别写好多个exclude,显然第一种方式比较简单粗暴。

3.3 对外暴露的接口尽量少

面向接口编程,以游戏SDK为例,对外暴露的接口一般有SDK初始化、登录、支付等,参考设计如下:

定义接口:

interface IGame {

// 1、在Application中调用,
fun registerApp(context: ApplicationContext, appId: String)

// 2、在activity中初始化
fun init(activity: Activity)

// 3、业务接口,登录、支付等等
fun login(loginCallBack: LoginCallBack)

fun pay(product: Product, payCallBack: PayCallBack)

}
复制代码

实现类

/**

  • 实现类
    */
    class GameImpl : IGame{

override fun registerApp(context: ApplicationContext, appId: String) {
//appid相关
}

override fun init(activity: Activity) {
//初始化逻辑,例如显示悬浮窗
}

override fun login(loginCallBack: LoginCallBack) {
//登录逻辑
}

override fun pay(product: Product, payCallBack: PayCallBack) {
//支付逻辑
}

}

复制代码

实现类是我们的内部逻辑,我们不希望被外部访问到,外部只需要知道有 IGame这个接口中的方法就行,我们可以再写个单例的管理类来给外部使用

/**

  • 单例的SDK管理类
    */
    object GameSDKManager :IGame{

//实现类私有化
private val gameImpl: IGame by lazy { GameImpl() }

override fun registerApp(application: Application, appId: String) {
gameImpl.registerApp(application,appId)
}

override fun init(activity: Activity) {
gameImpl.init(activity)
}

override fun login(loginCallBack: LoginCallBack) {
gameImpl.login(loginCallBack)
}

override fun pay(product: Product, payCallBack: PayCallBack) {
gameImpl.pay(product,payCallBack)
}

}

复制代码

kotlinobject关键字表示单例,

外部通过GameSDKManager.xxx来调用SDK中的方法,
以后要提供其它方法,只要修改 IGame接口,然后在 GameSDKManager 和 GameImpl 中分别实现即可。

当然,不是说一定要这样拆分三个类,这只是一个面向接口编程的例子。

四、游戏SDK的坑

游戏SDK前期开发自测可能是很顺利的,难度不大,但是在跟游戏对接的时候可能会出现一些问题, 什么ClassNotFound、Resource not found、依赖冲突、崩溃等等,至于为什么这样,下面会介绍~

4.1 SDK要支持Eclipse

SDK 1.0 测试通过,正式上线,高高兴兴地把文档甩给对接方,心里想,这个我测过的没问题,demo也给了,只要按照文档和demo来,问题不大。

然而,对方回复了一句:“有Eclipse接入文档吗?”

我一脸懵逼,这都什么年代了,真还有人用Eclipse开发App?

我想试图说服对方用Android Studio,然后得到的回复是:其它的游戏SDK都提供了Eclipse的接入方式~

想起我上一次用Eclipse应该是大三的时候…

就这样,第二天下载了Eclipse之后,按照教程安装APT插件,然而编译一直报错,忘记具体的错误信息了,最终的解决办法是下载了一份Eclipse版本的SDK,Eclipse 不能使用Android Studio版本的SDK

好了,Eclipse环境弄好了,hello world也跑起来了,开始写demo~

由于SDK的产物是aar,而Eclipse只能依赖jar包和library,一般都用jar包依赖,先将aar解压出来,把里面的classes.jar拷贝出来重命名,然后在Eclipse中依赖这个jar包,同时,SDK的资源文件、libs目录下的jar包也需要拷贝到Eclipse项目中。

终于,编译成功,安装,打开,闪退了~

奔溃信息指向:setContentView(xxx),错误信息是 Resources$NotFoundException: Resource ID #0x13d6b6

4.2 setContentView(xxx) 凭什么奔溃?

看以下这段代码

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//简单的一段代码
setContentView(R.layout.activity_test)

}

这段代码在打包aar的时候,Android Studio接入没问题,但是打成jar包,Eclipse接入的时候会奔溃,奔溃信息如下,

Caused by: android.content.res.Resources$NotFoundException: Resource ID #0x13d6b6
at android.content.res.ResourcesImpl.getValue(ResourcesImpl.java:246)
at android.content.res.Resources.loadXmlResourceParser(Resources.java:2256)
at android.content.res.Resources.getLayout(Resources.java:1228)
at android.view.LayoutInflater.inflate(LayoutInflater.java:427)
at android.view.LayoutInflater.inflate(LayoutInflater.java:380)
at androidx.appcompat.app.AppCompatDelegateImpl.setContentView(AppCompatDelegateImpl.java:555)
at androidx.appcompat.app.AppCompatActivity.setContentView(AppCompatActivity.java:161)
at luyao.util.ktx.base.BaseVMActivity.onCreate(BaseVMActivity.kt:25)
复制代码

按下command 键,鼠标放到R.layout.activity_test 上去

这是一个常量,这个常量定义在R文件中,在AAPT阶段生成,

有小伙伴应该已经看出问题所在了,先假设大家都不知道,我们来回顾一下apk打包的主要流程

  1. AAPT(或者AAPT2)工具打包资源文件,生成R.class文件,resources.arsc资源索引表
  2. AIDL 转换成Jave代码(有AIDL的话)
  3. Java代码编译成.class文件
  4. 通过dex工具将.class文件转换成Dalvik 字节码,也就是.dex文件
  5. 通过ApkBuilde工具将.dex文件和其它资源文件打包成未签名的apk
  6. 通过签名工具给apk签名,v1签名使用jarsigner、v2签名使用apksigner(sdk 25版本开始提供)

apk编译的第一个阶段,AAPT会打包资源文件,生成R.class文件和resources.arsc资源索引表

library项目在打包aar的时候,上面123这几个流程一定会走的,但是aar中并没有生成 resources.arsc 这个资源索引表,资源的id跟资源文件的映射关系记录在R.txt中,如下图:

而Eclipse因为只能接入jar包,也就是解压aar后取出里面的classes.jar,当我们把资源文件拷贝到Eclipse,再编译apk的时候,资源文件会对应一个新的资源id,而aar中classes.jar里引用的资源id是不变的

classes.jar里面的
setContentView(R.layout.activity_test)
相当于
setContentView(-1300150)

而当我们将 activity_test.xml 拷贝到Eclipse项目后编译,AAPT重新给它生成一个资源id,R.layout.activity_test 对应的资源id已经不是 -1300150 了,

这就是为什么classes.jar里面的setContentView(-1300150) 会报错找不到资源。

知道了问题的原因之后, 要解决这个问题,那么SDK里面使用资源id需要动态去获取,不能使用R文件里面的常量~

4.1.2 动态获取资源id

谷歌提供了相关的API,可以通过资源名称获取资源id

Resources#getIdentifier(String name, String defType, String defPackage)

/**

  • Return a resource identifier for the given resource name. A fully
  • qualified resource name is of the form “package:type/entry”. The first
  • two components (package and type) are optional if defType and
  • defPackage, respectively, are specified here.
  • Note: use of this function is discouraged. It is much more

  • efficient to retrieve resources by identifier than by name.
  • @param name The name of the desired resource.
  • @param defType Optional default resource type to find, if “type/” is
  •            not included in the name.  Can be null to require an
    
  •            explicit type.
    
  • @param defPackage Optional default package to find, if “package:” is
  •               not included in the name.  Can be null to require an
    
  •               explicit package.
    
  • @return int The associated resource identifier. Returns 0 if no such
  •     resource was found.  (0 is not a valid resource ID.)
    

*/
public int getIdentifier(String name, String defType, String defPackage) {
return mResourcesImpl.getIdentifier(name, defType, defPackage);
}

复制代码

第一个参数是资源名称,例如一个TextView定义的id叫tv_title;
第二个参数是类型,例如 string、xml、style、layout 等等,跟R.class文件里面的内部类是对应的

如果想获取布局文件id,传layout,如果是获取字符串id,传string,以此类推。

第三个参数是包名。

最后封装成工具类如下

object

《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》

【docs.qq.com/doc/DSkNLaERkbnFoS0ZF】 完整内容开源分享

ResourceUtil {

//缓存资源id
private val idMap: HashMap<String, Int> = HashMap()

private fun getIdByName(context: Context, defType: String, name: String): Int {

//缓存
val key = defType + “_” + name
val value: Int? = idMap.get(key)
value?.let {
return it
}

//获取资源id
val identifier = context.resources.getIdentifier(name, defType, context.packageName)
identifier?.let {
idMap.put(key, identifier)
}
return identifier
}

/**

  • 获取布局文件的资源ID,defType传 layout
    */
    fun getIdFromLayout(context: Context, name: String): Int {
    return getIdByName(context, “layout”, name)
    }

复制代码

然后setContentView(R.layout.test) 需要修改成

setContentView(ResourceUtil.getIdFromLayout(context, “test”))
复制代码

问题是解决了,但是还是需要了解一下底层原理,例如,AAPT打包资源文件,会生成资源id,资源id跟资源是如何关联起来的呢?通过资源名称去读资源id,又是如何读取的呢?

3.1.3 AAPT打包产物

编译的第一个阶段,使用AAPT打包资源文件,产物如下

  1. res文件夹内的图片及xml资源(xml被编译成二进制)
  2. assets文件夹(不会生成资源id)
  3. 二进制AndroidManifest.xml
  4. 资源索引表 resources.arsc
  5. R.class文件

重点关注资源索引表 resources.arsc,

resources.arsc 文件的数据格式比较复杂,Android Studio可以帮我们解析出来

通过Android Studio的 Build -> Analyze APK,打开apk后选择 resources.arsc打开

id(资源id)、name(资源名称)、value(资源路径)都可以通过这个索引表来互相转换,
前面说过 Resources#getIdentifier(String name, String defType, String defPackage),之所以可以通过资源名称获取到资源id,当然还是要借助 resources.arsc 这个资源索引表。

3.1.4 通过资源名称读取资源id的原理

Resources#getIdentifier源码我大概跟了一下,调用流程是

Resources#getIdentifier

ResourcesImpl#getIdentifier

AssetManager#getResourceIdentifier

AssetManager2.cpp#GetResourceId

不贴太多源码,大家有兴趣可以看 AssetManager2.cpp 这个类,里面关联了 ApkAssets,frameworks/base/libs/androidfw/ApkAssets.cpp

ApkAssets.cpp 里面有 resources.arsc 的定义和使用

得出结论是resources.arsc是在native层加载和解析的,通过resources.arsc这个资源索引表,可以将资源id和资源名称、资源路径相互转换。

上面讲的这些太枯燥了,游戏SDK就这些内容?能不能来点实用的呢?

五、游戏SDK开发,后期的一些操作

如果是普通的游戏SDK,那么只要保证接入方能够成功接入SDK就完事了,然而,

小红除了提供游戏SDK之外,还需要对 接入游戏SDK的游戏进行验收,确保游戏SDK的功能正常。

毕竟游戏是要在小红的平台上运营,小红有责任和义务对每一个游戏进行测试验收,确保基本功能正常,总不能用户一打开就奔溃吧~

随着SDK的版本升级,功能会增加,需要验收的功能会越来越多,例如:验证签名,SDK有检查更新的功能,token过期,游戏需要做退出登录逻辑等等…

下面将介绍我是如何处理一些问题的。

5.1 日志开关

SDK接入出现问题,release版本若关闭了日志,我们需要将日志打开复现问题,常用的有两种方式:

  1. 可以参考开发者模式的开关,设置某个控件的点击事件,例如在连续点击5次的时候打开日志开关。 日志开关需要持久化,例如保存到sp,在SDK初始化的时候去读这个开关。

  2. 还有一种做法是类似友盟,初始化的方法提供debug参数,让接入方可以传true来查看日志,但是考虑到SDK内部信息安全,我没有这么做。

5.2 配置检查

我提供的demo运行是正常的,但是第三方他们接入的时候经常会出现一些问题,可能是他们的Android SDK版本不一样,或者一些配置没有严格按照文档来写,作为SDK的开发者,我希望这些配置的问题接入方可以自己发现和处理,这就需要在游戏SDK中增加检测的逻辑。

5.2.1 一个检查更新的功能

Android 8.0 开始,调起应用安装页面,需要用户显式打开未知来源开关,于是有如下代码

有一次发现在接入方的apk中,context.packageManager.canRequestPackageInstalls(),一直返回false,没法调起安装页面,首先想到的是,接入方没有声明安装权限

然后自己去掉权限声明验证一下,发现会抛异常,说明不是这个原因。

最后发现 targetSdkVersion 小于26的话, packageManager.canRequestPackageInstalls()一直会返回false,目前各大应用市场已经陆续要求targetSdkVersion必须26或以上,为了保证SDK的更新功能正常,在SDK初始化的时候,添加如下检测代码

这样接入方targetSdkVersion就一定要26或以上,否则抛异常,从异常日志中就可以发现问题。

4.2.2 FileProvider 需要增加配置检查

由于 7.0之后安装apk需要通过FileProvider来获取url,所以manifest有了这样的代码

如果是Android Studio打包,一般会自动读取build.gradle中的applicationId来替换占位符${applicationId},

如果是Eclipse打包,占位符${applicationId}则原封不动,不会被替换,那么下面的代码就会报空指针了

FileProvider.getUriForFile(context,
context.packageName + “.fileprovider”, file)
复制代码

如何保证接入方一定有配置FileProvider,并且配置正确呢?增加配置检测代码如下

在sdk初始化的时候去私有目录创建一个空文件,然后通过 getUriFormFile 方法触发FileProvider获取url的逻辑,如果有异常,说明FileProvider配置不对。

之后在验收apk的时候,只要能正常安装打开,就说明FileProvider配置是正确的。

5.2.3 签名验证

游戏方接入游戏SDK之后打包成apk,这个apk要在我们平台上线,我们希望统一apk签名, 所以在验收apk的时候,需要确认apk的签名。

查看apk签名主要用两种方式:

针对v2签名

keytool -printcert -jarfile xxx.apk

or

apksigner verify -v --print-certs xxx.apk

这个命令虽然简单粗暴,但是要求apk使用v2签名,

针对v1签名

如果apk是使用v1签名,那么比较麻烦,需要解压apk,找到META-INFO目录下的 CERT.RSA,然后执行命令

keytool -printcert -file CERT.RSA

针对v1签名可能有更好的办法,我没找到~

代码签名检查

如果是使用v2签名还好,直接一个命令就能查看签名,但是大部分游戏发行商都是使用v1签名,手动验证签名还是比较麻烦的,还是代码里验证下比较香啊~

fun checkSign(context: Context) {
val signCheck = SignCheck(context, “A3:E1:5E:BA:…”)
if (signCheck.check()) {
Log.i(TAG, “签名正确”)
} else {
toast(“应用签名不匹配,请检查签名”)
}
}
复制代码
06)]

这个命令虽然简单粗暴,但是要求apk使用v2签名,

针对v1签名

如果apk是使用v1签名,那么比较麻烦,需要解压apk,找到META-INFO目录下的 CERT.RSA,然后执行命令

keytool -printcert -file CERT.RSA

[外链图片转存中…(img-o7tsnUUd-1639300677407)]

针对v1签名可能有更好的办法,我没找到~

代码签名检查

如果是使用v2签名还好,直接一个命令就能查看签名,但是大部分游戏发行商都是使用v1签名,手动验证签名还是比较麻烦的,还是代码里验证下比较香啊~

fun checkSign(context: Context) {
val signCheck = SignCheck(context, “A3:E1:5E:BA:…”)
if (signCheck.check()) {
Log.i(TAG, “签名正确”)
} else {
toast(“应用签名不匹配,请检查签名”)
}
}
复制代码

Logo

技术共进,成长同行——讯飞AI开发者社区

更多推荐