鸿蒙版ButterKnife

footballboy
发布于 2021-4-1 09:44
浏览
0收藏

目的


实现鸿蒙版本的ButterKnife,能实现控件注入,减少重复代码的编写。

 

实现方式


本质上是注入,可以选择运行期注入,或者编译期注入。运行期注入基于的技术主要是Runtime-Annotation和反射,编译期注入基于Class-Annotation和APT。APT是属于Java的技术,并不是只有在Android才能用,理论上在鸿蒙上也是行的通的。

 

运行期注入


注解声明


绑定控件的注解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface BindView {
    int value();
}

 

点击控件的注解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface OnClick {
    int[] value();
}

 

绑定界面布局的注解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface UiContent {
    int value();
}

 

上面三种的注解都是RetentionPolicy.RUNTIME,表示会在程序运行期生效。

 

注入代码:

public static void Inject(AbilityContext abilityContext) {
        if (abilityContext == null)
            return;
        AbilityContextType type = null;
        if (abilityContext instanceof AbilitySlice)
            type = AbilityContextType.AbilitySlice;
        else if (abilityContext instanceof Ability)
            type = AbilityContextType.Ability;
        else
            return;
        processUiContent(abilityContext, type);
        processBindView(abilityContext, type);
        processOnClick(abilityContext, type);
}

 

反注入代码:

public static void UnInject(AbilityContext abilityContext) {
        if (abilityContext == null)
            return;
        List<Field> cache = fieldCache.remove(abilityContext);
        for (Field field : cache) {
            try {
                field.setAccessible(true);
                field.set(abilityContext, null);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
}

 

举例处理绑定控件注释:

private static void processBindView(AbilityContext abilityContext, AbilityContextType type) {
        Class<? extends AbilityContext> clazz = abilityContext.getClass();
        Field[] fields = clazz.getDeclaredFields();
        List<Field> cache = new ArrayList<>();
        for (Field field : fields) {
            BindView annotation = field.getAnnotation(BindView.class);
            if (annotation != null) {
                int resId = annotation.value();
                Component component = null;
                if (type == AbilityContextType.Ability)
                    component = ((Ability) abilityContext).findComponentById(resId);
                else
                    component = ((AbilitySlice) abilityContext).findComponentById(resId);
                try {
                    field.setAccessible(true);
                    field.set(abilityContext, component);
                    cache.add(field);
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
        if (cache.size() > 0)
            fieldCache.put(abilityContext, cache);
}

代码就是扫描目标实例中所有属性,将被@BindView修饰的属性找出来处理,一样是通过findComponentById找个控件实例,再用反射技术把控件实例设置回去。 至此,注入工作就完成了。

 

使用代码:

@UiContent(ResourceTable.Layout_ability_main)
public class MainAbilitySlice extends AbilitySlice {

    @BindView(ResourceTable.Id_text_helloworld)
    private Text text;

    @Override
    public void onStart(Intent intent) {
        super.onStart(intent);
        InjectHelper.Inject(this);
        text.setText("hm2021");
    }

    @Override
    protected void onStop() {
        super.onStop();
        InjectHelper.UnInject(this);
    }

    @OnClick(ResourceTable.Id_text_helloworld)
    private void clickText(Component component) {
        present(new AptAbilitySlice(), new Intent());
    }
}

代码运行正常,V1版本完成。Android早期也会不少开源的注入框架是基于这个技术实现的,还是比较简单的。当然这并不是本文的重点,考虑到Android版的ButterKnife采用的APT的方式注入,所以我们来试一试。

 

编译期注入

 

APT技术介绍


APT全称Annotation Processing Tool,即编辑器注解处理技术。APT技术会在代码编译的时候处理注解,自动生成一些通用代码,这样就可以大大减轻开发中重复性的工作,即提高的工作效率,也让代码看上去比较简洁。在Android领域,APT的运行非常广泛,比如ButterKnife,ARouter等开源框架都是用到了这项技术。

 

APT注入开发步骤


声明编译期注解,还是用绑定控件举例

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindViewV2 {
    int value();
}

 

创建一个注解处理的模块,模块配置如下

//用来注册自定义注解处理器
implementation "com.google.auto.service:auto-service:1.0-rc4"
annotationProcessor "com.google.auto.service:auto-service:1.0-rc4"
//辅助生成java文件的帮助类
implementation 'com.squareup:javapoet:1.11.1'
//存放注解的模块
implementation project(':annotations')

 

创建自定义注解处理器

@AutoService(Processor.class)
public class InjectProcessor extends AbstractProcessor {

    private Map<String, JavaFileDetail> javaFileMap = new HashMap<>();

    private Elements mElementUtils;//用来处理程序元素的工具类
    private Filer mFilerUtils;//用来生成java文件的工具类

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        mElementUtils = processingEnv.getElementUtils();
        mFilerUtils = processingEnv.getFiler();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        processUiContentV2(annotations, roundEnv);
        processBindViewV2(annotations, roundEnv);
        processOnClickV2(annotations, roundEnv);

        //生成文件
        for (Map.Entry<String, JavaFileDetail> entry : javaFileMap.entrySet()) {
            JavaFile javaFile = JavaFile.builder(entry.getValue().getPackageName(), entry.getValue().generateFile()).build();
            try {
                javaFile.writeTo(mFilerUtils);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        //清除,否则会有无谓的报错
        javaFileMap.clear();
        return true;
    }
    ......
}

 

自定义的注解处理类,需要继承AbstractProcessor,主要重写4个方法:

//初始化函数
public synchronized void init(ProcessingEnvironment processingEnv)
//处理注解的函数
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
//设置支持的注解类型
public Set<String> getSupportedAnnotationTypes()
//设置代码支持的JDK版本
public SourceVersion getSupportedSourceVersion()

 

创建注入Api

public static void Inject(AbilityContext abilityContext) {
        try {
            Class clazz = Class.forName(abilityContext.getClass().getCanonicalName() + "$$ViewBinder");
            Constructor constructor = clazz.getConstructor();
            IViewBinder instance = (IViewBinder) constructor.newInstance();
            instance.bind(abilityContext);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void UnInject(AbilityContext abilityContext) {
        try {
            Class clazz = Class.forName(abilityContext.getClass().getCanonicalName() + "$$ViewBinder");
            Constructor constructor = clazz.getConstructor();
            IViewBinder instance = (IViewBinder) constructor.newInstance();
            instance.unBind(abilityContext);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

 

使用

@UiContentV2(ResourceTable.Layout_ability_apt)
public class AptAbilitySlice extends AbilitySlice {

    @BindViewV2(ResourceTable.Id_text_apt)
    Text aptText;

    @Override
    protected void onStart(Intent intent) {
        super.onStart(intent);
        InjectHelperV2.Inject(this);
        aptText.setText("apt222");
    }

    @Override
    protected void onStop() {
        super.onStop();
        InjectHelperV2.UnInject(this);
    }

    @OnClickV2(ResourceTable.Id_text_apt)
    void clickText(Component component) {
        ((Text) component).setText("apt33333");
    }
}

 

总结工作执行流程


当我们执行build编译的时候,会先在entry/build/generated/sources/...生成一个java文件,如下

public class AptAbilitySlice$$ViewBinder implements IViewBinder<AptAbilitySlice> {
  public void bind(AptAbilitySlice target) {
    target.setUIContent(16777221);
    target.aptText= (ohos.agp.components.Text)target.findComponentById(16777223);
    target.findComponentById(16777223).setClickedListener(cpt -> {target.clickText(cpt);});
  }

  public void unBind(AptAbilitySlice target) {
    target.aptText= null;
  }
}

 

然后执行注入的时候,会根据命名规则得到自动生成java类的类名,然后反射获取实例:

Class clazz = Class.forName(abilityContext.getClass().getCanonicalName() + "$$ViewBinder");

 

执行bind方法完成注入:

IViewBinder instance = (IViewBinder) constructor.newInstance();
instance.bind(abilityContext);

 

最后让我们把代码跑起来检验成果,代码运行在P40上一切正常,至此注入V2版本大功告成。

 

总结


注入的方式可以大大简化开发工作,代码也易于维护,运行期注入会因为大量反射消耗一点性能,编译器注入自运行的时候基本不影响性能,但是编译时可能时间会长点。总体来说,推荐使用V2版本来注入,V2版本才是真正的鸿蒙版ButterKnife。

 

相关链接

Github:https://github.com/loubinfeng2013/HarmonyTools

 

 

 

 

 

 

 

作者:暗影萨满 

分类
已于2021-4-1 09:44:11修改
收藏
回复
举报
回复
    相关推荐