preface

  • In general, Android developers should reduce the size of the generated Apk by removing invalid resource files, keeping only xxHDPI resources, and lazily loading non-essential resources offline.
  • Under special circumstances, for the consideration of user experience, some APPS that rely on high-definition lossless resources may generate installation packages of hundreds of megabytes or even more than 1G. Domestic distribution platforms have no mandatory regulations on the size of installation packages, but for overseas products, Google Play does not allow developers to upload installation packages of more than 100M.
  • To address these issues, Google officially provides Apk Expansion Files, which allows developers to build installation packages of more than 100 megabytes.

concept

  • First, in the traditional Android development world, subcontracting refers to MultiDex, the technique of breaking up a single dex into multiple functions to break through the number of functions bottleneck. In this case, Apk Expansion Files refer to the separation of Apk Files and large volume resource Files, including high definition large image Files, audio Files, video Files, etc., which will be compressed into a unified. Obb file. Note that the content extracted to obB does not include runtime code. Therefore, developers need to ensure that the application will still run properly without the.obb file.

  • Before subcontracting, developers need to know exactly what the bulk resource files in the project are. In most cases, they refer to assets and raw files. If the drawable and Mipmap directories have more than 1M files, you can also consider subcontracting them. In this case, the developer needs to change the reference mode of the resource from directly using the resource ID: r.draable. XXX to parsing from the file.

  • All resource files will be compressed into OBB files and eventually uploaded to GooglePlay for users to download.

Obb file

  • concept

    Opaque Binary Blob Opaque Binary Blob Opaque Binary Blob Opaque Binary Blob Seeing this definition, it’s easy to think of another file format – zip files. So, in essence, obB files and ZIP files are the same, they just have different interpretations in different areas. Obb has its own set of rules for Android subcontracting.

  • Naming rules

    Obb is named as follows:

    [main/patch].[versionCode].[packageName].obb
    Copy the code
    • The first part consists of optional fields that can only be filled in as main or patch. Main refers to the main extension file, and Patch is a patch or extension to Main. Fill in main for the first subcontract, and patch for subsequent incremental modifications to the subcontract. I’m in the habit of repackaging all resources into OBB files every release, so I only use the main field.
    • The second part is the versionCode of the current app. After confirming the versionCode of this release, you can fill it in boldly.
    • The third part is packageName, which can be obtained by reading the package field in the root node of androidmanifest.xml.
    • Finally, add the obB file suffix.
    • Here’s an example:
    main.16.com.example.obbtest.obb
    Copy the code
  • Generating methods

    • Method one:

      Jobb is an official tool for generating obB files, which can be found in the Android\ SDK \tools\bin folder. This is a command line tool with the following usage and parameters:

      $ jobb -d[Path of all resources] -o [generated OBB name (follow the above naming rules)] -k [package password] -pn [package name] -pv [versionCode(same as the versionCode of obB name)]Copy the code

      You can also use this tool to decompress obB files:

      $  jobb -d[output path] -o [obb file name] -k [password used for packaging]Copy the code
    • Method 2:

      Using the compression tool, you can use the Windows or Mac packaging tool to compress the file into a ZIP package and change the file name.

      Note that the compressed file format needs to select ZIP and change the compression mode to storage. If encryption is required, you can use the password setting method delivered with the compression tool to obtain the same effect as setting -k in the official method. After compression, do not forget to change the file name to the obB file name that complies with the naming convention, for example:

      main.16.com.example.obbtest.obb
      Copy the code
    • Method 3:

      Gradle packaging is a collective packaging of resources that need to be pushed into obB by adding compressed scripts to build.gradle. This method is described in more detail later in this article.

Upload obB tests

  • Local test The principle of local test is to copy the OBB file to the corresponding directory, imitating Google Play download. Obb files downloaded from Google Play are stored in the following path:

    / Android/obb/App/package nameCopy the code

    So, create [app package name like com.example.obbtest] folder under /Android/obb/ and copy obB file to this directory to simulate Google Play app installation.

  • The online test

    • Log in to the Google Play Console developer account, open the App list, and select the App you want to test:

    • Select Release Managerment from the left control bar, then App Release, and finally MANAGE Internal Test of Internal Test to publish the Internal test version.

    • Create a new release in internal test: Upload the GooglePlay version of the Apk. After uploading, click the Add More button to the right of the Apk to submit the OBB file. Note that the version of the OBB file must be the same as the version of the uploaded Apk. It is recommended that you test with a version of code that is not available online, such as a mobile phone number, girlfriend’s birthday, etc., so that the version number will not be used when submitting the official version later (I wonder why GooglePlay’s internal test and the official release can not be repeated).

    • Fill in the rest and send it. Return to the internal test management interface, select test manager, submit the Google account to be tested, and copy the address of “opt-in URL”.

    • Log in to the test account on the test machine, open the “opt-in URL” address in the browser, and join the internal test. You can download the test version of the App through Google Play App.

    • Once the download is complete, you can see a brand new OBB file under /Android/obb/App package name /.

Unzip and download

  • Unpack the

    After the app is installed for the first time, you need to decompress the OBB file and store the decompressed file in the folder defined by us (either data/data/ package name /files/ or customized project folder under the built-in storage). To decompress the obB file, the first step is to obtain the local path of the OBB file. The code is as follows:

    public static String getObbFilePath(Context context) {
        try {
            return Environment.getExternalStorageDirectory().getAbsolutePath()
                    + "/Android/obb/"
                    + context.getPackageName()
                    + File.separator
                    + "main."
                    + context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionCode
                    + "."
                    + context.getPackageName()
                    + ".obb";
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
            returnnull; }}Copy the code

    Now that you have the obB file path, you can decompress it:

    public static void unZipObb(Context context) {
       String obbFilePath = getObbFilePath(context);
       if (obbFilePath == null) {
           return;
       } else {
           File obbFile = new File(obbFilePath);
           if(! Obbfile.exists () {// Download obb files}else {
               File outputFolder = new File("yourOutputFilePath");
               if(! Outputfolder.exists ()) {// The directory is not created and outputFolder.mkdirs() has not been decompressed; unZip(obbFile, outputFolder.getAbsolutePath()); }else{// The directory has been createdif(outputFolder listFiles () = = null) {/ / decompression of the file to be deleted unZip (obbFile, outputFolder getAbsolutePath ()); }else{// Here you can add the file comparison logic}}}}}Copy the code

    APK Expansion Zip Library is available for developers to decompress OBB files. If you are interested, check out the following directory.

    <sdk>/extras/google/google_market_apk_expansion/zip_file/
    Copy the code

    I do not recommend using this library because it has been written for a few years, the SDK version compiled at that time is relatively low, and there are some compatibility bugs that require developers to modify the code before using it. So the upzip method used here is implemented using the most common ZipInputStream and FileOutputStream decompresses the zip package:

    // There is no logic to extract the password, Public static void unzip(File zipFile, String outPathString) throws IOException { FileUtils.createDirectoryIfNeeded(outPathString); ZipInputStreaminZip = new ZipInputStream(new FileInputStream(zipFile));
        ZipEntry zipEntry;
        String szName;
        while ((zipEntry = inZip.getNextEntry()) ! = null) { szName = zipEntry.getName();if (zipEntry.isDirectory()) {
                szName = szName.substring(0, szName.length() - 1);
                File folder = new File(outPathString + File.separator + szName);
                folder.mkdirs();
            } else {
                File file = new File(outPathString + File.separator + szName);
                FileUtils.createDirectoryIfNeeded(file.getParent());
                file.createNewFile();
                FileOutputStream out = new FileOutputStream(file);
                int len;
                byte[] buffer = new byte[1024];
                while ((len = inZip.read(buffer)) != -1) {
                    out.write(buffer, 0, len);
                    out.flush();
                }
                out.close();
            }
        }
        inZip.close();
    }
    
    public static String createDirectoryIfNeeded(String folderPath) {
        File folder = new File(folderPath);
        if(! folder.exists() || ! folder.isDirectory()) { folder.mkdirs(); }return folderPath;
    }
    Copy the code

    After decompression is complete, we can ask for the large capacity resources we need to access through the path of the output file, the file reading is not expanded here.

  • Download the obb

    If you download and install an App from Google Play, there is a certain probability that you will download an APK that does not contain the OBB file, or the OBB file may be artificially deleted. In this case, developers need to go to the download address provided by Google to download the corresponding OBB file. However, how to obtain the download address? The official Downloader Library is used here.

    The library is available for download through the Android Sdk Manager, Open Manager and click Google Play Licensing Library Package and Google Play APK Expansion Library Package to download. But when I was elated and ready to do a big job, I found that it actually compiled but [facepap]. This Library, like the APK Expansion Zip Library above, is largely unusable due to its age and disrepair. Toss about after some time, the devil changed a version, can finally use. A compiled JAR package, Google_apk_expand_helper, is provided. The specific code is as follows:

    // Random byte array, Private static final byte[] salt = new byte[]{18, 22, -31, -11, -54, 18, -101, -32, 43, 2, -8, -4, 9, 5, -106, private static final byte[] salt = new byte[]{18, 22, -31, -11, -54, 18, -101, -32, 43, 2, -8, -4, 9, 5, -106, -17, 33, 44, 3, 1}; private static final String TAG ="Obb";
    
    public static void getObbUrl(Context context, String publicKey) {
        final APKExpansionPolicy aep = new APKExpansionPolicy(
                context,
                new AESObfuscator(salt, 
                                  context.getPackageName(),
                                  Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID)
                ));
        aep.resetPolicy();
    
        final LicenseChecker checker = new LicenseChecker(context, aep, publicKey);
    
        checker.checkAccess(new LicenseCheckerCallback() {
            @Override
            public void allow(int reason) {
                Log.i(TAG, "allow:" + reason);
                if(ep.getExpansionurlCount () > 0) {String url = ep.getExpansionurl (0); } } @Override public void dontAllow(int reason) { Log.i(TAG,"dontAllow:" + reason);
            }
    
            @Override
            public void applicationError(int errorCode) {
                Log.i(TAG, "applicationError:"+ errorCode); }}); }Copy the code

    You need to provide the publicKey parameter in the above method. You can find the publicKey in GooglePlayConsole.

  • summary

    With this in mind, we have completed the main process of Apk subcontracting. Here are some examples of how to configure gradle files for multi-channel packaging and how to automatically compress large resource files into OBB at each packaging time.

Multi-channel and automation

  • example

    Let’s say we now need to distribute an installation package larger than 100M to GooglePlay and App Store. For GooglePlay, we need to generate apK and OBB files smaller than 100M, while for App Store, we only need to generate a full APK.

    The problem is that it is impossible to manually remove resource files and change the logic of resource references when you package GooglePlay, and then put them back when you package Apps. Doing so would greatly increase the developer’s workload and increase the likelihood of errors. Is there a way to package both the GooglePlay and app packages within a single project? The answer is yes, sourceSets in Build. gradle can solve this problem.

  • Isolate channel resources and resource reference code using sourceSets

    Suppose we have a splash. Mp4 file, in the app Store channel package, in the res/raw/ directory. In the googlePlay channel package, it’s placed in an obb file, and we can do that.

    First, create two new directories googlePlay and Tencent under the SRC directory, and create Java, RES, and assest folders under them.

    Add GooglePlay and app app channel information to your app level build.gradle file:

    android {
        flavorDimensions "default"
        productFlavors {
        
            GooglePlay { dimension "default" }
            Tencent { dimension "default"} /** Add <meta-data android:name= to androidmanifest.xml"Channel"
                           android:value="${CHANNEL_NAME}" />
            **/
        
            productFlavors.all { flavor ->
                flavor.manifestPlaceholders = [CHANNEL_NAME: name]
            }
    
        }
    }
    Copy the code

    Next add the sourceSets configuration to specify the resource and code addresses for the different channels, where main is the common resource and code, and the rest are the resources and codes for the corresponding channel package:

    sourceSets {
        main {
            java.srcDirs = ['src/main/java']
            assets.srcDirs = ['src/main/assets']
            res.srcDirs = ['src/main/res']
        }
        GooglePlay {
            java.srcDirs = ['src/googlePlay/java']
            res.srcDirs = ['src/googlePlay/res']
            assets.srcDirs = ['src/googlePlay/assets']
        }
        Tencent {
            java.srcDirs = ['src/tencent/java']
            res.srcDirs = ['src/tencent/res']
            assets.srcDirs = ['src/tencent/assets']}}Copy the code

    Put splash. Mp4 in Tencent /res/raw/ and create package name folder and resourcesHelper.java for Java folder of different channels. The directory structure is as follows:

    There are two caveats:

    First, you must create package name folders under the Java package, otherwise you will not be able to reference the classes under the project. In this case, the com.example.obbtest package.

    Second, you can choose the channel package type you want to compile through the Build Variants window in the lower left corner of AndroidStudio. When you select GooglePlay, you will find that the Java file under Tencent is invalid. So, if you need to modify the Java files under a channel, please switch to the specified channel first through Build Variants.

    Finally, different resource acquisition methods are adopted for different channels of Resourceshelper. Java:

    GooglePlay version:

    public class ResourcesHelper {
       public static void playSplashVideoResource(VideoView videoView){
           String filePath = ObbHelper.getCurrentObbFileFolder()+"raw/"+"splash.mp4"; videoView.setVideoPath(filePath); }}Copy the code

    Tencent version:

    public class ResourcesHelper {
        public static void playSplashVideoResource(VideoView videoView) {
            int resource = R.raw.splash;
            String uri = "android.resource://" + videoView.getContext().getApplicationContext().getPackageName() + "/"+ resource; videoView.setVideoURI(Uri.parse(uri)); }}Copy the code

    Isolating channel resources and resource reference code through sourceSets is done here, but for more complex scenarios, you need to extend and modify it as needed. Let’s take a look at how to automatically package resources into OBB files at build time.

  • Obb files are generated at build time

    To generate obB files at build time, you must add gradle scripts. Let’s start by creating a new script file, select.gradle, in the project directory.

    In order to package the obB file, you need to know which channel package is being built.

    def String getCurrentFlavor() {
        Gradle gradle = getGradle()
        String tskReqStr = gradle.getStartParameter().getTaskRequests().toString()
    
        Pattern pattern;
    
        if (tskReqStr.contains("assemble"))
            pattern = Pattern.compile("assemble(\\w+)(Release|Debug)")
        else
            pattern = Pattern.compile("generate(\\w+)(Release|Debug)")
    
        Matcher matcher = pattern.matcher(tskReqStr)
    
        if (matcher.find())
          return matcher.group(1).toLowerCase()
        else {
            println "NO MATCH FOUND"
            return ""}}Copy the code

    We know that obBS are zip files in nature, so we can generate OBBs by adding a method for compressed files into flavour. Gradle. Since I am not proficient in Groovy, I can use Java code to solve the problem, adding:

    import java.util.regex.Matcher import java.util.regex.Pattern import java.util.zip.ZipEntry import Java. Util. Zip. ZipOutputStream ext {zipObb = this. & zipObb getCurrentFlavor = this. & getCurrentFlavor} / / external compression method entry, Def static zipObb(File[] fs, String zipFilePath) {def static zipObb(File[] fs, String zipFilePath) {def static zipObb(File[] fs, String zipFilePath) {if (fs == null) {
            throw new NullPointerException("fs == null");
        }
        ZipOutputStream zos = null;
        try {
            zos = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(zipFilePath)));
            for (File file : fs) {
                if(file == null || ! file.exists()) {continue;
                }
                compress(file, zos, file.getName());
            }
            zos.flush();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if(zos ! = null){ try { zos.close(); } catch (IOException e) { e.printStackTrace(); }}}} // Def static compress(FilesourceFile, ZipOutputStream zos, String name) throws Exception {
        byte[] buf = new byte[2048];
        if (sourceFile.isfile ()) {// Add a zip entity to the zip output stream. Zos.putnextentry (new ZipEntry(name)) {// Add a zip entity to the zip output stream. // copy file to zip output stream int len; FileInputStream inputStream = new FileInputStream(sourceFile);
            while((len = inputStream.read(buf)) ! = -1) { zos.write(buf, 0, len); } // Complete the entry zos.closeEntry(); inputStream.close(); }else {
            File[] listFiles = sourceFile.listFiles();
            if(listFiles = = null | | listFiles. Length = = 0) {/ / need to keep the original file structure, the need to deal with an empty folder zos. PutNextEntry (new ZipEntry (name +"/")); // No file, no copy of file closeEntry(); }else {
                for (File file : listFiles) {
                    compress(file, zos, name + "/" + file.getName());
                }
            }
        }
    }
    def String getCurrentFlavor() {... }Copy the code

    Gradle we have added a method for getting the current channel and compressed file into our flavour. Now go back to our build.gradle file under app and compress all files that need to be compressed by checking whether the current channel is GooglePaly. Output to the same directory as the googlePlay channel package APk:

    apply from: ".. /flavour.gradle"Obb task zipObb(type: JavaExec) {// Check whether GooglePlay channel package is availableif (getCurrentFlavor().equals("googleplay")) {// Get debug or Release mode output to a different address String outputFilePathif(gradle.startParameter.taskNames.toString().contains("Debug")){
                outputFilePath = "app/build/outputs/apk/GooglePlay/debug/main." + android.defaultConfig.versionCode + ".com.example.testobb.obb"
            }else{
                outputFilePath = "app/GooglePlay/release/main." + android.defaultConfig.versionCode + ".com.example.testobb.obb"
            }
            File file = new File('app/src/tencent/res/raw/splash.mp4'File[] files = new File[]{File} zipObb(files, outputFilePath)}}Copy the code

    At this point, our multi-channel packaging and automated generation of OBBs are complete.

    If you find any mistakes or do not understand the place can leave a message.