轉自:http://blog.csdn.net/myarrow/article/details/7760579
一、APK擴展文件基本知識
Android Market (Google Play Store)中每個APK文件的最大限制是50MB。如果您的程序中包含大量的數據文件,以前您只能把這些數據文件放到自己的服務器上,當用戶啟動程序的時候讓用戶去下載。現在這些數據文件可以直接上傳到Android Market了。在新的Market控制台上傳App的時候,可以添加擴展文件了。
下面就來看看什么時候該使用擴展文件,該如何使用?
每個APK可以有2個擴展文件,每個文件最大限制是2GB。為了減少用戶的帶寬消耗,最好使用壓縮格式文件吧。 這兩擴展文件具有不同的用途: 第一個被稱為 main (主)擴展文件,該擴展文件保護您程序中需要用到的附加數據; 第二個被稱為 patch 擴展(修補)文件,該文件是可選的,並且應該只包含一些不同版本的補丁數據。
當然您可以按照您需要的方式來使用這兩個擴展文件,不過Android官方還是推薦把這兩個文件的功能分開。main擴展文件包含核心數據,並且盡量不隨程序版本的升級去修改;而patch擴展文件可以隨程序版本的升級做修改。為了幫助大家理解具體的含義,我們使用一個地圖App來解釋下: 比如 Google 地圖程序需要包含一個離線地圖數據包,這樣可以方便用戶離線查看地圖,在程序發布的時候,可以把現有的離線數據包作為main擴展文件上傳到Market。 然后過了半年Google地圖更新了,新添加了一些剛剛修好的高速公路、新建立的商場 等信息,可以把這些新增的信息作為patch擴展文件使用。 這樣Google 地圖 1.0版本對應一個main擴展文件;而Google地圖1.1版本對應一個main擴展文件和一個1.1版本的patch擴展文件;Google地圖1.2版本對應一個main擴展文件和一個1.2版本的patch擴展文件。 這里面的main擴展文件是同一個文件而patch擴文件是隨版本變化的。 這樣的好處就是當程序升級的時候, 用戶不用重新下載main擴展文件了,只需要下載少量的新增文件即可,節省用戶流量。
二、擴展文件的命名格式
擴展文件可以使用任何文件格式(ZIP, PDF, MP4, 等)。不管任何文件格式Android都認為他們是obb(opaque binary blobs)文件,並且會根據如下文件命名規則來重命名擴展文件:
[main|patch].<expansion-version>.<package-name>.obb
main or patch 指定文件是main擴展文件還是patch擴展文件,每個APK只能有一個main擴展文件和一個patch擴展文件。
<expansion-version> 和第一次上傳該擴展文件的APK文件的android:versionCode一致。后續版本的APK可以重用前面上傳的擴展文件。 您程序的Java包名
<package-name> 例如程序的版本為5,程序的包名為org.goodev.expansion.downloader。則上傳的main擴展文件會被重命名為:main.5.org.goodev.expansion.downloader.obb
三、擴展文件的保存位置
當Android Market下載程序的擴展文件的時候會保存到系統的共享存儲區。為了確保程序正常運行,您不能刪除、移動或者重命名擴展文件。在某些設備上Market無法自動下載該擴展文件,那么您應該在程序啟動的時候去下載該文件並且保存到同樣的位置。 擴展文件保存位置如下: <shared-storage>/Android/obb/<package-name>/
<shared-storage> 代表共享文件的目錄路徑,通過函數getExternalStorageDirectory()獲取; <package-name> APK的Java包名。 對於每個App而言,該目錄下最多只能包含2個擴展文件。一個是main擴展文件另外一個是patch擴展文件。當更新程序的時候,如果有新的擴展文件則新文件會覆蓋舊的擴展文件。
如果您需要解壓縮擴展文件來使用,請注意不要刪除該.obb文件,並且也不要把文件解壓縮到該目錄。您應該把解壓縮后的文件保存到getExternalFilesDir()返回的目錄下面。如果有可能的話,最好使用程序能直接讀取的文件格式而不用再次解壓縮文件了。Android開發團隊提供了一個項目( APK Expansion Zip Library)可以直接讀取ZIP文件中的內容而不用解壓縮該文件. 需要注意的是:保存在系統共享存儲區的文件,用戶和其他APP也可以訪問。
四、下載擴展文件的流程
在大多數情況下,Market會在下載APK的同時去下載擴展文件。然而,在某些情況下Market無法下載擴展文件或者用戶刪除了以前下載的擴展文件,您的程序需要處理這種異常情況。當您的程序啟動的時候,可以檢測文件是否存在並且可以從Market上下載。
開發者檢查清單:
您可以通過下面的清單來檢查是否需要使用擴展文件
- 1.您的程序是否真的需要超過50MB的大小限制。在移動設備上空間是非常寶貴的,您應該盡可能減少App的大小。如果您僅僅是為了提供支持多種顯示設備的圖片資源的話,可以考慮使用發布多個APK的方式來減少每個APK的大小。
- 2.判斷哪些數據需要打包為擴展文件發布。
- 3.在程序中添加訪問共享存儲區中擴展文件的代碼
- 4.在程序的啟動Activity中添加檢測擴展文件是否存在,以及下載擴展文件的代碼
五、擴展文件的規則和限制
- 1.每個擴展文件最大為2GB
- 2.用戶必需要從Android Market獲取您的程序才能自動從Market中下載擴展文件
- 3.當在您的程序中下載擴展文件的時候,Market每次都會為每個文件生成一個唯一的下載URL,該URL會在短期內失效。
- 4.當你上傳一個新的APK的時候,可以選擇使用以前上傳的擴展文件
- 5.如果您使用多個APK文件來適配不同的設備,並且也希望使用多個擴展文件。為了獲取一個唯一的versionCode和不同的Market filter, 您需要分別為每個設備上傳不同的APK文件。
- 6. 不能通過更新擴展文件來發布一個新的版本。
- 7. 不要在obb/文件夾中保存其他數據
- 8.不要刪除或者重命名.obb文件
六、APK擴展文件使用實例
要在App中使用擴展文件,需要兩個附加的Android庫項目:
1)Google Market Licensing package
2)Google Market APK Expansion Library package
可以通過Android SDK Manager來下載,也可以直接通過如下鏈接下載:
https://dl-ssl.google.com/android/repository/market_licensing-r02.zip
https://dl-ssl.google.com/android/repository/market_apk_expansion-r01.zip
下載完成后使用market_licensing-r02.zip文件中的目錄google_market_licensing\library來創建一個庫項目;
然后使用market_apk_expansion-r01.zip中的google_market_apk_expansion\downloader_library來創建另外一個庫項目。
同時為了簡化對ZIP格式擴展文件的處理,在market_apk_expansion-r01.zip文件中還包含了一個對ZIP文件處理的庫項目:google_market_apk_expansion\zip_file。 如果您使用的擴展文件格式是ZIP,那么也可以創建這個庫項目。
1. 聲明需要的權限
<manifest...>
<!-- Required to access Android Market Licensing -->
<uses-permission android:name="com.android.vending.CHECK_LICENSE"/>
<!-- Required to download files from Android Market -->
<uses-permission android:name="android.permission.INTERNET"/>
<!-- Required to keep CPU alive while downloading files (NOT to keep screen awake) -->
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<!-- Required to poll the state of the network connection and respond to changes -->
<uses-permissionandroid:name="android.permission.ACCESS_NETWORK_STATE"/>
<!-- Required to check whether Wi-Fi is enabled -->
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<!-- Required to read and write the expansion files on shared storage -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> ... </manifest>
注意:默認情況下,下載庫項目需要的API level為4 而APK擴展ZIP庫項目需要API level為5.
准備工作完成后,下面來具體看看如何使用擴展文件。
2. 實現下載服務(Implementing the downloader service)
為了實現在后台下載文件,下載庫項目提供了一個Service實現,名稱為DownloaderService。您應該繼承自這個文件來實現您的下載服務。為了簡化下載服務的開發,該DownloaderService還實現了如下功能:
- 注冊一個BroadcastReceiver來監聽設備的網絡連接狀態的改變。如果網絡連接斷開就暫停下載;如果網絡連接恢復就繼續下載。
- 安排一個 RTC_WAKEUP 通知,當下載服務被終結的時候可以通過該通知來啟動下載服務
- 生成一個通知(Notification )來顯示下載的進度以及下載錯誤等狀態
- 允許您的程序手工的暫停和恢復下載
- 檢測共享存儲區掛載了並且可用,在下載文件之前檢測 文件是否已經存在、存儲空間是否足夠。如果出現問題就通知用戶。
您僅僅需要創建一個繼承自DownloaderService的類,並且實現如下三個函數即可:
getPublicKey():您Market賬號的 Base64 編碼 RSA 公共密鑰,可以通過如下網址獲取: https://market.android.com/publish/Home#ProfileEditorPlace:
getSALT(): 許可策略用來生成混淆器(Obfuscator)的一組隨機bytes。
getAlarmReceiverClassName(): 返回您程序中用來重啟下載進程的BroadcastReceiver類名稱。當某些情況下,下載服務被意外終止的時候通過該BroadcastReceiver類來重新下載。比如 進程管理的程序終止了下載服務。
下面是一個DownloaderService類的實現代碼:、
public class SampleDownloaderService extends DownloaderService { // You must use the public key belonging to your publisher account
publicstaticfinalString BASE64_PUBLIC_KEY ="YourAndroidMarketLVLKey"; // You should also modify this salt
publicstaticfinalbyte[] SALT =newbyte[] {1, 42, -12, -1, 54, 98, -100, -12, 43, 2, -8, -4, 9, 5, -106, -107, -33, 45, -1, 84 }; @Override publicString getPublicKey() { returnBASE64_PUBLIC_KEY; } @Override publicbyte[] getSALT() { returnSALT; } @Override publicString getAlarmReceiverClassName() { returnSampleAlarmReceiver.class.getName(); } }
然后在manifest文件中聲明該Service即可。非常簡單吧!
3. 實現 alarm receiver
為了檢測下載進程和重啟下載服務,DownloaderService會安排一個RTC_WAKEUP Alarm來發送一個Intent到程序的 BroadcastReceiver。你必需定義這個 BroadcastReceiver 來調用 Downloader Library提供的函數,通過該函數來檢測下載狀態和在必要的情況下重啟下載服務。
實現這個類也是非常簡單的,一般來說只要覆寫onReceive()函數並且調用DownloaderClientMarshaller.startDownloadServiceIfRequired()函數即可。如下所示:
public class SampleAlarmReceiver extendsBroadcastReceiver { @Override publicvoidonReceive(Context context, Intent intent) { try{ DownloaderClientMarshaller.startDownloadServiceIfRequired(context, intent, SampleDownloaderService.class); }catch(NameNotFoundException e) { e.printStackTrace(); } } }
注意這個類的名字就是前面getAlarmReceiverClassName()函數返回的名稱。然后在manifest文件中聲明該receiver即可。 同樣很簡單吧!
4. 開始下載擴展文件
程序的主Activity(通過Launcher圖標啟動的Activity)應該負責檢查擴展文件是否存在、如果不存在就啟動下載服務。 使用Downloader Library來下載需要遵守如下步驟:
1)檢查文件是否已經下載了 Downloader Library中的Helper類中包含了一些函數來簡化這個步驟: getExtendedAPKFileName(Context, c, boolean mainFile, int versionCode) doesFileExist(Context c, String fileName, long fileSize) 例如在示例項目中,在Activity的onCreate()函數中通過如下函數來檢查文件是否存在:
boolean expansionFilesDelivered() { for(XAPKFile xf : xAPKS) { String fileName = Helpers.getExpansionAPKFileName(this, xf.mIsBase, xf.mFileVersion); if(!Helpers.doesFileExist(this, fileName, xf.mFileSize,false)) return false; } return true; }
這里的XAPKFile對象保存了已知擴展文件的版本號和大小以及是否為main擴展文件。如果該函數返回false則啟動下載服務。
2)通過 DownloaderClientMarshaller.startDownloadServiceIfRequired(Context c, PendingIntent notificationClient, ClassserviceClass)該函數來開始下載。 該函數的參數如下: context: Your application’s Context. notificationClient: 用來啟動主Activity的PendingIntent。用在DownloaderService 創建的用來顯示下載進度的通知中。當用戶選擇該通知,系統調用該PendingIntent來打開顯示下載進度的Activity(一般而言就是啟動下載的Activity)。 serviceClass: 程序中繼承自DownloaderService的類。在必要的情況下會啟動該服務來開始下載。
這個函數返回一個整數來表示是否有必要下載文件。有如下幾個值:
- NO_DOWNLOAD_REQUIRED: 表示文件已經存在或者當前正在下載。
- LVL_CHECK_REQUIRED:表示需要授權驗證來獲取下載擴展文件的URL。
- DOWNLOAD_REQUIRED: 表示擴展文件的URL已經獲取到了,但是還沒開始下載。
LVL_CHECK_REQUIRED 和 DOWNLOAD_REQUIRED 在本質上是一樣的,一般而言您無需關注這個狀態。在您的主Activity中調用 startDownloadServiceIfRequired(),你只需要看看返回值是否為NO_DOWNLOAD_REQUIRED即可。如果返回值不是NO_DOWNLOAD_REQUIRED, Downloader Library 開始啟動下載,您應該更新程序界面來顯示下載進度;如果返回值是 NO_DOWNLOAD_REQUIRED,表明該文件已經下載好了,您的程序可以正常啟動了。
例如:
@Override public void onCreate(Bundle savedInstanceState) { // Check if expansion files are available before going any further
if(!expansionFilesDelivered()) { // Build an Intent to start this activity from the Notification
Intent notifierIntent =newIntent(this, MainActivity.getClass()); notifierIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); ... PendingIntent pendingIntent = PendingIntent.getActivity(this,0, notifierIntent, PendingIntent.FLAG_UPDATE_CURRENT); // Start the download service (if required)
intstartResult = DownloaderClientMarshaller.startDownloadServiceIfRequired(this, pendingIntent, SampleDownloaderService.class); // If download has started, initialize this activity to show download progress
if(startResult != DownloaderClientMarshaller.NO_DOWNLOAD_REQUIRED) { // This is where you do set up to display the download progress (next step)
... return; }// If the download wasn't necessary, fall through to start the app
} startApp();// Expansion files are available, start the app
}
3)當 startDownloadServiceIfRequired() 函數的返回值不是NO_DOWNLOAD_REQUIRED的時候,調用DownloaderClientMarshaller.CreateStub(IDownloaderClient client, ClassdownloaderService)函數來創建一個IStub實例。這個IStub實例提供了Activity和下載服務之前的綁定功能,這樣您的Activity就可以收到下載事件了。
CreateStub()函數需要一個實現了IDownloaderClient接口的類和DownloaderService的實現類作為參數。一般而言只要讓Activity實現IDownloaderClient接口即可。
Android開發團隊推薦在Activity的onCreate()函數中創建IStub對象(在startDownloadServiceIfRequired()函數之后創建)。
例如:
// Start the download service (if required)
int startResult = DownloaderClientMarshaller.startDownloadServiceIfRequired(this, pendingIntent, SampleDownloaderService.class); // If download has started, initialize activity to show progress
if (startResult != DownloaderClientMarshaller.NO_DOWNLOAD_REQUIRED) { // Instantiate a member instance of IStub
mDownloaderClientStub = DownloaderClientMarshaller.CreateStub(this, SampleDownloaderService.class); // Inflate layout that shows download progress
setContentView(R.layout.downloader_ui); return; }
當onCreate()函數返回以后,Activity會執行onResume()函數,在該函數中調用IStub的 connect() 函數。同樣在onStop()函數中調用IStub的 disconnect()函數。
例如:
@Override protectedvoidonResume() { if(null!= mDownloaderClientStub) { mDownloaderClientStub.connect(this); } super.onResume(); } @Override protectedvoidonStop() { if(null!= mDownloaderClientStub) { mDownloaderClientStub.disconnect(this); } super.onStop(); }
調用connect()用來綁定Activity和DownloaderService 。
5. 處理下載進度
要接收下載進度信息,需要實現IDownloaderClient 接口。該接口有如下函數:
onServiceConnected(Messenger m) 在初始化完IStub后,會回調該函數。該函數的參數是用來訪問您的DownloaderService的,通過 DownloaderServiceMarshaller.CreateProxy()函數來創建這個IDownloaderService對象,然后可以用這個對象來控制下載服務,比如 暫停、繼續下載等。
推薦的實現方式:
private IDownloaderService mRemoteService; ... @Override public void onServiceConnected(Messenger m) { mRemoteService = DownloaderServiceMarshaller.CreateProxy(m); mRemoteService.onClientUpdated(mDownloaderClientStub.getMessenger()); }
onDownloadStateChanged(int newState) 當下載狀態發生變化的時候調用該函數,例如 開始下載或者下載完成。
參數newState的值是IDownloaderClient接口中定義的一些常量之一(以 STATE_ 開頭的); 可以通過函數 Helpers.getDownloaderStringResourceIDFromState()來獲取一個狀態的文本描述,這樣用戶更容易理解。例如 STATE_PAUSED_ROAMING 對應的文本描述是: “Download paused because you are roaming/當前在漫游狀態,下載停止”
onDownloadProgress(DownloadProgressInfo progress) 該函數的參數DownloadProgressInfo包含了下載進度的各種信息,例如 預計完成時間、當前下載速度、完成的百分比等。可以根據該信息來更新下載界面。
另外還有一些有用的函數: requestPauseDownload() 暫停下載 requestContinueDownload() 恢復下載 setDownloadFlags(int flags) 設置下載的網絡標示。當前只支持一個標示:FLAGS_DOWNLOAD_OVER_CELLULAR。 通過移動網絡下載擴展文件。默認情況下該標示沒有啟用,所以默認情況下只通過WIFI下載。
6. 讀取擴展文件
首先要獲取擴展文件的路徑,可以通過如下代碼完成該操作:
// The shared path to all app expansion files
private final static String EXP_PATH ="/Android/obb/"; static String[] getAPKExpansionFiles(Context ctx, intmainVersion,intpatchVersion) { String packageName = ctx.getPackageName(); Vector<String> ret =newVector<String>(); if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { // Build the full path to the app's expansion files
File root = Environment.getExternalStorageDirectory(); File expPath =newFile(root.toString() + EXP_PATH + packageName); // Check that expansion file path exists
if(expPath.exists()) { if( mainVersion >0) { String strMainPath = expPath + File.separator +"main."+ mainVersion +"."+ packageName +".obb"; File main =newFile(strMainPath); if( main.isFile() ) { ret.add(strMainPath); } } if( patchVersion >0) { String strPatchPath = expPath + File.separator +"patch."+ mainVersion +"."+ packageName +".obb"; File main =newFile(strPatchPath); if( main.isFile() ) { ret.add(strPatchPath); } } } } String[] retArray =newString[ret.size()]; ret.toArray(retArray); return retArray; }
您可以在開始下載的時候,把擴展文件的版本號保存到 SharedPreferences 中,然后在這里使用。
7. 使用 APK Expansion Zip Library
APK Expansion Zip Library項目包含了對ZIP文件的處理,您可以通過該項目提供的函數來直接讀取ZIP文件內容而不用解壓縮擴展文件,它把ZIP擴展文件當一個虛擬文件系統來使用。
APK Expansion Zip Library項目包含如下類和函數: APKExpansionSupport 提供一些函數來訪問擴展文件名稱和ZIP文件。
getAPKExpansionFiles() 返回擴展文件的文件路徑
getAPKExpansionZipFile(Context ctx, int mainVersion, int patchVersion) 返回一個包含main擴展文件和patch擴展文件的ZipResourceFile。如果您同時提供了 mainVersion 和 patchVersion ,則該函數返回main和patch擴展文件的所有內容,如果patch中的內容和main中的有重復,則使用patch的內容覆蓋main中的內容。
ZipResourceFile 用來處理ZIP文件的類 getInputStream(String assetPath) 讀取ZIP文件中的具體文件,assetPath應該是相對於ZIP文件的路徑信息
getAssetFileDescriptor(String assetPath) 獲取ZIP文件中具體文件的 AssetFileDescriptor 信息。
APEZProvider 大多數的程序都不會用到這個類。具體情況請參考其文檔。
使用APK 擴展ZIP庫從ZIP文件中讀取文件參考代碼如下:
// Get a ZipResourceFile representing a merger of both the main and patch files
ZipResourceFile expansionFile = APKExpansionSupport.getAPKExpansionZipFile(appContext, mainVersion, patchVersion); // Get an input stream for a known file inside the expansion file ZIPs
InputStream fileStream = expansionFile.getInputStream(pathToFileInsideZip);
上面的代碼可以讀取main擴展文件或patch擴展文件(通過讀取兩個文件的合並文件來實現)中的任何文件
如果要讀取指定的擴展文件,其方法如下:
// Get a ZipResourceFile representing a specific expansion file
ZipResourceFile expansionFile = new ZipResourceFile(filePathToMyZip); // Get an input stream for a known file inside the expansion file ZIPs
InputStream fileStream = expansionFile.getInputStream(pathToFileInsideZip);
8. 測試擴展文件
在發布之前要測試兩個東東,下載文件和讀取文件。
測試讀取文件 在發布您的程序之前應該先測試下您的程序能否讀取擴展文件,測試很簡單,只要把擴展文件放到共享存儲區的特殊位置,然后啟動程序即可。 1)創建文件目錄: 如果程序的包名為org.goodev,就創建如下的目錄:Android/obb/org.goodev/ 2)把擴展文件添加到該目錄 如果程序的包名為org.goodev,則主擴展文件名如下:main.03.org.goodev.obb。 版本號可以為大於零的任意值。
3)現在可以啟動程序來測試讀取擴展文件的功能了。
測試下載文件 由於在某些情況下需要在程序第一次使用的時候手工下載擴展文件。所以需要測試來確保您的程序可以成功的獲取下載URL、下載文件並且保存到設備中。
您可以把程序上傳到Market,同時上傳擴展文件,然后不要發布程序。這樣擴展文件已經可以從Market下載了。 當你測試完成后再發布您的程序。
9. 更新程序
使用擴展文件的一大好處就是每次更新App 用戶不用重新下載幾十上百兆的數據文件了。Android Market讓你可以為每個APK提供兩個擴展文件,這樣可以避免每次更新App都重新下載主擴展文件數據,減少下載時間。
為了方便大家研究如下使用擴展文件,可以到這里下載示例項目代碼: http://code.google.com/p/goodev-demo-code/downloads/list 文件名:Market_Downloader_Sample.zip 里面包含了所需要的各種庫項目。 在Eclipse中導入即可使用。