藍師傅最近幾個月很是忙,好久沒更新文章了,慚愧慚愧,距離上一篇技術文章已是半年前了~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工程師必須跨越的檻,我會結合實際項目,來完成這個系列的文章。
敬請期待~