基于React Native的三端融合方案
组件精简
由于原生的RN组件库比较庞大,并且有很多单端独有的组件(如Android端的BackHandler、PermissionAndroid等),因此需要做一次RN组件的裁剪以减少包大小以及拉齐三端的组件功能。当然这么做的弊端就是对于一些单端功能的支持变差,需要通过额外手段(如直接通过native代码)来完善这些功能,此外我们也无法用RN来实现交互“较重”的页面。
那么要实现组件精简,我们就需要直接依赖RN的源码,从而能实现删减代码的目的。具体的步骤我们可以参考官方的文档:https://reactnative.cn/docs/building-from-source/ 这里想提几个需要注意的事项:
- 需要NDK的版本,太高版本貌似不行,目前使用的是r10e
- 需要修改ReactAndroid里sdk的版本
- 注意com.android.support:appcompat-v7的重复引用
与native通信的插件(并且与H5即WebView插件对齐)编写
公司内部对WebView实现了封装,JS与Native的通信也使用了类似RN的Module形式,但是Module的实现需要继承特定的基类,并且在注册使用时候还用到了反射,因此我们无法使用同一个类同时给WebView以及RN使用。因此我们需要有一个实现具体功能的类以及包含这个实现类的两个插件类分别作用于WebView以及RN,除此之外还需要分别将这个两个插件类注册到WebView以及RN中。这个过程中间包含了很多模板代码,因此我们选择使用注解来减少重复代码量。
WebView以及RN插件类图
注解处理器
@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class AnimagiProcessor extends AbstractProcessor {
private Filer mFiler; //文件相关的辅助类
private Elements mElementUtils; //元素相关的辅助类
private Messager mMessager; //日志相关的辅助类
private Map<String, AnnotatedClass> mAnnotatedClassMap;
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
//filter用来创建新的源文件、class文件以及辅助文件
mFiler = processingEnv.getFiler();
//elements中包含着操作element的工具方法
mElementUtils = processingEnv.getElementUtils();
//用来报告错误、警告以及其他提示信息
mMessager = processingEnv.getMessager();
mAnnotatedClassMap = new TreeMap<>();
//processingZEnvirment中还有操作TYPE mirror的
//processingEnv.getTypeUtils();
System.out.println("This is AnimagiProcessor");
}
/**
* 每一个注解处理器类都必须有一个空的构造函数。然而,这里有一个特殊的init()方法,
* 它会被注解处理工具调用,并输入ProcessingEnviroment参数。
* ProcessingEnviroment提供很多有用的工具类Elements,Types和Filer
* <p>
* 这相当于每个处理器的主函数main()。 在这里写扫描、评估和处理注解的代码,以及生成Java文件。
* 输入参数RoundEnviroment,可以让查询出包含特定注解的被注解元素
*/
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
//RoundEnvironment
//可以返回包含指定注解类型的元素的集合
mAnnotatedClassMap.clear();
try {
processAnimagi(roundEnv);
} catch (IllegalArgumentException e) {
e.printStackTrace();
error(e.getMessage());
}
for (AnnotatedClass annotatedClass : mAnnotatedClassMap.values()) {
try {
//输出中间文件
annotatedClass.generateFusion().writeTo(mFiler);
annotatedClass.generateReact().writeTo(mFiler);
} catch (IOException e) {
error("Generate file failed, reason: %s", e.getMessage());
}
}
return true;
}
private void processAnimagi(RoundEnvironment roundEnv) throws IllegalArgumentException {
for (Element element : roundEnv.getElementsAnnotatedWith(Animagi.class)) {
AnnotatedClass annotatedClass = getAnnotatedClass(element);
}
}
/**
* 获取注解所在文件对应的生成类
*/
private AnnotatedClass getAnnotatedClass(Element element) {
//typeElement表示类或者接口元素
TypeElement typeElement = (TypeElement) element;
String fullName = typeElement.getQualifiedName().toString();
//这里其实就是变相获得了注解的类名(完全限定名称,这里是这么说的)
AnnotatedClass annotatedClass = mAnnotatedClassMap.get(fullName);
// Map<String, AnnotatedClass>
if (annotatedClass == null) {
AnimagiType animagiType = new AnimagiType(element);
annotatedClass = new AnnotatedClass(typeElement, mElementUtils, animagiType);
mAnnotatedClassMap.put(fullName, annotatedClass);
}
return annotatedClass;
}
private void error(String msg, Object... args) {
mMessager.printMessage(Diagnostic.Kind.ERROR, String.format(msg, args));
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new LinkedHashSet<>();
types.add(Animagi.class.getCanonicalName());
return types;
}
}
封装TypeElement的类
public class AnimagiType {
private TypeElement mTypeElement;
private int mMode; //仅WebView插件、仅React插件、两者都有
private String[] mMethods; //需要生成的所有方法名
AnimagiType(Element element) throws IllegalArgumentException {
if (element.getKind() != ElementKind.CLASS) {
throw new IllegalArgumentException(String.format("Only classes can be annotated with @%s",
Animagi.class.getSimpleName()));
}
mTypeElement = (TypeElement) element;
//从目标文件中获取注解标记的变量
Animagi animagi = mTypeElement.getAnnotation(Animagi.class);
mMode = animagi.mode();
mMethods = animagi.methods();
if (mMode < 0) {
throw new IllegalArgumentException(
String.format("type() in %s for class %s is not valid !", Animagi.class.getSimpleName(),
mTypeElement.getSimpleName()));
}
}
/**
* @return 获取类名
*/
Name getTypeName() {
return mTypeElement.getSimpleName();
}
/**
* @return 获取mode
*/
int getMode() {
return mMode;
}
/**
* @return 获取所有需要生成的方法名
*/
String[] getMethods() {
return mMethods;
}
/**
* @return 获取注解所在类的类型
*/
TypeMirror getType() {
return mTypeElement.asType();
}
}
具体的注解生成模板代码类
public class AnnotatedClass {
/**
* 类或者接口元素
*/
private TypeElement mTypeElement;
/**
* 注解的类的信息
*/
private AnimagiType mAnimagiType;
/**
* 辅助类,用于后文的文件输出
*/
private Elements mElements;
/**
* @param typeElement 注解所在的类或者接口
* @param elements 辅助类
* @param animagiType 注解处理的类
*/
AnnotatedClass(TypeElement typeElement, Elements elements, AnimagiType animagiType) {
mTypeElement = typeElement;
mElements = elements;
mAnimagiType = animagiType;
}
JavaFile generateFusion() {
String packageName = mElements.getPackageOf(mTypeElement).getQualifiedName().toString();
String className = mTypeElement.getSimpleName() + "$$Fusion";
TypeSpec.Builder builder = ClassMaker.makeClassFusion(className, mTypeElement);
builder.addMethod(ClassMaker.makeConstructorFusion(mTypeElement).build());
builder.addMethod(ClassMaker.makeFunctionCollectFusion().build());
builder.addMethod(ClassMaker.makeFunctionCollectStaticFusion(className).build());
if (mAnimagiType.getMethods() != null) {
for (String method : mAnimagiType.getMethods()) {
builder.addMethod(ClassMaker.makeFunctionForJSFusion(method).build());
}
}
TypeSpec injectClass = builder.build();
return JavaFile.builder(packageName, injectClass).build();
}
JavaFile generateReact() {
String className = mTypeElement.getSimpleName() + "$$React";
TypeSpec.Builder builder = ClassMaker.makeClassReact(className, mTypeElement);
builder.addMethod(ClassMaker.makeConstructorReact(mTypeElement).build());
builder.addMethod(ClassMaker.makeFunctionCollectReact(className).build());
builder.addMethod(ClassMaker.makeFunctionCollectStaticReact(className).build());
builder.addMethod(ClassMaker.makeFunctionName(className).build());
builder.addMethod(ClassMaker.makeFunctionGetConstants().build());
if (mAnimagiType.getMethods() != null) {
for (String method : mAnimagiType.getMethods()) {
builder.addMethod(ClassMaker.makeFunctionForJSReact(method).build());
}
}
TypeSpec injectClass = builder.build();
String packageName = mElements.getPackageOf(mTypeElement).getQualifiedName().toString();
return JavaFile.builder(packageName, injectClass).build();
}
}
React Native插件生成模板代码的实现代码片段
public static final ClassName BASE_MODULE_REACT = ClassName.get("com.didi.amm.animagi.react", "BaseModuleReact");
public static final ClassName I_COLLECTOR_REACT = ClassName.get("com.didi.amm.animagi.react", "ICollectorReact");
public static final ClassName CALLBACK_REACT = ClassName.get("com.facebook.react.bridge", "Callback");
public static final ClassName BASE_MODULE_REMIX = ClassName.get("com.didi.amm.animagi.remix", "BaseModuleRemix");
public static final ClassName REACT_APPLICATION_CONTEXT = ClassName.get("com.facebook.react.bridge", "ReactApplicationContext");
public static final ClassName ANNOTATION_REACTMETHOD = ClassName.get("com.facebook.react.bridge", "ReactMethod");
public static final ClassName JSON_OBJECT = ClassName.get("org.json", "JSONObject");
public static final ClassName MAP = ClassName.get("java.util", "Map");
public static final ClassName LIST = ClassName.get("java.util", "List");
public static final ClassName HASHMAP = ClassName.get("java.util", "HashMap");
public static final ClassName STRING = ClassName.get("java.lang", "String");
public static final ClassName OBJECT = ClassName.get("java.lang", "Object");
public static final ClassName JSON_EXCEPTION = ClassName.get("java.lang", "Exception");
public static final ClassName READABLE_MAP = ClassName.get("com.facebook.react.bridge", "ReadableMap");
public static final ClassName NATIVE_MODULE = ClassName.get("com.facebook.react.bridge", "NativeModule");
public static TypeSpec.Builder makeClassReact(String className, TypeElement typeElement) {
return TypeSpec.classBuilder(className)
.addModifiers(Modifier.PUBLIC)
.addSuperinterface(TypeUtil.I_COLLECTOR_REACT)
.superclass(ParameterizedTypeName.get(ClassMaker.TypeUtil.BASE_MODULE_REACT, TypeName.get(typeElement.asType())));
}
public static MethodSpec.Builder makeConstructorReact(TypeElement typeElement) {
MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(TypeUtil.REACT_APPLICATION_CONTEXT, "reactContext");
constructor.addStatement("super(reactContext, new $T($T.TYPE_REACT))", TypeName.get(typeElement.asType()), TypeUtil.BASE_MODULE_REMIX);
return constructor;
}
public static MethodSpec.Builder makeFunctionCollectReact(String className) {
MethodSpec.Builder builder = MethodSpec.methodBuilder("collect")
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.addParameter(TypeUtil.REACT_APPLICATION_CONTEXT, "reactContext")
.addParameter(ParameterizedTypeName.get(TypeUtil.LIST, TypeUtil.NATIVE_MODULE), "modules");
builder.addStatement("modules.add(new $N(reactContext))", className);
return builder;
}
public static MethodSpec.Builder makeFunctionCollectStaticReact(String className) {
MethodSpec.Builder builder = MethodSpec.methodBuilder("collectStatic")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addParameter(TypeUtil.REACT_APPLICATION_CONTEXT, "reactContext")
.addParameter(ParameterizedTypeName.get(TypeUtil.LIST, TypeUtil.NATIVE_MODULE), "modules");
builder.addStatement("modules.add(new $N(reactContext))", className);
return builder;
}
public static MethodSpec.Builder makeFunctionName(String className) {
MethodSpec.Builder builder = MethodSpec.methodBuilder("getName")
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.returns(String.class);
builder.addStatement("return " + "\"" + className + "\"");
return builder;
}
public static MethodSpec.Builder makeFunctionGetConstants() {
MethodSpec.Builder builder = MethodSpec.methodBuilder("getConstants")
.addModifiers(Modifier.PUBLIC)
.addAnnotation(Override.class)
.returns(ParameterizedTypeName.get(TypeUtil.MAP, TypeUtil.STRING, TypeUtil.OBJECT));
builder.addStatement("final $T<String, Object> constants = new $T<>()", TypeUtil.MAP, TypeUtil.HASHMAP);
builder.addStatement("return constants");
return builder;
}
public static MethodSpec.Builder makeFunctionForJSReact(String method) {
//定义方法 method(JSONObject params, Callback callback);
MethodSpec.Builder builder = MethodSpec.methodBuilder(method)
.addModifiers(Modifier.PUBLIC)
.addAnnotation(TypeUtil.ANNOTATION_REACTMETHOD)
.addParameter(TypeUtil.READABLE_MAP, "params", Modifier.FINAL)
.addParameter(TypeUtil.CALLBACK_REACT, "callback");
builder.addStatement("try{ $T jsonObj = new JSONObject(params.toHashMap())", TypeUtil.JSON_OBJECT);
builder.addStatement("mBaseModuleRemix." + method + "(getReactApplicationContext(),jsonObj,callback);}catch($T e){e.printStackTrace();}", TypeUtil.JSON_EXCEPTION);
return builder;
}
bundle拆分、下载、解压、合并管理
需要拆包的原因主要有以下几个:
- bundle文件过大: 一个新建的App,如果使用RN 0.56版本,使用官方命令react-native bundle打包出来的JSBundle文件大小大约为530KB,RN依赖模块本身占了99.9%。
- 页面加载慢: 如果使用热更新,从网络获取整个包的下载时间很长,每次进入RN页面都需要执行RN基础模块的定义。
拆包方案大概有以下几种:
- 侵入RN代码,修改打包流程,使得打出来的包就是基础+业务包,如QQ音乐
- 在RN打包的基础上,实现新的打包方案,如携程 moles-Packer
-
Patch方案,打包流程不变,生成基础包后,根据diff来生成每个业务不同的patch包
我们的大概策略就是先拆除一个sdk以及公共业务相关的common包,然后将每个业务模块拆成独立的business包。我们会将common包打进安装包内,business包则会由服务端通过一定的版本管理策略下发。下载完成后需要将business包进行解压(下载的会是一个zip包),然后将common包与business进行合并,并最终将合成包移动到指定的生产环境目录中,最终打开页面时候将会加载生产环境下的合成包。
RN包管理涉及到的类
降级策略
当遇到加载RN页面失败甚至闪退时需要降级为H5来显示。具体策略需要根据具体场景来订制。