SDK
SDK(Software Development Kit)是軟體開發工具包的縮寫,一般來說,SDK是用于給開發人員提供進行應用程式開發的工具的,這樣程式員就可以快速的開發出應用軟體,省去了編寫硬體代碼和基礎代碼架構的過程,我們常見的Android SDK就屬于這一類。除了這種比較大的SDK,我們平時開發的library也屬性SDK,隻不過功能比較單一,适用的場合也比較簡單,如短視訊SDK、推送SDK,分享SDK等。
而我們所做的遊戲SDK主要是用于第三方遊戲開發接入我們的賬号體系和支付體系,類似于友盟分享等聚合SDK。
遊戲SDK
遊戲SDK是啥
在遊戲行業中,會存在兩個最基本的角色,即遊戲開發和遊戲營運,一個遊戲能不能成功,除了技術體驗好之外,營運是一門很重要的學問,他們的關系如下圖所示。
正如前面說描述的一樣,遊戲和營運往往是單獨開來的,除非像騰訊、網易這些頭部大公司,不僅可以自己研發遊戲,還有實力自己推廣和營運遊戲。不過,事實上,很多小的遊戲開發商就那麼幾個人或者幾十個人,根本沒有自己的營運能力,而市面上正好有專業的遊戲營運公司,這時候它們就開始合作了。
在上面的圖例中,小紅是做社交App的娛樂公司,日活幾千萬,想讓自己平台多元化,比如做個遊戲下載下傳的功能,給使用者下載下傳,使用者覺得好玩,可能就會付費買裝備,但是有個問題,小紅并不會做遊戲,如果單開一個産品線去研發遊戲,投入是相當巨大的,是以想到能不能去外面接遊戲進來。
遊戲SDK的流程圖
遊戲SDK最核心的功能就是登入和支付,其它都是一些營運相關的,例如埋點、資料統計、崩潰等等。其中,登入的流程大體如下。
而支付的流程大體是先SDK,然後再通知遊戲支付結構,流程如下所示。
遊戲SDK開發要求
遊戲SDK作為基礎SDK,通常需要遵循一些基本的開發規範,例如,盡量少用第三方庫、減少對外接口、明确技術文檔等。
少依賴
作為SDK,我們應該盡量少使用開源庫或者說不用開源庫,盡量直接使用系統提供的庫,實在不行也可以手寫網絡架構,手寫資料庫等等,主要基于以下兩個方面考慮。
- 減小SDK體積 ;
- 避免第三方接入的時候發生依賴沖突
解決依賴沖突
當然,依賴庫并不是說不能用,有時候一些資料統計的庫就需要依賴第三方庫,那麼對于這種情況沖突是不可避免的,通常解決沖突有兩種常見的手段。
強制使用某個版本,例如:
configurations.all {
resolutionStrategy {
force 'com.android.support:support-v4:26.1.0' //解決v4包沖突,強制使用這個版本的v4包
}
}
複制
很多做應用開發的都知道,如果一個項目中重複使用了某個庫,那麼可以使用exclude排除某個依賴,如下:
implementation("com.xxx.xxx:xx") {
exclude group: 'com.android.support'
}
複制
exclude是最常用的解決依賴沖突的方式,但如果多個依賴庫引入不同版本的其它庫,需要分别寫好多個exclude,顯然第一種方式比較簡單粗暴。
減少對外接口
對于SDK開發,對外的接口盡量越少越好。以遊戲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) {
//支付邏輯
}
...
}
複制
實作類是我們SDK内部的處理邏輯,我們不希望被外部通路到,外部隻需要知道有 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)
}
}
複制
實際使用時,外部通過GameSDKManager.xxx來調用SDK中的方法,以後要提供其它方法,隻要修改 IGame接口,然後在 GameSDKManager 和 GameImpl 類中分别進行實作即可。
SDK開發注意點
遊戲SDK開發相比應用開發來說,技術難度一般不大,問題大多出在跟遊戲對接的時候。可能會出現一些問題, 比如ClassNotFound、Resource not found、依賴沖突、崩潰等等,對于這類問題,我們需要先分析問題的原因,然後确認由誰負責,最後确定修改方案。
1, SDK需要支援Eclipse
和應用開發不同,很多遊戲還是使用Eclipse進行開發的,是以在對接遊戲時需要提供Eclipse版本。需要說明的是,Eclipse 不能使用Android Studio版本的SDK,搭建Eclipse的Android環境需要使用ADT插件,具體怎麼使用請參考官網。
由于SDK的産物是aar,而Eclipse隻能依賴jar包和library,一般都用jar包依賴,是以先将aar解壓出來,把裡面的classes.jar拷貝出來重命名,然後在Eclipse中依賴這個jar包,同時SDK的資源檔案、libs目錄下的jar包也需要拷貝到Eclipse項目中。
2,setContentView相關問題
2.1 問題描述
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)
複制
我們打開源代碼檔案,然後将滑鼠放到R.layout.activity_test上,如下圖所示。
搞過Android開發的同學都知道,上面的常量是在AAPT打包的階段生成的,是一個R常量。對于Android工程是如何打包的,下面讓我們來簡單的回顧下流程:
- 使用AAPT(或者AAPT2)工具打包資源檔案,生成R.class檔案,resources.arsc資源索引表等;
- 如果有AIDL的跨程序調用,需要将AIDL 轉換成Jave代碼;
- 将Java代碼編譯成.class位元組碼檔案;
- 使用dex工具将.class檔案轉換成Dalvik 位元組碼,也就是.dex檔案;
- 通過ApkBuilde工具将.dex檔案和其它資源檔案打包成未簽名的apk;
- 通過簽名工具給apk簽名,v1簽名使用jarsigner、v2簽名使用apksigner(sdk 25版本開始提供)
Android的打包流程可以檢視:Android打包流程
apk編譯的第一個階段,AAPT會打包資源檔案,生成R.class檔案和resources.arsc資源索引表。對于library項目,在打包aar的時候,aar中并不需要生成 resources.arsc 資源索引表,資源的id跟資源檔案的映射關系記錄在R.txt中,如下圖所示。
Eclipse因為隻能接入jar包,也就是解壓aar後取出裡面的classes.jar,當我們把資源檔案拷貝到Eclipse,再編譯apk的時候,資源檔案會對應一個新的資源id,而aar中classes.jar裡引用的資源id是不變的,是以就會出現上面的問題。
知道這個問題後,要解決這個問題,那麼SDK裡面使用資源id就需要動态去擷取,不能使用R檔案裡面的常量。
2.2 動态擷取資源Id
谷歌提供了相關的API(getIdentifier),可以通過資源名稱擷取資源id,getIdentifier的源碼如下。
/**
* 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檔案裡面的内部類是對應的;第三個參數是應用的包名。例如,下面是第二個參數的R檔案對應。
為了友善動态擷取資源的Id,我們可以封裝個工具,如下所示:
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"))
複制
2.3 AAPT資源打包
以下是Android 官網給的apk的打包流程,如下下圖所示。
在apk編譯的第一個階段,需要使用AAPT(Android Asset Packaging Tool縮寫)打包資源檔案,産物如下。
- res檔案夾内的圖檔及xml資源(xml被編譯成二進制);
- assets檔案夾(不會生成資源id)
- 二進制AndroidManifest.xml
- 資源索引表 resources.arsc
- R.class檔案
我們需要重點關注的是資源索引表 resources.arsc,resources.arsc 檔案的資料格式比較複雜,我們可以将apk檔案拖到Android Studio中,然後選擇 resources.arsc打開,如下圖所示。
可以發現,resources.arsc檔案會包含很多的資源索引,打開layoyt檔案,會發現該檔案 主要由id(資源id)、name(資源名稱)、value(資源路徑)都可以通過這個索引表來互相轉換,前面說過 Resources#getIdentifier(String name, String defType, String defPackage),之是以可以通過資源名稱擷取到資源id,當然還是要借助 resources.arsc 這個資源索引表。
遊戲SDK支援與維護
如果是普通的遊戲SDK,那麼隻要保證接入方能夠成功接入SDK就完事了。然而,遊戲SDK的支援還需要對接入遊戲SDK的遊戲進行驗收,確定遊戲SDK的功能正常,能夠正常送出應用市場。并且,随着SDK的版本更新,功能會增加,需要驗收的功能會越來越多,例如:驗證簽名,SDK有檢查更新的功能,token過期,遊戲需要做登出邏輯等等。下面是遊戲SDK支援的一些場景和處理方式,以及經驗分享。
日志開關
衆所周知,不管是應用開發還是SDK開發,release版本一般都是關閉了日志的,我們需要使用日志複現問題,常用的有兩種方式:
- 參考開發者模式的開關,設定某個控件的點選事件,例如在連續點選5次的時候打開日志開關。然後将日志資料持久化,例如儲存到sp,在SDK初始化的時候去讀這個開關。
- 使用一些資料SDK,然後将資料上報到背景,考慮到SDK内部資訊安全,我們可以自己開關相關的SDK。
配置參數
有時候,我們提供的Demo工程是運作是正常的,但是第三方遊戲接入的時候經常會出現一些問題,可能是他們的Android SDK版本不一樣,或者一些配置沒有嚴格按照文檔來寫,作為SDK的開發者,我希望這些配置的問題接入方可以自己發現和處理,這就需要在遊戲SDK中增加檢測的邏輯。
1,檢查更新的功能
從Android 8.0 版本開始,調起應用安裝頁面需要使用者顯式打開未知來源開關,下面是系統的相關檢察源碼。
對于這個問題,首先想到是接入方沒有聲明安裝權限,如下。
<!--安裝apk需要的權限-->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
複制
然後,我們去掉權限聲明驗證發現還是會抛異常,說明不是這個原因。最後,經過分析發現當 targetSdkVersion 小于26的話, packageManager.canRequestPackageInstalls()一直會傳回false,是以各大應用市場已經陸續要求targetSdkVersion必須26或以上,為了保證SDK的更新功能正常,在SDK初始化的時候,添加如下檢測代碼。
2,FileProvider 需要增加配置檢查
由于 7.0之後安裝apk需要通過FileProvider來擷取url,是以需要在Manifest.xml添加如下代碼。
如果是Android Studio打包,一般會自動讀取build.gradle中的_PACKAGENAME_的值來替換占位符_PACKAGENAME_的資料,如果是Eclipse打包,占位符_PACKAGENAME_則原封不動,不會被替換。不管使用Eclipse打包就會報一個空指針,如下所示是Eclipse代碼。
FileProvider.getUriForFile(context,
context.packageName + ".fileprovider", file)
複制
那如何保證接入方一定有配置FileProvider,并且配置正确呢?我們可以增加配置檢測如下代碼。
在上面的代碼中,我們可以在sdk初始化的時候去私有目錄建立一個空檔案,然後通過 getUriFormFile 方法觸發FileProvider擷取url的邏輯,如果有異常則說明FileProvider配置不對,直接給出錯誤資訊。
3,簽名驗證
遊戲方接入遊戲SDK之後打包成apk,這個apk需要在我們平台上線,我們希望統一apk簽名, 是以在驗收apk的時候還需要确認apk的簽名。要檢視apk簽名,我們可以使用指令行和工具兩種方式。檢視簽名的指令如下:
v2簽名
keytool -printcert -jarfile xxx.apk
或者
apksigner verify -v --print-certs xxx.apk
複制
v1簽名
如果apk是使用v1簽名,那麼比較麻煩,首先需要解壓apk,找到META-INFO目錄下的 CERT.RSA,然後執行如下指令。
keytool -printcert -file CERT.RSA
複制
除了使用指令方式外,我們還可以使用工具來察看,如macOS的fHash等軟體,将apk檔案拖到軟體中即可,如下所示。
遊戲管道包
做過Android應用開發的同學對于管道包肯定不會陌生,由于Android應用市場衆多,如果要上不同的應用市場,那麼就需要打不同的管道包。Android多管道包除了可以使用Gradle多管道打包外,還可以使用美團Walle、友盟等多管道工具進行多管道打包。
不管,對于遊戲SDK來說,單純使用Walle并不适合,因為大部分遊戲發行商,預設的apk簽名方式都是v1簽名。參考過大多數的遊戲管道分發公司,基本都是使用打包腳本進行打包,并且打包腳本一般使用python開發。
Android v1的簽名是基于JAR 的,簽名jar Signature來自JDK,Android v2的簽名是基于APK Signature Scheme v2,是Android 7.0版本引入的,而最新的v2是對v2版本的優化,适用于Android 9.0及以上版本 。它們的差別如下:
- V1:應該是通過ZIP條目進行驗證,這樣APK 簽署後可進行許多修改 - 可以移動甚至重新壓縮檔案。
- V2:驗證壓縮檔案的所有位元組,而不是單個 ZIP 條目,是以,在簽名後無法再更改(包括zipalign)。正因如此,現在在編譯過程中,我們将壓縮、調整和簽署合并成一步完成。好處顯而易見,更安全而且新的簽名可縮短在裝置上進行驗證的時間(不需要費時地解壓縮然後驗證),進而加快應用安裝速度。
v1簽名
衆所周知,apk檔案其實就是一個帶簽名資訊的zip檔案,根據zip檔案格式規範,zip檔案末尾有一部分中繼資料代表zip檔案注釋,正确修改這一部分資料不會對zip檔案造成破壞,如下所示。
針對v1簽名,還有其它管道包打包方案,但是大部分都存在效率問題,例如利用gradle的productFlavors屬性打管道包,速度慢;或者利用META-INF目錄不被簽名校驗的特點,加入檔案名為管道名的空檔案,但是讀取管道的時候比較慢,因為需要解壓apk,涉及檔案的讀取。
v2簽名
V2簽名塊中有個區塊可以添加一些附屬資訊,并且不會被簽名校驗,将自定義管道資訊寫入這個區塊,生成管道包。可以參考下美團Walle。正如前文所說,我們使用的python打包腳本,應該不存在上面的問題。
Apk反編譯與重打包
反編譯
在Android逆向工程中,有一個很重要的工具,那就是Apktool。首先,我們到Apktool官網下載下傳下工具,當然我們也可以從其他地方進行下載下傳。下載下傳apktool.jar、apktool可執行腳本,放到 /usr/local/bin/ 目錄下,然後 command + x 設定權限就可以了。
然後,我們使用apktool d指令反編譯apk檔案,如下所示。
apktool d demo.apk
複制
其中,如果不指定目錄那麼預設會輸出到原目錄,如果需要指定目錄,那麼可以使用-o 參數來指定輸出目錄。反編譯之後,接下來就可以修改資源檔案或者位元組碼。
回編譯
修改資源檔案或者位元組碼檔案後,我們需要回編譯包,回編譯的指令如下。
apktool b demo -o unsign.apk
複制
不過,上面的輸出的是未簽名的apk,需要簽名才能安裝到手機上。
apk簽名
對于Android應用開發來說,可以直接使用Android Studio來制作一個簽名檔案。但是,單獨給一個未簽名的apk簽名,就需要借助簽名工具,v1簽名是使用jarsigner,v2簽名是使用apksigner。其中,v1簽名的指令如下:
jarsigner -verbose
-keystore [簽名檔案路徑]
-keypass [密碼]
-storepass [密碼]
-signedjar [輸出apk路徑] [需要簽名的apk路徑]
-digestalg [摘要算法的名稱如SHA1]
-sigalg [簽名算法的名稱如MD5withRSA]
[證書别名]
複制
例如,我有一個簽名檔案叫 demo.keystore,别名密碼都是 123456,那麼簽名指令如下。
jarsigner -verbose -keystore demo.keystore -keypass 123456 -storepass 123456 -signedjar sign.apk unsign.apk -digestalg SHA1 -sigalg MD5withRSA 123456
複制
如果需要使用v2 簽名,由于v2簽名使用的是apkSigner,在SDK build-tools下,注意在版本25以上才有。
使用apkSigner簽名的指令如下所示。
apksigner sign
--ks [簽名檔案]
--ks-pass pass:[密碼]
--out [輸出apk路徑]
[需要簽名的apk]
複制
例如,簽名檔案叫 demo.keystore,别名密碼都是 123456,那麼apkSigner簽名指令如下。
apksigner sign --ks demo.keystore --ks-pass pass:123456 --out sign_v2.apk unsign.apk
複制
apksigner 簽名過程沒有任何提示,可以結合驗證簽名指令一起使用,如下所示。
apksigner verify -v --print-certs sign_v2.apk
複制
到此,Android 遊戲SDK相關的開發和管道包相關的就介紹完了,後面回對打包遇到的一些問題進行補充,如果你對遊戲聯運有興趣,也歡迎留言交流。