Analysis of the

Some common operations and commands related to CodePush have been briefly introduced in CodePush Hot Update Common Commands and Precautions. When issuing an update package, we usually use the following command:

Code-push release <appName> <platform>./bundle-images folder path /-d1.1.1 - Production - t des"Update description"Copy the code

or

Code-push release-react <appName> <platform> -t 1.1.1-d Production --des "Update description" -m true(Forced update)Copy the code

The difference is that the second command automatically generates bundles and image resources.

Assume that the current hot update version is 1.1.0 and the content is as follows:

Resource folder / ├ ─ ─ drawable - mdpi │ ├ ─ ─ Amy polumbo ng │ └ ─ ─ p. ng └ ─ ─ index. The iOS. BundleCopy the code

The upcoming update is 1.1.1, which reads as follows:

Resource folder / ├ ─ ─ drawable - mdpi │ ├ ─ ─ Amy polumbo ng │ ├ ─ ─ p. ng │ └ ─ ─ c.p ng / / new pictures c.p ng └ ─ ─ index. The iOS. BundleCopy the code

If your App is currently in version 1.1.0 and you haven’t done any codePush updates before. When updating from 1.1.0 to 1.1.1, according to the characteristics of differentiated codePush update as we understand, after the App detects the update of 1.1.1, it will download the C. PNG pictures and jsbundle files with structure changes to the local, and load and render them at an appropriate time. But that’s not the truth!

1. When codePush system detects a new version and performs hot update loading for the first time, all resources will be downloaded locally. 2. If the user has previously upgraded from 1.0.0 to 1.1.0 and then to 1.1.1 through CodePush, CodePush will download the JSBundle after Diff and the newly added C. PNG image files.Copy the code

So the first hot update consumes a little bit of bandwidth.

RN image loading process

The main cause of this phenomenon also from RN framework about loading the images, let’s look at an RN Image loading process of the core code, open the node_modules/react – native/Libraries/Image/AssetSourceResolver js:

 /** * Whether the jsBundle file is loaded from the Packager service
  isLoadedFromServer(): boolean {
    return!!!!!this.serverUrl;
  }
 
  /** * whether the jsBundle file is loaded from the local directory */
  isLoadedFromFileSystem(): boolean {
    return!!!!! (this.jsbundleUrl && this.jsbundleUrl.startsWith('file://'));
  }
 
  /** * image loading */
  defaultAsset(): ResolvedAssetSource {
    if (this.isLoadedFromServer()) {
      return this.assetServerURL();
    }
 
    if (Platform.OS === 'android') {
      return this.isLoadedFromFileSystem()
        ? this.drawableFolderInBundle()
        : this.resourceIdentifierWithoutScale();
    } else {
      return this.scaledAssetURLNearBundle(); }}Copy the code

In the defaultAsset() method, you first determine if the JSBundle file is loaded from the Packager service (debug mode), and if it is loaded directly from the local service. The drawableFolderInBundle() method is used to determine whether the bundle is loaded from the phone’s local directory. Vice call resourceIdentifierWithoutScale () method. (2) Call scaledAssetURLNearBundle() directly on iOS. We first Android platform drawableFolderInBundle (), resourceIdentifierWithoutScale () two methods

 / * * * if jsbundle load to run from the local file directory, will parse position relative to its assets * e.g. 'file:///sdcard/AwesomeModule/drawable-mdpi/icon.png' * /
  drawableFolderInBundle(): ResolvedAssetSource {
    const path = this.jsbundleUrl || 'file://';
    return this.fromSource(path + getAssetPathInDrawableFolder(this.asset));
  }
 
  /** * the default location of the assets bundled with the application, located by resource identifier * Android resource system selects the correct scale * E.g. 'assets_awesomemodule_icon' */
  resourceIdentifierWithoutScale(): ResolvedAssetSource {
    invariant(
      Platform.OS === 'android'.'resource identifiers work on Android',);return this.fromSource(
      assetPathUtils.getAndroidResourceIdentifier(this.asset),
    );
  }Copy the code

From the source, we is not hard to find: (1) if the JSBundle File from the local File directory (File) load, such as (/ sdcard/com. Songlcy. Myapp /…). An image is loaded from a drawable- XXX directory relative to an assets directory if it is not loaded from the assets directory.

Assuming that the current load JSBundle file path is/sdcard/com songlcy. Myapp/code - push/index. The iOS. JSBundle, From/sdcard/com. Songlcy. Myapp/code - push/directory search image.Copy the code

(2) If the JSBundle is loaded from assets, the image will be loaded from drawable- XXX in apK.

The iOS platform doesn’t make any distinction and calls the scaledAssetURLNearBundle() method:

/** * Find assets directory * LLDB directly from JSBundle.'file:///sdcard/bundle/assets/AwesomeModule/[email protected]'
   */
  scaledAssetURLNearBundle(): ResolvedAssetSource {
    const path = this.jsbundleUrl || 'file://';
    return this.fromSource(path + getScaledAssetPath(this.asset));
  }Copy the code

After analyzing the entire loading process of images, let’s return to codePush update. As we all know, when the current APP detects an update, CodePush will download the JSBundle and image resources on the server to the local directory of the mobile phone, so the JSBundle file is loaded from the file directory of the mobile phone system. According to the RN image loading process, Updated image resources also need to be placed in the JSBundle directory. Therefore, when codePush is updated for the first time, all resources need to be downloaded; otherwise, an error will occur that resources cannot be found and loading fails. It is also done for the convenience of unified management. Codepush will make a diff-patch at the second update to achieve differentiated incremental update through comparison.

Optimization scheme

Given the current update traffic overhead of codePush’s first update, how can we optimize the package size of the first update to make it a differentiated incremental update as well? Through the above analysis, we can modify the RN image loading process by combining assets with the local directory. After the update, we can determine whether the resources before the update exist in the local directory where the current JSBundle resides. If so, we can directly load the resources; if not, we can load them from the drawable- XXX directory in the APK package. At this point, we don’t need to upload all the image resources, just upload the updated resources. To modify RN image loading process, we can directly modify in the source code, or we can use hook to modify, to ensure that the coupling degree between the project and node_modules is reduced, the core code is as follows:

import { NativeModules } from 'react-native';    
import AssetSourceResolver from "react-native/Libraries/Image/AssetSourceResolver";
import _ from 'lodash';
 
let iOSRelateMainBundlePath = ' ';
let _sourceCodeScriptURL = ' ';
 
// Get the default path of jsBundle in ios
const defaultMainBundePath = AssetsLoad.DefaultMainBundlePath;
 
function getSourceCodeScriptURL() {
    if (_sourceCodeScriptURL) {
        return _sourceCodeScriptURL;
    }
    // Call Native Module to get the JSbundle path
    RN allows developers to customize the JS loading path on the Native side by calling SourceCode. ScriptURL on the JS side
    // If the developer does not specify the path of the JSbundle, the asset directory is returned offline
    let sourceCode =
        global.nativeExtensions && global.nativeExtensions.SourceCode;
    if(! sourceCode) { sourceCode = NativeModules && NativeModules.SourceCode; } _sourceCodeScriptURL = sourceCode.scriptURL;return _sourceCodeScriptURL;
}
 
// Get all drawable image resource paths in the bundle directory
let drawablePathInfos = [];
AssetsLoad.searchDrawableFile(getSourceCodeScriptURL(),
     (retArray)=>{
      drawablePathInfos = drawablePathInfos.concat(retArray);
});
// use the hook defaultAsset method to customize the image loading method
AssetSourceResolver.prototype.defaultAsset = _.wrap(AssetSourceResolver.prototype.defaultAsset, function (func, ... args) {
     if (this.isLoadedFromServer()) {
         return this.assetServerURL();
     }
     if (Platform.OS === 'android') {
         if(this.isLoadedFromFileSystem()) {
             // Get the image resource path
             let resolvedAssetSource = this.drawableFolderInBundle();
             let resPath = resolvedAssetSource.uri;
             // Get all drawable file paths in the JSBundle directory and check whether the current image path exists
             // If so, return directly
             if(drawablePathInfos.includes(resPath)) {
                 return resolvedAssetSource;
             }
             // Check whether the image resource has a local file directory
             let isFileExist = AssetsLoad.isFileExist(resPath);
             // There are direct returns
             if(isFileExist) {
                 return resolvedAssetSource;
             } else {
                 // The drawable directory in the apK package is loaded according to the resource Id
                 return this.resourceIdentifierWithoutScale(); }}else {
             // The drawable directory in the APK package is loaded according to the resource Id
             return this.resourceIdentifierWithoutScale(); }}else {
         let iOSAsset = this.scaledAssetURLNearBundle();
         let isFileExist =  AssetsLoad.isFileExist(iOSAsset.uri);
         isFileExist = false;
         if(isFileExist) {
             return iOSAsset;
         } else {
             let oriJsBundleUrl = 'file://'+ defaultMainBundePath +'/' + iOSRelateMainBundlePath;
             iOSAsset.uri = iOSAsset.uri.replace(this.jsbundleUrl, oriJsBundleUrl);
             returniOSAsset; }}});Copy the code

The implementation logic is simple:

(1) Redefine defaultAsset() by hook.

(2) If the JSBundle file is loaded from the mobile system file directory:

1. Obtain the current image resource file path and check whether the current JSBundle directory exists. If so, the current resource is returned directly.

2. Check whether the image resource exists in the local file directory of the mobile phone. If yes, return to the current resource. If not, the image resource is loaded from the APK package based on the resource Id.

(3) Instead of loading the JSBundle file from the mobile system file directory, load the image resource directly from the APK package according to the resource Id.

After the above process, differentiated incremental update of resources can be realized when CodePush is updated for the first time. For details, see React-native code-push-assets