obb的制作及使用

obb制作官方文档参考:

前言:

  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 参数指定内容的输出目录。

注意:转储现有OBB文件时,可以省略该 -d 参数以获取文件内的目录列表,而不提取内容。

-v

设置该工具的详细输出。

-about

显示该jobb工具的版本和帮助信息。

2、使用winRAR进行压缩(推荐)

将需要压缩的文件放入assets 或者根据目录中在使用winRAR进行压缩。
选中assets文件夹右击—添加到压缩文件,打开下面的窗口,根据命名规则进行命名。


如果你的扩展文件是一些媒体文件并且你不想解压资源包,而是借助media playback call

(例如MediaPlayer.setDataSource() and SoundPool.load())直接播放资源包里面的媒体文件。


那么在创建扩展文件包的时候务必不要压缩文件,而仅仅是打包即可 压缩方式为:存储




android里的obb可以删吗 andriod/obb_上传


压缩过程



三、扩展文件的保存位置

  当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 account
    public static final String BASE64_PUBLIC_KEY ="YourAndroidMarketLVLKey";
    // You should also modify this salt
    public 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
    };

    @Override
    public String getPublicKey() {
        return BASE64_PUBLIC_KEY;
    }

    @Override
    public byte[] getSALT() {
        return SALT;
    }

    @Override
    public 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 {

    @Override
    public 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 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 。

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

首先要获取扩展文件的路径,可以通过如下代码完成该操作:

// 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);
    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 block
    e.printStackTrace();
}