Microsoft CodePush 研究报告

1. 功能

1.1 三种更新策略选择:

  • 静默模式(slient mode): 无提示的直接安装新的更新,可以更新起效的时间点:
    • IMMEDIATE 立即生效,activity重启;
    • ON_NEXT_RESTART 下一次应用restart
    • ON_NEXT_RESUME 下一次应用resume或者restart时
  • 选择模式(active mode): 弹出一个对话框,提示用户升级,由用户决定;
  • 自定义模式(custom mode): CodePush 提供了onSyncStatusChange 和 onDownloadProgress的回调函数的使用方法,可以通过自定回调函数以达到自定义的更新流程的功能

1.2 注册使用

2. 基本架构

涉及到Javascript部分和Java部分,值得注意的是两部分均有网络访问服务器的功能,Javascript部分的网络访问请求主要是检查是否有更新,而Java部分的网络访问请求则是下载更新包,猜测CodePush将检查是否有更新放到Javascript里可能是考虑到该接口可能会有更改,比如报文字段,而下载更新包的接口实质是给定资源地址,不存在请求参数,所以变动不大。

CodePush for RN
CodePush for RN

2.1 JavaScript部分


CodePush.js

热更新js端入口文件,其实质是一个对应用的根组件的进行装饰的装饰类.

// CodePush.js
var decorator = (RootComponent) => {
    return class CodePushComponent extends React.Component {
        componentDidMount() {
            // ...
            CodePush.sync(options,...);
        }
        render() {
            return <RootComponent {...this.props} ref={"rootComponent"} />;
        }
    }
};
if (typeof options === "function") {
// Infer that the root component was directly passed to us.
    return decorator(options);
} else {
    return decorator;
}
// 不使用CodePush的写法
class myApp extends Component {
    //…
};
AppRegistry.registerCompent("myApp", () => myApp);

/************************************************/

// 使用CodePush的写法
class myApp extends Component{
    //…
};

let CodePushOptions = { //设置一些CodePush相关属性
    //…
    }; 
let CodePushApp = CodePush(CodePushOptions)(myApp); //装饰myApp
AppRegistry.registerCompent("myApp", () => CodePushApp);

AcquistionManager

与后台服务器通信的SDK文件,负责检查更新请求的发送等内容,注意下载更新包的请求并不式js端完成,而是在Java端完成;

  • queryUpdateWithCurrentPackage() 根据当前的安装包信息查询更新情况
  • reportStatusDeploy() 向服务器上传信息
  • reportStatusDownload() 向服务器上传信息

RestartManager

维持一个_restartQueue数组,提供以下四个函数

  • allow() 设置_allowed变量为true,如果队列中不为空,则执行restartApp(_restartQueue.shit(1))
  • clearPendingRestart() 清空队列,即_restartQueue = []
  • disallow() 设置_allowed变量为false
  • restartApp() 进一步调用NativeCodePush.restartApp()

NativeCodePush

JavaCodePushNativeModuleJS端的调用对象,用于调用Native的方法.

let NativeCodePush = require("react-native").NativeModules.CodePush;

2.2 Java部分


CodePush.java

一个ReactPackage的实现,管理CodePushNativeModule

public class CodePush implements ReactPackage {
    // ...
    @Override
    public List<NativeModule> createNativeModules(ReactApplicationContext reactApplicationContext) {
        CodePushNativeModule codePushModule = new CodePushNativeModule(reactApplicationContext, this, mUpdateManager, mTelemetryManager, mSettingsManager);
        CodePushDialog dialogModule = new CodePushDialog(reactApplicationContext);

        List<NativeModule> nativeModules = new ArrayList<>();
        nativeModules.add(codePushModule);
        nativeModules.add(dialogModule);
        return nativeModules;
    }
}

CodePushNativeModule

定义对JS层暴露的方法:

  • getConfiguration() 获取配置参数,如appVersionserverUrl

  • getUpdateMetadata() 获取当前package的app.json的内容

  • getNewStatusReport()

  • downloadUpdate() 下载更新,实质执行的CodePushUpdateManager.downloadUpdate()

  • installUpdate() 安装更新,实质执行的是CodePushUpdateManager.installUpdate()

  • notifyApplicationReady()

  • recordStatusReported()

  • restartApp() 调用loadBundle()(利用反射):

    private void loadBundle() {
        mCodePush.clearDebugCacheIfNeeded();
        try {
            // #1) Get the ReactInstanceManager instance, which is what includes the
            //     logic to reload the current React context.
            final ReactInstanceManager instanceManager = resolveInstanceManager();
            if (instanceManager == null) {
                return;
            }
    
            String latestJSBundleFile = mCodePush.getJSBundleFileInternal(mCodePush.getAssetsBundleFileName());
    
            // #2) Update the locally stored JS bundle file path
            setJSBundle(instanceManager, latestJSBundleFile);
    
            // #3) Get the context creation method and fire it on the UI thread (which RN enforces)
            final Method recreateMethod = instanceManager.getClass().getMethod("recreateReactContextInBackground");
            new Handler(Looper.getMainLooper()).post(new Runnable() {
                @Override
                public void run() {
                    try {
                        recreateMethod.invoke(instanceManager);
                        mCodePush.initializeUpdateAfterRestart();
                    } catch (Exception e) {
                        // The recreation method threw an unknown exception
                        // so just simply fallback to restarting the Activity (if it exists)
                        loadBundleLegacy();
                    }
                }
            });
    
        } catch (Exception e) {
            // Our reflection logic failed somewhere
            // so fall back to restarting the Activity (if it exists)
            loadBundleLegacy();
        }
    }
    
  • saveStatusReportForRetry()

CodePushUpdateManager

管理更新包的下载、删除等

  • downloadPackage( updateJsonObj ) 根据传入的updateJsonObj对象构造下载请求,并存储结构存储下载的更新包
  • installPackage( updateJsonObj ) 根据传入的updateJsonObj对象,更新codepush.json文件,codepush.json文件用于记录当前使用的和上一次使用的package的信息
  • rollbackPackage() 版本回滚,实质是更新codepush.json文件

CodePushTelemetryManager

管理SharedPreference中的RETRY_DELOYMENT_KEYLAST_DELOPYMENT_KEY两个值,提供的相应的增删改查操作;

  • RETRY_DELOYMENT_KEY
  • LAST_DELOPYMENT_KEY

SettingsManager

管理SharedPreference中的FAILED_UPDATES_KEYPENDING_UPDATE_KEY两个值,提供的相应的增删改查操作;

  • FAILED_UPDATES_KEY 存储安装失败的pacakage的信息(信息格式为json字符串)
  • PENDING_UPDATE_KEY 存储等待安装的pacakage的信息(信息格式为json字符串)

3. 数据存储

3.1 存储

  • SharedPerefence

    • FAILED_UPDATES_KEY
    • PENDING_UPDATE_KEY
    • RETRY_DELOYMENT_KEY
    • LAST_DELOPYMENT_KEY
  • 内部存储
    根目录/data/data/com.xxx.xxx/files,即Context.getFilesDir().getAbsolutePath(),属于应用的私文件

- CodePush/
    - codepush.json
    - {hashcode}/
      - xxxx.bundle
      - app.json
    - {hashcode}/
      - xxxx.bundle
      - app.json
    - unzipped/ (临时,若下载的更新包是zip文件)

3.2 json结构

// codepush.json 中的参数
{
    // ...
    currentPackage: "当前package的hashCode值",
    priviousPackage: "上一个版本的package的hashcode值"
}
// app.json 中的参数
{
    // ...
    packageHash: "该package对应的hashcode值",
    bundlePath: "JS bundle的相对位置"
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容