Android 混合式开发的一次实践

Native_html5_hybrid.png

谈到移动端混合式开发,我们很容易想到当下如日中天的React Native(以下简称 RN)或者Cordova 移动端开发框架。其实关于RN以及Cordova的开发实践网上的总结也很多,所以这里不是我介绍的重点。我想介绍的“混合式开发”是将 Cordova 构建的工程作为一个插件库的形式添加到 native 工程中。

问题背景

对于新项目的技术选型来说,没有最完美的,只有最合适的。技术选型过程中我们通常会考虑如下几点:

  • 技术本身的成熟度,社区资源
  • 项目成员的技术栈
  • 项目成本预算

而我们接手的客户项目是一个Android native 的遗留项目,APP功能主要是文本、图片等业务信息显示。团队成员上 Android 开发人员较少,相较而言前端开发人员较多。并且项目上线时间比较紧急。所以团队在预研可行性并评估之后决定采用 Cordova 来做一次混合式开发。

与我们最常认识的混合式开发不同的是,我们的混合开发的是一个模块,并且这个子模块和原有的 Android native 之后有较多的页面交互。所以我们一路摸索开始了这次尝试。

最值得一提的是,由于某些客观因素,我们必须在公司外网构建这个Cordova 工程,然后再集成到内网的 native 应用中。这对开发的影响是,Native 工程和 Cordova 工程在开发过程中都是相互隔离的,集成阶段才会将 Cordova 构建出来的包放置到内网 native 工程中。

核心难点

其实在项目开发过程中 Cordova + Angular + ionic 这一套也有很多可以总结的点。而我则主要将 Cordova 和 Native 工程的沟通桥梁来谈。可能做过RN 或者 Cordova 的人或多或少都知道这个桥梁叫做 “React Context Module” 或者 “Cordova Plugin”。这个桥梁连接了 Native 和 JS 这两个不同世界。

那么在这个开发过程中,我们需要解决哪些问题呢?

  1. App 集成方式
  2. 应用基本信息共享
  3. 用户授权信息管理
  4. Native 打开指定的 Cordova 页面
  5. Cordova 打开指定的 Native 页面

解决手段

1. App 集成方式

通常而言,Cordova 打包 Android 平台会产生 APK 文件,然后对外发布 APK 即可。但是为了满足模块化混合开发方式,只需要将 platforms/android/build.gradle 修改为 library 形式即可:

apply plugin: 'com.android.library'

buildscript {
    repositories {
        mavenCentral()
        jcenter()
    }

    dependencies {
        classpath 'com.android.tools.build:gradle:2.1.0'
    }
}

...

通过 cordova build android --release 在 Android 和 CordovaLib 下找到打包完成的 AAR 文件。这个工程的主入口是MainActivity,我们只需要在 Native 工程中打开 MainActivity,这样便完成了 Cordova 与 Native 的集成。唯一的改变是MainActivity 在 Cordova 里面算是个主入口,但是集成到 Native 工程中就只能算是一个子模块入口。后期开发只需要更新 AAR 文件即可。

2. 应用基本信息共享

应用基本信息包括,用户名、用户头像、App运行环境(sit/uat/pro)等。按照第一点集成的流程,在打开 Cordova 主入口的时候可以传入用户的基本信息。主入口的 Activity 获取到基本信息之后,JS 中通过插件的形式从 Activity 中获取,就完成了这次传递。

public class DataPlugin extends CordovaPlugin {
  // Do not modify the string variable!!
  private static final String ACTION_GET_CONFIG = "getConfig";
  @Override
  public boolean execute(String action, JSONArray args, CallbackContext callbackContext) {
    try {
      if (ACTION_GET_CONFIG.equals(action)) {
        MainActivity mainActivity = (MainActivity) cordova.getActivity();
        String host = mainActivity.getHost();
        String language = mainActivity.getLanguage();
        String currentUser = mainActivity.getCurrentUser();

        JSONObject configJsonObject = new JSONObject();
        configJsonObject.put("host", host);
        configJsonObject.put("language", language);
        configJsonObject.put("username", currentUser);
        callbackContext.success(configJsonObject);
        return true;
      } else {
        callbackContext.error("Invalid action");
        return false;
      }
    } catch (Exception e) {
      callbackContext.error(e.getMessage());
      return true;
    }
  }
}

比如上面的 DataPlugin 就将 username, language, host 等信息暴露给 JS 端。从而实现了这种基本信息的共享。

3. 用户授权信息管理

用户授权管理包括:登录、权限过期、注销等主要场景。用户进入 Cordova 模块中,不应该再次要求用户登录,所以只需要将 Native 中用户登录产生的 Token 或者 Session 等信息通过上述手段共享给 Corodva 层,然后 JS 中按需使用即可。

授权过期的问题,比如我们发现每 30 分钟 token 都会失效。这时候就需要 JS 中进行判断,一旦出现权限问题,就通过“广播插件”发送App广播,Native中注册token失效监听逻辑被触发,重新进入 Cordova 子模块,从而实现刷新 token 的效果。

注销登录,和授权过期的处理类似,通过局部广播调用 native 层的注销登录逻辑,然后退出当前应用,完成注销操作。

4. Native 打开指定的 Cordova 页面

Native 打开指定的 Cordova 页面,还是通过页面信息共享的方式传入参数。在 JS 工程初始化完成,通过 DataPlugin 读取页面跳转参数之后,通过前端的路由进行相关跳转。

一般而言,Native 打开 H5 前端页面,比较常见于接收到推送通知,用户点击跳转的逻辑。需要 Native 和 Corodva 约定跳转数据格式。我比较习惯于采用 Restful 的模式指定页面路由进行跳转。

5. Cordova 打开指定的 Native 页面

在 H5 页面中打开 Naitve 的页面,场景需求还比较多。比如 H5 页面中有需要直接复用原有 Native 页面的逻辑。或者类似于选择手机相册的功能,前端时间效果不好的时候,可以考虑使用 Native 页面实现。Corodva 前端页面打开 Native 的工程还是通过 CordovaPlugin 实现。

  private CallbackContext callbackContext;

  @TargetApi(Build.VERSION_CODES.CUPCAKE)
  @Override
  public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
    this.callbackContext = callbackContext;

    if (ACTION_APP_EXIT.equals(action)) {
      cordova.getActivity().finish();
      callbackContext.success();
      return true;
    }

    if (ACTION_OPEN_MY_SETTING.equals(action)) {
      Intent intent = new Intent();
      intent.setComponent(new ComponentName(getApplicationId(), MAIN_APP_PACKAGE_NAME + ".activity.MeDetailActivity"));
      cordova.startActivityForResult(this, intent, REQUEST_CODE_GALLERY);
      return true;
    }

    if (ACTION_OPEN_GALLERY.equals(action)) {
      Intent galleryIntent = new Intent();
      galleryIntent.setComponent(new ComponentName(getApplicationId(), MAIN_APP_PACKAGE_NAME + ".activity.reception.GalleryChooserActivity"));
      cordova.startActivityForResult(this, galleryIntent, REQUEST_CODE_GALLERY);
      return true;
    }

    return false;
}

  @Override
  public void onActivityResult(int requestCode, int resultCode, Intent intent) {
    if (resultCode == Activity.RESULT_OK) {
      if (requestCode == REQUEST_CODE_GALLERY) {
        String[] imageUris = intent.getStringArrayExtra("galleryImages");
        callbackContext.success(parseJsonArrayFromStrings(imageUris));
        return;
      }
    }
    super.onActivityResult(requestCode, resultCode, intent);
  }

虽然是通过 CordovaPlugin 开发,Native 中能实现的 startActivity, startActivityForResult 也都可以轻松实现。

经验总结

1. 善于利用 Native 中的资源

在混合开发中,由于 Cordova Plugin 基本能够无缝的和原生页面进行交互,所以如果 Native 模块已经有的功能尽量不要在 H5 中重复添加,页面或者数据尽量的共用,降低维护成本。

除了资源共用问题,我们还需要多利用 Native 的优势,比如 Native 优于 H5 的性能。对于某些复杂的需求,比如读取手机相册、加载大几百条列表数据,应该考虑到 H5 可能会存在性能问题,这时候不妨试试用 Native 实现这个页面。

2. 模块依赖过深时尝试使用反射

如果你的两个模块在不同的工程中,相互引用不太方便。但是在页面间传数据的时候, Cordova 又不得不引用 Native 中的某个 model 类。这时候尝试使用反射调用来规避这种限制。当然这样之后,一旦 Native 中的文件需要重命名或者更改路径,都必须在 Cordova 插件中同步修改。

3. 使用本地广播进行消息传递

Cordova H5 工程中如果出现某些变更需要及时通知 Native 层,比如切换 App 语言、更新用户名或者头像等。可以借用广播这种模式来传递。因为在插件中能够轻易地引用 Android 相关的库,但是调用 Native 工程自定义的库或者类比较困难。所以广播在这种场景中效果相当方便。

4. 使用基础数据类型在页面间传递数据

由于在 Cordova 工程中直接引用 Native 工程中某个类比较的困难,所以在可以选择的情况下,页面之前的数据传递尽量越简单越好。而且尽量使用基础数据类型,对于一些结构化的数据可以使用 json 字符串传输。

5. 警惕 Cordova 首次加载速度问题

Cordova 本质上是一个 WebView,直接加载打包好的前端 index.html 页面。在项目中发现应用启动后,首次进入 Cordova 模块的时间稳定在 3-5 秒。如果 Cordova 对应的页面是应用的主入口,这个时间倒没什么不妥。但是对于混合开发中,进入一个二级子页面,却耗时 3-5 秒,的确是一个令人头疼的事情,如果这时候客户对子页面的响应速度要求过高。这个或许是一个最大的风险。Cordova 启动耗时主要是 Cordova 插件加载以及前端 JS 框架的加载。我们项目中有尝试过优化,但是效果比较一般。

6. 用户体验问题

原生和非原生的用户体验相差的确很多,在 Cordova 上这个问题体验尤其明显。比如项目中有个几百人的联系人列表,在 Native 上使用列表组件流畅无比,但是如果在 H5 上绘制几百个 DIV,并且滑动过程中Dom元素很难做到自动回收。使用效果就不尽如人意。所以对于某些问题突出的页面可以直接用 native 代替也未尝不是一种解决思路。

结语

混合式开发模式,能够在体验 Hybrid 开发移动应用的同时,也可以最大限度的利用 Native 的一些优势,做到效率和用户体验之间的平衡。其中有优点也有缺点,需要我们根据项目实际情况选择适合当前的。

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

推荐阅读更多精彩内容