本文主要是介绍obb的生成及使用,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
obb的制作及使用
obb制作官方文档参考:
- obb制作及使用官方文档
- obb文件生成可参考jobb工具生成(官网推荐)
前言:
Google Play应用商店在上传限制100MB大小,超过该大小的应用必须将超过部分以扩展文件的形式进行上传处理,总共可上传2个扩展文件,每个最大文件可为2GB,同时obb文件格式【扩展文件可以使用任何文件格式(ZIP, PDF, MP4, 等)。不管任何文件格式Android都认为他们是obb(opaque binary blobs)文件】可自选。
对每个App而言,该目录下最多只能包含2个扩展文件。一个是main扩展文件另外一个是patch扩展文件,所以一般只需要处理main扩展文件。
一 、OBB命名规则
[main|patch].<expansion-version>.<package-name>.obb
文件名包含四部分,其中:
- main|patch: 用来表示是主扩展还是补充扩展。
- expansion-version: 当前上传apk的version code。
- package-name: apk的包名
- obb:后缀,其实本质就是zip文件,Google play会自动将zip文件改为obb文件。
例如,假设您的APK版本是(versionCode)25,你的包名为com.example.app。如果上传的主扩展文件,该文件被重命名为:
main.25.com.example.app.obb
注意:
- 上传APK时,OBB会另外上传,不能只更新OBB文件
- 下载应用程序时,OBB文件下载到%external storage%/Android/obb/package name目录
二、obb 生成方法:
1、google官方提供的工具 jobb
制作成OBB(Opaque Binary Blob)格式文件的一种工具,在Android SDK中, %ANDROID——HOME/tools%中,在tools/bin下有jobb。
命令:
jobb -d [目录名称的完整路径] -o [输出目标文件的完整路径] -pn [软件包名] -pv [包版本]
注:如果没有将android sdk配置环境就需要打开命令窗口后将jobb.bat拖入cmd窗口内才能执行
EG:
源文件目录 D:contents\main\assets\ 目标文件夹: D:\obb\output.obb 软件包名 com.example.app 包版本 25
jobb -d D:\contents\ main\assets\ -o D:\obb\output.obb -pn com.example.app -pv 25
注意:
- jobb -d,-o 需要指定完整路径。如果指定相对路径,%ANDROID_HOME%/tools/将作为相对路径
- JOBB工具如果不能输出有一定容量的.obb的文件的话,出现错误提示
- 下表列出了该jobb工具的命令行选项。
选项 | |
-d | 设置用于创建OBB文件的输入目录,或在提取(-dump)现有文件时设置输出目录。创建OBB文件时,指定目录及其所有子目录的内容都包含在OBB文件系统中。 |
-o | 指定OBB文件的文件名。创建OBB并提取(转储)其内容时,此参数是必需的。 |
-pn | 指定安装OBB文件的应用程序的软件包名称,该名称对应package于应用程序清单中指定的值。创建OBB文件时需要此参数。 |
-pv | 设置可以挂载OBB文件的应用程序的最低版本,该版本对应android:versionCode于应用程序清单中的值。创建OBB文件时需要此参数。 |
-k | 指定用于加密新OBB文件或解密现有加密OBB文件的密码。 |
-ov | 创建OBB文件,该文件是现有OBB文件结构的叠加层。此选项允许将新包装内容装入与先前包装相同的位置,并用于创建以前生成的OBB文件的修补程序版本。覆盖OBB文件中的文件替换具有相同路径的文件。 |
-dump | 提取指定OBB文件的内容。使用此选项时,还必须使用-d 参数指定内容的输出目录。 |
| |
-v | 设置该工具的详细输出。 |
-about | 显示该jobb工具的版本和帮助信息。 |
2、使用winRAR进行压缩(推荐)
将需要压缩的文件放入assets 或者根据目录中在使用winRAR进行压缩。
选中assets文件夹右击—添加到压缩文件,打开下面的窗口,根据命名规则进行命名。
- tip:
- 如果你的扩展文件是一些媒体文件并且你不想解压资源包,而是借助media playback call
(例如MediaPlayer.setDataSource() and SoundPool.load())直接播放资源包里面的媒体文件。
那么在创建扩展文件包的时候务必不要压缩文件,而仅仅是打包即可 压缩方式为:存储
压缩过程
三、扩展文件的保存位置
当Android Market下载程序的扩展文件的时候会保存到系统的共享存储区。为了确保程序正常运行,您不能删除、移动或者重命名扩展文件。在某些设备上Market无法自动下载该扩展文件,那么您应该在程序启动的时候去下载该文件并且保存到同样的位置。
扩展文件保存位置如下:
<shared-storage>/Android/obb/<package-name>/
<shared-storage> 代表共享文件的目录路径,通过函数getExternalStorageDirectory()获取;
<package-name> APK的Java包名。其中shared-storage是设备的primary external storage。
对于每个App而言,该目录下最多只能包含2个扩展文件。一个是main扩展文件另外一个是patch扩展文件。当更新程序的时候,如果有新的扩展文件则新文件会覆盖旧的扩展文件。
如果您需要解压缩扩展文件来使用,请注意不要删除该.obb文件,并且也不要把文件解压缩到该目录。您应该把解压缩后的文件保存到getExternalFilesDir()返回的目录下面。如果有可能的话,最好使用程序能直接读取的文件格式而不用再次解压缩文件了。Android开发团队提供了一个项目( APK Expansion Zip Library)可以直接读取ZIP文件中的内容而不用解压缩该文件.
需要注意的是:保存在系统共享存储区的文件,用户和其他APP也可以访问。
四、APK扩展文件使用实例
要在App中使用扩展文件,需要两个附加的Android库项目:
- Google Market Licensing package-
- 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-permissionandroid:name="com.android.vending.CHECK_LICENSE"/><!-- Required to download files from Android Market --><uses-permissionandroid:name="android.permission.INTERNET"/><!-- Required to keep CPU alive while downloading files (NOT to keep screen awake) --><uses-permissionandroid: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-permissionandroid:name="android.permission.ACCESS_WIFI_STATE"/><!-- Required to read and write the expansion files on shared storage --><uses-permissionandroid: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 accountpublic static final String BASE64_PUBLIC_KEY ="YourAndroidMarketLVLKey";// You should also modify this saltpublic static final byte[] SALT =new byte[] {1,42, -12, -1,54,98,-100, -12,43,2, -8, -4,9,5, -106, -107, -33,45, -1,84};@Overridepublic String getPublicKey() {return BASE64_PUBLIC_KEY;}@Overridepublic byte[] getSALT() {return SALT;}@Overridepublic String getAlarmReceiverClassName() {return SampleAlarmReceiver.class.getName();}
}
然后 在Manifest文件中声明该Service即可。非常简单吧!
<application ...><service android:name=".SampleDownloaderService" />
</application>
- 实现AlarmReceiver
为了检测下载进程和重启下载服务,DownloaderService会安排一个RTC_WAKEUP Alarm来发送一个Intent到程序的 BroadcastReceiver。你必需定义这个 BroadcastReceiver 来调用 Downloader Library提供的函数,通过该函数来检测下载状态和在必要的情况下重启下载服务。
实现这个类也是非常简单的,一般来说只要覆写onReceive()函数并且调用DownloaderClientMarshaller.startDownloadServiceIfRequired()函数即可。
定义AlarmReceiver继承自BroadcastReceiver如下所示:
public class AlarmReceiver extends BroadcastReceiver {@Overridepublic void onReceive(Context context, Intent intent) {try {PrintLog.d("AlarmReceiver startDownloadServiceIfRequired");DownloaderClientMarshaller.startDownloadServiceIfRequired(context, intent,YRDownloaderService.class);} catch (NameNotFoundException e) {e.printStackTrace();} }}
然后 在Manifest文件中声明该Receiver即可。非常简单吧!
<application ...><service android:name=".AlarmReceiver" />
</application>
3.开始下载扩展文件
程序的主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))returnfalse;}returntrue;
}
这里的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 furtherif(!expansionFilesDelivered()) {// Build an Intent to start this activity from the NotificationIntent 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 progressif(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 IStubmDownloaderClientStub = DownloaderClientMarshaller.CreateStub(this,SampleDownloaderService.class);// Inflate layout that shows download progresssetContentView(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 。
4. 处理下载进度
要接收下载进度信息,需要实现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下载。
5.读取扩展文件
将APK扩展文件下载下来之后,紧接着我们就要考虑如何使用它了。但是Android 6.0的一些实现(API级别23)以及以后的一些实现仍然需要权限,因此需要在应用程序清单中声明存储权限,并在运行时请求外部存储权限,如下所示:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
或者
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
在使用obb扩展文件前需要将Zip Library的库移入自己的项目中进行依赖使用此库可以轻松地将ZIP扩展文件中的资源作为虚拟文件系统读取。
Tip:(Zip Library位于其中/extras/google/google_market_apk_expansion/zip_file/)
使用APKExpansionSupport类来获取obb中的资源
提供一些访问扩展文件名和ZIP文件的方法:
- getAPKExpansionFiles()
- 上面显示的相同方法返回两个扩展文件的完整文件路径。
getAPKExpansionZipFile(Context ctx, int mainVersion, int patchVersion)
返回ZipResourceFile表示主文件和补丁文件的总和。也就是说,如果同时指定mainVersion和patchVersion,则返回一个ZipResourceFile提供对所有数据的读访问权,并将补丁文件的数据合并到主文件的顶部。 - ZipResourceFile
- 表示共享存储上的ZIP文件,并执行基于ZIP文件提供虚拟文件系统的所有工作。您可以使用- APKExpansionSupport.getAPKExpansionZipFile()或ZipResourceFile通过将实例传递给扩展文件来获取实例。此类包含各种有用的方法,但您通常不需要访问其中的大多数方法。一些重要的方法是:
-
- getInputStream(String assetPath)
提供InputStream读取ZIP文件中的文件。的 assetPath必须的路径所需的文件,相对于的ZIP文件内容的根。 - getAssetFileDescriptor(String assetPath)
提供一个AssetFileDescriptor对ZIP文件中的文件。的assetPath必须的路径所需的文件,相对于的ZIP文件内容的根。这对某些需要的Android API很有用AssetFileDescriptor
- getInputStream(String assetPath)
首先要获取扩展文件的路径,可以通过如下代码完成该操作:
// 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 filesFile root = Environment.getExternalStorageDirectory();File expPath =newFile(root.toString() + EXP_PATH + packageName);// Check that expansion file path existsif(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);returnretArray;
}
示例提取资源:
ProvinceUtil provinceUtil = new ProvinceUtil();
ZipResourceFile expansionFile = null;
try {expansionFile = APKExpansionSupport.getAPKExpansionZipFile(getApplicationContext(), BuildConfig.VERSION_CODE, 0);if (expansionFile != null) {AssetFileDescriptor fd = expansionFile.getAssetFileDescriptor("assets/my_province_data.txt");PrintLog.e("-------" + fd.getLength());InputStream inputStream = expansionFile.getInputStream("assets/my_province_data.txt");if (inputStream != null) {provinceUtil.initProvinceDatas(inputStream);String[] provinces = provinceUtil.getmProvinceDatas();if (provinces != null) {//参考ArrayAdapter的构造函数listView.setAdapter(new ArrayAdapter<String>(this,android.R.layout.simple_list_item_1,provinces));}}}
} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();
}
这篇关于obb的生成及使用的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!