蓝师傅最近几个月很是忙,好久没更新文章了,惭愧惭愧,距离上一篇技术文章已是半年前了~java
前几个月负责游戏SDK的开发、维护、对接工做,项目结束了一段时间了,梳理一下游戏SDK开发涉及到的知识点。android
有些朋友可能对游戏SDK开发有点陌生,但愿本文对你有一些帮助。git
记得17年毕业那会儿找工做的时候,去一家公司面试,面的是游戏SDK岗位,面试官一开口就问我以前有没有作过SDK开发,我说没作过,结果聊了几句就回去等通知了~github
HR在筛选简历的时候可能出了点问题,可是,从技术角度讲,没作过SDK开发就不能胜任这个岗位了吗?面试
SDK(Software Development Kit)是软件开发工具包的意思,通常咱们将一部分功能单独封装成一个library进行开发和维护,而后将编译产物(jar包或者aar)提供给多个项目使用,这就属于SDK开发。 常见的如短视频SDK、推送SDK,分享SDK,以及这篇文章的重点:游戏SDK。算法
小红是作社交App的娱乐公司,日活几千万,想让本身平台多元化,好比作个游戏下载的功能,给用户下载,用户以为好玩,可能就会付费买装备,可是有个问题,小红并不会作游戏,若是单开一个产品线去研发游戏,投入是至关巨大的,因此想到能不能去外面接游戏进来。数据库
最终小红和小绿确认合做关系:编程
一、小红提供游戏SDK,须要包含核心的登陆功能、支付功能;
二、小绿开发完的游戏,不接入其它平台的登陆和支付系统,直接接入小红的游戏SDK,用小红的登录和支付系统
三、小红每月都要跟小绿对帐,按比例分红给小绿缓存
小结: 游戏SDK跟普通SDK的区别在于,它提供一个游戏帐号体系和支付体系,核心就是登陆和支付功能。安全
游戏SDK最核心的是登陆和支付功能,其它的都是运营相关的,例如埋点、数据统计等等~
登陆和支付的流程大概以下图:
图画的比较简陋,解释一下,上半部分是登陆流程、下半部分是支付流程,
流程还算比较简单的~
接下来讲说游戏SDK开发的一些须要注意的点:
不少开发者都知道,做为SDK,应该尽可能少使用开源库,或者说不用开源库,
而是经过手写网络框架,手写数据库等等,主要是考虑两个方面:
固然,依赖库并非说不能用,有时候一些数据统计的库须要依赖第三方,那这种状况是没有办法避免的,能够在对接文档中提供一个解决依赖冲突的办法
在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,显然第一种方式比较简单粗暴。
面向接口编程,以游戏SDK为例,对外暴露的接口通常有SDK初始化、登陆、支付等,参考设计以下:
定义接口:
interface IGame {
// 一、在Application中调用,
fun registerApp(context: ApplicationContext, appId: String)
// 二、在activity中初始化
fun init(activity: Activity)
// 三、业务接口,登陆、支付等等
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)
}
}
复制代码
kotlin
的object
关键字表示单例,
外部经过GameSDKManager.xxx
来调用SDK中的方法,
之后要提供其它方法,只要修改 IGame
接口,而后在 GameSDKManager
和 GameImpl
中分别实现便可。
固然,不是说必定要这样拆分三个类,这只是一个面向接口编程的例子。
游戏SDK前期开发自测多是很顺利的,难度不大,可是在跟游戏对接的时候可能会出现一些问题, 什么ClassNotFound、Resource not found、依赖冲突、崩溃等等
,至于为何这样,下面会介绍~
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
看如下这段代码
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打包的主要流程
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文件里面的常量~
谷歌提供了相关的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.
*
* <p>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 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,又是如何读取的呢?
编译的第一个阶段,使用AAPT打包资源文件,产物以下
重点关注资源索引表 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 这个资源索引表。
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有检查更新的功能,token过时,游戏须要作退出登陆逻辑等等...
下面将介绍我是如何处理一些问题的。
SDK接入出现问题,release版本若关闭了日志,咱们须要将日志打开复现问题,经常使用的有两种方式:
能够参考开发者模式的开关,设置某个控件的点击事件,例如在连续点击5次的时候打开日志开关。 日志开关须要持久化,例如保存到sp,在SDK初始化的时候去读这个开关。
还有一种作法是相似友盟,初始化的方法提供debug参数,让接入方能够传true来查看日志,可是考虑到SDK内部信息安全,我没有这么作。
我提供的demo运行是正常的,可是第三方他们接入的时候常常会出现一些问题,多是他们的Android SDK版本不同,或者一些配置没有严格按照文档来写,做为SDK的开发者,我但愿这些配置的问题接入方能够本身发现和处理,这就须要在游戏SDK中增长检测的逻辑。
Android 8.0 开始,调起应用安装页面,须要用户显式打开未知来源开关,因而有以下代码
有一次发如今接入方的apk中,context.packageManager.canRequestPackageInstalls()
,一直返回false,无法调起安装页面,首先想到的是,接入方没有声明安装权限
<!--安装apk须要的权限-->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
复制代码
而后本身去掉权限声明验证一下,发现会抛异常,说明不是这个缘由。
最后发现 targetSdkVersion 小于26的话, packageManager.canRequestPackageInstalls()
一直会返回false,目前各大应用市场已经陆续要求targetSdkVersion必须26或以上,为了保证SDK的更新功能正常,在SDK初始化的时候,添加以下检测代码
这样接入方targetSdkVersion就必定要26或以上,不然抛异常,从异常日志中就能够发现问题。
因为 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配置是正确的。
游戏方接入游戏SDK以后打包成apk,这个apk要在咱们平台上线,咱们但愿统一apk签名, 因此在验收apk的时候,须要确认apk的签名。
查看apk签名主要用两种方式:
keytool -printcert -jarfile xxx.apk
or
apksigner verify -v --print-certs xxx.apk
这个命令虽然简单粗暴,可是要求apk使用v2签名,
若是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("应用签名不匹配,请检查签名")
}
}
复制代码
SignCheck
类的逻辑主要是获取应用签名,check方法是将应用签名跟正确的签名作对比,相同就返回true。
若是签名不正确,游戏方接入SDK过程会弹toast提示
若是有其它必选配置,相似的方式处理一下,一劳永逸~
渠道包你们都不陌生,通常是为了统计app在不一样应用市场的数据,例如新增、日活、留存等。
游戏SDK的渠道包概念稍微有点不一样:
平台上线了游戏以后,依赖用户本身来下载游戏,起量是很慢的,因此须要推广,若是使用推送让用户去下载,那么用户体验会不好。因此须要让那些有影响力的人来作有偿推广。SDK中每个请求接口都会传渠道标识,好比A用户去推广游戏,咱们会给他一个打了A渠道标识的apk,经过这个apk注册的用户,就归属A用户。
按照签名方式的不一样,目前有两个比较热门的打渠道包的开源库
下一代Android打包工具:https://github.com/mcxiaoke/packer-ng-plugin
有两个版本,支持v1签名和v2签名。
Walle(瓦力):https://github.com/Meituan-Dianping/walle
目测只支持v2签名。
对于游戏SDK来讲,单纯使用Walle并不适合,由于大部分游戏发行商,默认的apk签名方式都是v1签名。
成年人不喜欢作选择,两个都要
fun getChannel(context: Context): String {
//针对v1签名
var channel = PackerNg.getMarket(context)
if (TextUtils.isEmpty(channel)) {
//针对v2签名
channel = WalleChannelReader.getChannel(context, Utils.getDefaultChannel())
}
return channel
}
复制代码
可使用 PackerNg-v1 + PackerNg-v2,也可使用 PackerNg-v1 + Walle。
PackerNg-v1 的原理:
APK文件实际上是一个带签名信息的ZIP文件,根据 ZIP文件格式规范,ZIP文件末尾有一部分元数据表明ZIP文件注释,正确修改这一部分数据不会对ZIP文件形成破坏
针对v1签名,还有其它渠道包方案,可是大部分都存在效率问题,例如利用gradle的productFlavors属性打渠道包,速度慢;或者利用META-INF目录不被签名校验的特色,加入文件名为渠道名的空文件,可是读取渠道的时候比较慢,由于须要解压apk读取。
使用v2签名的apk,上面针对v1签名的方案所有失效。
Walle 的原理是:
V2签名块中有个区块能够添加一些附属信息,而且不会被签名校验,将自定义渠道信息写入这个区块,生成渠道包。
前期,游戏发行商出的apk可能没有使用咱们的签名,让他们从新打包有时候耗时比较长,因此必须掌握apktool的相关命令,来进行解包和打包,以及签名。
须要配置下环境,比较简单,mac:下载apktool.jar、apktool可执行脚本,放到 /usr/local/bin/ 目录下,而后 command + x 设置权限就能够了。
apktool d demo.apk
会将demo.apk反编译以后输出到demo目录,-o 参数能够指定输出目录。
反编译以后就能够修改资源文件或者字节码
apktool b demo -o unsign.apk
输出的是未签名的apk,须要签名才能安装到手机上
通常咱们用Android Studio打一个签名的apk很简单
可是单独给一个未签名的apk签名,就须要借助签名工具,v1签名是使用jarsigner,v2签名是使用apksigner,
jarsigner -verbose
-keystore [签名文件路径]
-keypass [密码]
-storepass [密码]
-signedjar [输出apk路径] [须要签名的apk路径]
-digestalg [摘要算法的名称如SHA1]
-sigalg [签名算法的名称如MD5withRSA]
[证书别名]
例如个人签名文件叫 lizhigame.keystore,别名密码都是 lizhigame,那么签名命令以下
jarsigner -verbose -keystore lizhigame.keystore -keypass lizhigame -storepass lizhigame -signedjar sign.apk unsign.apk -digestalg SHA1 -sigalg MD5withRSA lizhigame
执行命令后能够看到控制台日志
V2 签名使用apkSigner,在SDK build-tools下,注意在版本25以上才有
apkSigner签名命令:
apksigner sign
--ks [签名文件]
--ks-pass pass:[密码]
--out [输出apk路径]
[须要签名的apk]
例如个人签名文件叫 lizhigame.keystore,别名密码都是 lizhigame,那么签名命令以下
apksigner sign --ks lizhigame.keystore --ks-pass pass:lizhigame --out sign_v2.apk unsign.apk
apksigner 签名过程没有任何提示,能够结合验证签名命令一块儿使用
验证签名
apksigner verify -v --print-certs sign_v2.apk
这篇文章是我对游戏SDK开发三个多月工做的总结和分享,游戏SDK开发,更多的是业务问题处理和对接问题处理。将重复性的工做作成自动化,经过代码检查配置的方式,强制让接入方按照咱们的要求来接入SDK,能够减小没必要要的沟通成本。
本文知识点总结:
若是你正在找工做,招聘网站上多多少少有一些游戏SDK开发的岗位,薪资通常不会过低,但愿这篇文章能带给你一些帮助。
最近几个月很是忙,没有太多精力写文章(主要仍是懒)~
接下来我仍是会抽时间坚持写的,主要方向是:高质量开发、高效开发、架构等方面,这是通往高级Android工程师必须跨越的槛,我会结合实际项目,来完成这个系列的文章。
敬请期待~