CodePush支持多包——加載不一樣業務的bundle包

背景

前期因爲對CodePush的預研不足,覺得支持多包熱更新,結果在實際應用中發現CodePush拿的熱更新bundle資源是沒有區分業務的,致使切換業務場景的時候出現白屏現象,因此須要對CodePush源碼進行重構。java

fork了github.com/microsoft/r…,並提交了重構後的代碼:github.com/hsl5430/rea…node

申明

  • 下文均以[reactProject]標識react native項目根目錄
  • react native簡稱rn
  • react native版本:0.59.5
  • CodePush版本:5.6.1

在講CodePush重構前須要先講講幾個重點:react

關於react.gradle

路徑:[reactProject]/node_modules/react-native/react.gradleandroid

在[reactProject]/android/app/build.gradle中,依賴了react.gradleios

apply from: "../../node_modules/react-native/react.gradle"
複製代碼

這個gradle腳本主要作了些什麼呢?其實就是建立了2個task,在app構建過程當中生成bundle文件和相關的資源文件(圖片等)。git

bundle${targetName}JsAndAssets

def currentBundleTask = tasks.create(
    name: "bundle${targetName}JsAndAssets",
    type: Exec) {
    group = "react"
    description = "bundle JS and assets for ${targetName}."
    ...
}
複製代碼

調用node腳本github

commandLine(*nodeExecutableAndArgs, cliPath, bundleCommand, "--platform", "android", "--dev", "${devEnabled}",
                    "--reset-cache", "--entry-file", entryFile, "--bundle-output", jsBundleFile, "--assets-dest", resourcesDir, *extraArgs)
複製代碼

生成bundle文件到json

file("$buildDir/generated/assets/react/${targetPath}")
複製代碼

生成資源文件到react-native

file("$buildDir/generated/res/react/${targetPath}")
複製代碼

copy${targetName}BundledJs

def currentAssetsCopyTask = tasks.create(
    name: "copy${targetName}BundledJs",
    type: Copy) {
    group = "react"
    description = "copy bundled JS into ${targetName}."
    ...
}
複製代碼

拷貝bundle文件到$buildDir/intermediates/assets/${targetPath}$buildDir/intermediates/merged_assets/${variant.name}/merge${targetName}Assets/out設計模式

Android項目在構建過程當中mergeAssets的時候,會將該目錄下的bundle文件打包到assets目錄下。

因此,若是你是直接經過node腳本生成bundle包,或者由別的開發人員將生成好的bundle包給你,並放在[reactProject]/android/app/src/main/assets下,其實能夠不用依賴react.gradle,就不用每次在build過程當中起跑上述這兩個task

關於codepush.gradle

路徑:[reactProject]/node_modules/react-native-code-push/android/codepush.gradle

在[reactProject]/android/app/build.gradle中,依賴了codepush.gradle

apply from: "../../node_modules/react-native-code-push/android/codepush.gradle"
複製代碼

這個gradle腳本主要作了些什麼呢?

記錄apk文件的構建時間

gradle.projectsEvaluated {
    android.buildTypes.each {
        // to prevent incorrect long value restoration from strings.xml we need to wrap it with double quotes
        // https://github.com/Microsoft/cordova-plugin-code-push/issues/264
        it.resValue 'string', "CODE_PUSH_APK_BUILD_TIME", String.format("\"%d\"", System.currentTimeMillis())
    }
    ...
}
複製代碼

起初我在接入CodePush的時候,沒有依賴codepush.gradle,也不報錯,結果跑起來後,直接崩潰了,跟蹤下源碼發現CodePush在下載更新包downloadUpdate的時候,調用了getBinaryResourcesModifiedTime

long getBinaryResourcesModifiedTime() {
    try {
        String packageName = this.mContext.getPackageName();
        int codePushApkBuildTimeId = this.mContext.getResources().getIdentifier(CodePushConstants.CODE_PUSH_APK_BUILD_TIME_KEY, "string", packageName);
        // replace double quotes needed for correct restoration of long value from strings.xml
        // https://github.com/Microsoft/cordova-plugin-code-push/issues/264
        String codePushApkBuildTime = this.mContext.getResources().getString(codePushApkBuildTimeId).replaceAll("\"", "");
        return Long.parseLong(codePushApkBuildTime);
    } catch (Exception e) {
        throw new CodePushUnknownException("Error in getting binary resources modified time", e);
    }
}
複製代碼

很顯然,拿不到CODE_PUSH_APK_BUILD_TIME就直接崩了。

生成CodePushHash

def generateBundledResourcesHash = tasks.create(
        name: "generateBundledResourcesHash${targetName}",
        type: Exec) {
    commandLine (*nodeExecutableAndArgs, "${nodeModulesPath}/react-native-code-push/scripts/generateBundledResourcesHash.js", resourcesDir, jsBundleFile, jsBundleDir)
    enabled config."bundleIn${targetName}" ||
    config."bundleIn${variant.buildType.name.capitalize()}" ?:
    targetName.toLowerCase().contains("release")
}
複製代碼

調用node腳本generateBundledResourcesHash.js有興趣的童鞋能夠深挖hash是怎麼生成的),傳入bundle包和對應的圖片等資源文件,生成對應的hash,並以文件的形式存儲這個hash,存儲在[reactProject]/android/app/build/generated/assets/react/debug/CodePushHash

[reactProject]/android/app/build/generated/assets/react/debug
 ├── CodePushHash
 └── index.android.bundle
複製代碼

同上,起初接入CodePush的時候,沒有依賴codepush.gradle,也不報錯,結果跑起來後,logcat下打印了一條日誌

Unable to get the hash of the binary's bundled resources - "codepush.gradle" may have not been added to the build definition.
複製代碼

定位到CodePush源碼

public static String getHashForBinaryContents(Context context, boolean isDebugMode) {
    try {
        return CodePushUtils.getStringFromInputStream(context.getAssets().open(CodePushConstants.CODE_PUSH_HASH_FILE_NAME));
    } catch (IOException e) {
        try {
            return CodePushUtils.getStringFromInputStream(context.getAssets().open(CodePushConstants.CODE_PUSH_OLD_HASH_FILE_NAME));
        } catch (IOException ex) {
            if (!isDebugMode) {
                // Only print this message in "Release" mode. In "Debug", we may not have the
                // hash if the build skips bundling the files.
                CodePushUtils.log("Unable to get the hash of the binary's bundled resources - \"codepush.gradle\" may have not been added to the build definition.");
            }
        }
        return null;
    }
}
複製代碼

這個是react獲取bundle包資源信息的一部分,具體react層拿到這個hash作什麼,我沒有去深挖。

總之,既然接入CodePush,必然要按照文檔要求,依賴codepush.gradle,保證能正常記錄apk文件的構建時間和生成CodePushHash。

關於目錄結構

內置bundle包和圖片資源的路徑

src
 └─ main
     ├─ assets
     │   └─ index.android.bundle
     └─ res
         ├─ drawable-mdpi
         │   ├─ ic_back.png
         │   └─ ...
         ├─ drawable-xhdpi
         │   ├─ ic_back.png
         │   └─ ...
         └─ ...
複製代碼

CodePush熱更新bundle包和圖片資源的路徑

/data/data/[應用包名]/files
 └─ CodePush
     ├─ codepush.json // 記錄當前熱更新資源包的一些已基本信息
     ├─ [hash]
     │   ├─ app.json // 記錄本hash對應熱更新資源包的一些已基本信息
     │   └─ [version] // 這裏的version是CodePush定義的熱更新版本號,非APP版本號
     │       ├─ index.android.bundle
     │       ├─ drawable-mdpi
     │       │   ├─ ic_back.png
     │       │   └─ ...
     │       ├─ drawable-xhdpi
     │       │   ├─ ic_back.png
     │       │   └─ ...
     │       └─ ...
     ├─ [hash2]
     └─ ...
複製代碼

CodePush.getJSBundleFile("index.android.bundle")在讀取名稱爲index.android.bundle的bundle包時,會先拿熱更新bundle包,若是沒有,便取內置bundle包。具體可查看方法CodePush.getJSBundleFileInternal(String assetsBundleFileName)


爲什麼說CodePush不支持多業務多包

  • react.gradle、codepush.gradle的實現都是針對單個bundle包的
  • assets目錄下看似能夠放多個不一樣名稱的bundle包,但生成的CodePushHash文件是跟bundle包放在同一目錄下的,即放在assets目錄下,文件名稱定死了,就是"CodePushHash",無法作到一個bundle文件對應一個CodePushHash文件
  • 熱更新,CodePush目錄下以hash值做爲文件夾區分不一樣的熱更新包,但由於沒有區分業務,這就意味着找熱更新包的時候找錯。咱們在開發中就踩坑了,A頁面(AActivity)加載內置在assets下的a.android.bundle,同時下載了熱更新(支持強制更新和下次進入頁面應用更新),再訪問B頁面(BActivity)加載內置在assets下的b.android.bundle,同時下載了熱更新,再訪問A頁面,結果加載到B頁面的熱更新,頁面白屏了。簡而言之,就是應用了最後一次下載的熱更新資源。
String jsBundleFile = CodePush.getJSBundleFile("a.android.bundle");
Log.d(「JSBundleFile」, jsBundleFile);
// 好比這裏Log出來的值:/data/data/com.react.demo/files/CodePush/4247860b1dc848a13e6c980ac9bee9323de4210951ea917bc68f7346575370e2/1.0.0/b.android.bundle

複製代碼

因此,要支持多業務多包,必需要將各業務的react native環境和codepush環境隔離,保證互不影響。


一步一步實現CodePush分包

先上一個思惟導圖

CodePush分包

1、工程目錄管理

內置bundle包和圖片資源分目錄存放

  1. 新建一個Module --> Android Library --> rnLib來管理rn相關的代碼
  2. rnLib下新建一個目錄react來存放各個業務的bundle包和圖片
  3. 在react目錄下以業務的bundle文件名稱做爲目錄名稱新建子目錄,如:a.android.bundle
  4. 業務a.android.bundle下存放bundle文件和圖片資源,其餘業務亦同
rnLib
└─ react
    ├─ a.android.bundle(目錄)
    │   ├─ a.android.bundle(文件)
    │   ├─ drawable-mdpi
    │   │   ├─ a_ic_back.png // 圖片的命名建議區分業務,不一樣業務不要出現名稱相同的圖片,
    │   │   └─ ...           // 不然apk打包的時候,會報資源合併衝突的錯誤
    │   └─ ...
    └─ b.android.bundle(目錄)
        ├─ b.android.bundle(文件)
        ├─ drawable-mdpi
        │   ├─ b_ic_back.png
        │   └─ ...
        └─ ...

複製代碼

這麼作,主要是爲了方便管理這些rn資源

2、編譯打包處理

基於上述目錄結構,重構react.gradlecodepush.gradle腳本

react.gradle重構,支持多包

上文在rnLib下建的react目錄,它不像assets、res等預置目錄目錄,能被Android工程所識別,在打包過程當中打到apk文件中,因此這裏利用react.gradle,拷貝react目錄下的文件到build目錄下特定的目錄,能被Android工程所識別。重構後的代碼:

/* * 重寫/node_modules/react-native/react.gradle的實現邏輯: * 支持多業務分包方案, /react目錄下以業務bundle名稱做爲文件夾名稱來存放各個業務的bundle文件合關聯的資源文件 * */
def config = project.hasProperty("react") ? project.react : []

def reactRoot = file(config.root ?: "../../")
def inputExcludes = config.inputExcludes ?: ["android/**", "ios/**"]

// React js bundle directories
def jsBundleAndResDir = file("$rootDir/rnLib/react")
def generatedDir = "$buildDir/generated"

afterEvaluate {

    def isAndroidLibrary = plugins.hasPlugin("com.android.library")
    def variants = isAndroidLibrary ? android.libraryVariants : android.applicationVariants
    variants.all { def variant ->
        // Create variant and target names
        def targetName = variant.name.capitalize()
        def targetPath = variant.dirName

        def jsBundleRootDir = file("$generatedDir/assets/react/${targetPath}")

        // 遍歷jsBundleAndResDir下的各個業務
        ArrayList<File> sources = new ArrayList<>()
        ArrayList<File> jsBundleDirs = new ArrayList<>()
        ArrayList<File> resourcesDirs = new ArrayList<>()
        files(jsBundleAndResDir.listFiles(new FilenameFilter() {
            @Override
            boolean accept(File dir, String name) {
                //自定義過濾規則
                return name.endsWith(".android.bundle")
            }
        }
        )).each { source ->
            // 業務目錄
            if (!source.exists() || !source.directory) {
                return
            }
            sources.add(source)

            def jsBundleRelativeDir = "assets/react/${targetPath}/${source.name}"
            def resourcesRelativeDir = "res/react/${targetPath}/${source.name}"

            def jsBundleDir = file("${generatedDir}/${jsBundleRelativeDir}")
            def resourcesDir = file("${generatedDir}/${resourcesRelativeDir}")

            jsBundleDirs.add(jsBundleDir)
            resourcesDirs.add(resourcesDir)
        }

        if (sources.isEmpty()) {
            return
        }

        // 跟react-native/react.gradle的實現有所區別
        // 這裏是巧用"bundle${targetName}JsAndAssets"來作JsAndAssets的copy
        def currentBundleTask = tasks.create(
                name: "bundle${targetName}JsAndAssets",
                type: Copy) {
            group = "react"
            description = "bundle JS and assets for ${targetName}."

            // Set up inputs and outputs so gradle can cache the result
            inputs.files fileTree(dir: reactRoot, excludes: inputExcludes) outputs.dir(jsBundleRootDir) // 遍歷jsBundleAndResDir下的各個業務 files(sources.toArray()).each { source ->
                // 業務目錄
                def jsBundleRelativeDir = "assets/react/${targetPath}/${source.name}"
                def resourcesRelativeDir = "res/react/${targetPath}/${source.name}"

                def jsBundleDir = file("${generatedDir}/${jsBundleRelativeDir}")
                def resourcesDir = file("${generatedDir}/${resourcesRelativeDir}")

                // Create dirs if they are not there (e.g. the "clean" task just ran)
                jsBundleDir.deleteDir()
                jsBundleDir.mkdirs()
                resourcesDir.deleteDir()
                resourcesDir.mkdirs()

                // Set up outputs so gradle can cache the result
                //outputs.dir(jsBundleDir)
                outputs.dir(resourcesDir)

                into(generatedDir)
                // 將react/[bundle name]下的JsBundle copy 到指定目錄
                into(jsBundleRelativeDir) {
                    from(source)
                    include '*.bundle'
                }
                // 將react/[bundle name]下的drawable copy 到指定目錄
                into(resourcesRelativeDir) {
                    from(source)
                    include 'drawable*/*'
                }
            }

            enabled config."bundleIn${targetName}" ||
                    config."bundleIn${variant.buildType.name.capitalize()}" ?:
                    targetName.toLowerCase().contains("release")
        }

        // Expose a minimal interface on the application variant and the task itself:
        variant.ext.bundleJsAndAssets = currentBundleTask
        currentBundleTask.ext.generatedResFolders = files(resourcesDirs.toArray()).builtBy(currentBundleTask)
        currentBundleTask.ext.generatedAssetsFolders = files(jsBundleDirs.toArray()).builtBy(currentBundleTask)

        // registerGeneratedResFolders for Android plugin 3.x
        if (variant.respondsTo("registerGeneratedResFolders")) {
            variant.registerGeneratedResFolders(currentBundleTask.generatedResFolders)
        } else {
            variant.registerResGeneratingTask(currentBundleTask)
        }
        variant.mergeResourcesProvider.get().dependsOn(currentBundleTask)

        // packageApplication for Android plugin 3.x
        def packageTask = variant.hasProperty("packageApplication")
                ? variant.packageApplicationProvider.get()
                : tasks.findByName("package${targetName}")
        if (variant.hasProperty("packageLibrary")) {
            packageTask = variant.packageLibrary
        }

        // pre bundle build task for Android plugin 3.2+
        def buildPreBundleTask = tasks.findByName("build${targetName}PreBundle")

        def currentAssetsCopyTask = tasks.create(
                name: "copy${targetName}BundledJs",
                type: Copy) {
            group = "react"
            description = "copy bundled JS into ${targetName}."

            into("$buildDir/intermediates")
            into("assets/${targetPath}") {
                from(jsBundleRootDir)
            }

            // Workaround for Android Gradle Plugin 3.2+ new asset directory
            into("merged_assets/${variant.name}/merge${targetName}Assets/out") {
                from(jsBundleRootDir)
            }

            // mergeAssets must run first, as it clears the intermediates directory
            dependsOn(variant.mergeAssetsProvider.get())

            enabled(currentBundleTask.enabled)
        }

        packageTask.dependsOn(currentAssetsCopyTask)
        if (buildPreBundleTask != null) {
            buildPreBundleTask.dependsOn(currentAssetsCopyTask)
        }
    }
}
複製代碼

codepush.gradle重構,支持多包

  1. 拿到task"bundle${targetName}JsAndAssets"拷貝文件後的bundle目錄和圖片目錄
  2. 遍歷上述目錄,調用node腳本generateBundledResourcesHash.js生成CodePushHash

重構後的代碼:

/* * Adapted from https://raw.githubusercontent.com/facebook/react-native/d16ff3bd8b92fa84a9007bf5ebedd8153e4c089d/react.gradle * * 重寫/node_modules/react-native-code-push/android/codepush.gradle的實現邏輯: * 支持多業務分包方案, 不一樣的業務生成各自須要的CodePushHash * */
import java.nio.file.Paths

def config = project.hasProperty("react") ? project.react : []

void runBefore(String dependentTaskName, Task task) {
    Task dependentTask = tasks.findByPath(dependentTaskName)
    if (dependentTask != null) {
        dependentTask.dependsOn task
    }
}

gradle.projectsEvaluated {
    android.buildTypes.each {
        // to prevent incorrect long value restoration from strings.xml we need to wrap it with double quotes
        // https://github.com/Microsoft/cordova-plugin-code-push/issues/264
        it.resValue 'string', "CODE_PUSH_APK_BUILD_TIME", String.format("\"%d\"", System.currentTimeMillis())
    }

    android.applicationVariants.all { variant ->
        if (!variant.hasProperty("bundleJsAndAssets")) {
            return
        }

        Task reactBundleTask = variant.bundleJsAndAssets if (!reactBundleTask.hasProperty("generatedAssetsFolders")) {
            return
        }

        def jsBundleDirs = reactBundleTask.generatedAssetsFolders
        def resourcesDirs = reactBundleTask.generatedResFolders if (jsBundleDirs.isEmpty()) {
            return
        }

        def nodeModulesPath if (config.root) {
            nodeModulesPath = Paths.get(config.root, "/node_modules")
        } else if (project.hasProperty('nodeModulesPath')) {
            nodeModulesPath = project.nodeModulesPath
        } else {
            nodeModulesPath = "../../node_modules"
        }
        // Additional node commandline arguments
        def nodeExecutableAndArgs = config.nodeExecutableAndArgs ?: ["node"]

        def targetName = variant.name.capitalize()

        for (int i = 0; i < jsBundleDirs.size(); i++) {
            File jsBundleDir = jsBundleDirs[i]
            File resourcesDir = resourcesDirs[i]
            // jsBundleFile的name正好是目錄的name
            File jsBundleFile = file("${jsBundleDir}/${jsBundleDir.name}")

            def indexOf = jsBundleFile.name.indexOf('.')
            def taskSuffix = jsBundleFile.name.substring(0, 1).toUpperCase() + jsBundleFile.name.substring(1, indexOf)

            // Make this task run right after the bundle task
            def generateBundledResourcesHash = tasks.create(
                    name: "generateBundledResourcesHash${targetName}${taskSuffix}",
                    type: Exec) {
                group = "react"
                description = "generate CodePushHash for ${jsBundleFile.name}."
                commandLine(*nodeExecutableAndArgs, "${nodeModulesPath}/react-native-code-push/scripts/generateBundledResourcesHash.js", resourcesDir.absolutePath, jsBundleFile, jsBundleDir.absolutePath)
                enabled(reactBundleTask.enabled)
            }

            generateBundledResourcesHash.dependsOn(reactBundleTask)
            runBefore("processArmeabi-v7a${targetName}Resources", generateBundledResourcesHash)
            runBefore("processX86${targetName}Resources", generateBundledResourcesHash)
            runBefore("processUniversal${targetName}Resources", generateBundledResourcesHash)
            runBefore("process${targetName}Resources", generateBundledResourcesHash)
        }
    }
}
複製代碼

生成的apk包,解壓查看資源目錄結構

apk
├─ assets
│   ├─ a.android.bundle(目錄)
│   │   ├─ a.android.bundle(文件)
│   │   ├─ CodePushHash
│   │   └─ ...
│   └─ b.android.bundle(目錄)
│       ├─ b.android.bundle(文件)
│       ├─ CodePushHash
│       └─ ...
└─ res
    ├─ drawable-mdpi-v4
    │   ├─ a_ic_back.png
    │   ├─ b_ic_back.png
    │   └─ ...
    ├─ drawable-xhdpi-v4
    │   ├─ a_ic_back.png
    │   ├─ b_ic_back.png
    │   └─ ...
    └─ ...

複製代碼

3、重構Java層代碼

重構gradle腳本後,根據apk中的資源目錄結構,相應的,Java層代碼也要相應調整

修改getJSBundleFile的傳參

CodePush.getJSBundleFile(String assetsBundleFileName)內部調用了CodePush.getJSBundleFileInternal(String assetsBundleFileName),這裏重構的點就是將assetsBundleFileName改成assetsBundleFilePath,不要寫死傳文件名,應當支持傳assets文件路徑

修改前

public class CodePush implements ReactPackage {

    private String mAssetsBundleFileName;
    ...
    
    public String getAssetsBundleFileName() {
        return mAssetsBundleFileName;
    }

    public String getJSBundleFileInternal(String assetsBundleFileName) {
        this.mAssetsBundleFileName = assetsBundleFileName;
        String binaryJsBundleUrl = CodePushConstants.ASSETS_BUNDLE_PREFIX + assetsBundleFileName;

        String packageFilePath = null;
        try {
            packageFilePath = mUpdateManager.getCurrentPackageBundlePath(this.mAssetsBundleFileName);
        } catch (CodePushMalformedDataException e) {
            // We need to recover the app in case 'codepush.json' is corrupted
            CodePushUtils.log(e.getMessage());
            clearUpdates();
        }
        ...
    }
}
複製代碼

修改後

public class CodePush implements ReactPackage {

    private String mAssetsBundleFileName;
    private String mAssetsBundleFilePath;
    ...
    
    public String getAssetsBundleFileName() {
        return mAssetsBundleFileName;
    }

    public String getAssetsBundleFilePath() {
        return mAssetsBundleFilePath;
    }

    public String getAssetsBundleFileDir() {
        try {
            return new File(getAssetsBundleFilePath()).getParent();
        } catch (Exception e) {
            return null;
        }
    }
    
    public String getJSBundleFileInternal(String assetsBundleFileName) {
        // 支持assets文件路徑
        this.mAssetsBundleFilePath = assetsBundleFileName;
        File file = new File(assetsBundleFileName);
        mAssetsBundleFileName = file.getName();
        
        String binaryJsBundleUrl = CodePushConstants.ASSETS_BUNDLE_PREFIX + assetsBundleFileName;

        String packageFilePath = null;
        try {
            packageFilePath = mUpdateManager.getCurrentPackageBundlePath(this.mAssetsBundleFileName);
        } catch (CodePushMalformedDataException e) {
            // We need to recover the app in case 'codepush.json' is corrupted
            CodePushUtils.log(e.getMessage());
            clearUpdates();
        }
        ...
    }
}
複製代碼

同時,要檢查getJSBundleFileInternal的調用,發現CodePushNativeModule.loadBundle()中調用了

public class CodePushNativeModule extends ReactContextBaseJavaModule {
    ...
    private void loadBundle() {
        ...
        String latestJSBundleFile = mCodePush.getJSBundleFileInternal(mCodePush.getAssetsBundleFileName());
        ... 
    }
}
複製代碼

修改後

public class CodePushNativeModule extends ReactContextBaseJavaModule {
    ...
    private void loadBundle() {
        ...
        String latestJSBundleFile = mCodePush.getJSBundleFileInternal(mCodePush.getAssetsBundleFilePath());
        ... 
    }
}
複製代碼

相應地,上層就能夠這樣調用了:

CodePush.getJSBundleFile("a.android.bundle/a.android.bundle");
複製代碼

修改獲取CodePushHash的邏輯代碼

修改前

public class CodePushUpdateUtils {

    public static String getHashForBinaryContents(Context context, boolean isDebugMode) {
        try {
            return CodePushUtils.getStringFromInputStream(context.getAssets().open(CodePushConstants.CODE_PUSH_HASH_FILE_NAME));
        } catch (IOException e) {
            try {
                return CodePushUtils.getStringFromInputStream(context.getAssets().open(CodePushConstants.CODE_PUSH_OLD_HASH_FILE_NAME));
            } catch (IOException ex) {
                if (!isDebugMode) {
                    // Only print this message in "Release" mode. In "Debug", we may not have the
                    // hash if the build skips bundling the files.
                    CodePushUtils.log("Unable to get the hash of the binary's bundled resources - \"codepush.gradle\" may have not been added to the build definition.");
                }
            }
            return null;
        }
    }
}
複製代碼

因爲這個方法只有在CodePushNativeModule中調用,因此我直接在CodePushNativeModule中增長方法getHashForBinaryContents

public class CodePushNativeModule extends ReactContextBaseJavaModule {
    
    private String mBinaryContentsHash = null;
    private CodePush mCodePush;
    ...

    public CodePushNativeModule(ReactApplicationContext reactContext, CodePush codePush, CodePushUpdateManager codePushUpdateManager, CodePushTelemetryManager codePushTelemetryManager, SettingsManager settingsManager) {
        super(reactContext);
        mCodePush = codePush;
        ...
        mBinaryContentsHash = getHashForBinaryContents(codePush);
       ...
    }

    /** * 重寫{@link CodePushUpdateUtils#getHashForBinaryContents(Context, boolean)}的實現, 分業務目錄讀取{@link CodePushConstants#CODE_PUSH_HASH_FILE_NAME} */
    public String getHashForBinaryContents(CodePush codePush) {
        // return CodePushUpdateUtils.getHashForBinaryContents(getReactApplicationContext(), codePush.isDebugMode());
        Context context = codePush.getContext();
        String assetsBundleDir = codePush.getAssetsBundleFileDir();
        String codePushHashFilePath;
        try {
            codePushHashFilePath = new File(assetsBundleDir, CodePushConstants.CODE_PUSH_HASH_FILE_NAME).getPath();
            return CodePushUtils.getStringFromInputStream(context.getAssets().open(codePushHashFilePath));
        } catch (IOException e) {
            try {
                codePushHashFilePath = new File(assetsBundleDir, CodePushConstants.CODE_PUSH_OLD_HASH_FILE_NAME).getPath();
                return CodePushUtils.getStringFromInputStream(context.getAssets().open(codePushHashFilePath));
            } catch (IOException ex) {
                if (!codePush.isDebugMode()) {
                    // Only print this message in "Release" mode. In "Debug", we may not have the
                    // hash if the build skips bundling the files.
                    CodePushUtils.log("Unable to get the hash of the binary's bundled resources - \"codepush.gradle\" may have not been added to the build definition.");
                }
            }
            return null;
        }
    }
}
複製代碼

修改獲取CodePushPath的邏輯代碼

CodePushUpdateManager.getCodePushPath,這個很是重要!CodePush內部有多出引用。就是指定了熱更新包讀寫的根目錄,因此爲了區分業務,就應該在本來的基礎上,增長子目錄,來區分業務

修改前

public class CodePushUpdateManager {

    private String mDocumentsDirectory;

    public CodePushUpdateManager(String documentsDirectory) {
        mDocumentsDirectory = documentsDirectory;
    }

    ...

    private String getCodePushPath() {
        String codePushPath = CodePushUtils.appendPathComponent(getDocumentsDirectory(), CodePushConstants.CODE_PUSH_FOLDER_PREFIX);
        if (CodePush.isUsingTestConfiguration()) {
            codePushPath = CodePushUtils.appendPathComponent(codePushPath, "TestPackages");
        }
        return codePushPath;
    }
}
複製代碼

修改後

public class CodePushUpdateManager {

    private String mDocumentsDirectory;
    private CodePush mCodePush;

    public CodePushUpdateManager(String documentsDirectory, CodePush codePush) {
        mDocumentsDirectory = documentsDirectory;
        mCodePush = codePush;
    }

    ...

    private String getCodePushPath() {
        String codePushPath = CodePushUtils.appendPathComponent(getDocumentsDirectory(), CodePushConstants.CODE_PUSH_FOLDER_PREFIX);
        if (!TextUtils.isEmpty(mCodePush.getAssetsBundleFileName())) {
            // 文件目錄按bundle文件名(bundle name)分類:/data/data/[app包名]/files/CodePush/[bundle name]/
            codePushPath = CodePushUtils.appendPathComponent(codePushPath, mCodePush.getAssetsBundleFileName());
        }
        if (CodePush.isUsingTestConfiguration()) {
            codePushPath = CodePushUtils.appendPathComponent(codePushPath, "TestPackages");
        }
        return codePushPath;
    }
}
複製代碼

以上重構都是爲了解決路徑的問題,簡而言之,就是增長一級子目錄(以業務bundle名稱命名),來作到區分業務,加載不一樣的bundle包。

隔離各bundle包環境

梳理了基礎須要調整的地方

  • ReactNativeHost本來是在application中實例化,如今要求每一個Activity實例化一個ReactNativeHost
  • CodePush使用了單例的設計模式,因此得去除單例以及相關的static變量,

CodePush.java的改動比較大,重構後,本來使用類名調用靜態方法的地方就報錯,須要一一去解決,使用變量去調用非靜態方法便可。代碼:github.com/hsl5430/rea…

相關文章
相關標籤/搜索