浅谈LiveData的通知过程

news2024/9/20 22:39:12

浅谈 LiveData 的通知机制

LiveData 和 ViewModel 一起是 Google 官方的 MVVM 架构的一个组成部分。巧了,昨天分析了一个问题是 ViewModel 的生命周期导致的。今天又遇到了一个问题是 LiveData 通知导致的。而 ViewModel 的生命周期和 LiveData 的通知机制是它们的主要责任。所以,就这个机会我们也来分析一下 LiveData 通知的实现过程。

  1. 关于 ViewModel 的生命周期:《浅谈 LiveData 的通知机制》;
  2. 关于 MVVM 设计模式的基本应用,你可以参考这篇文章:《Android 架构设计:MVC、MVP、MVVM和组件化》.

1、一个 LiveData 的问题

今天所遇到的问题是这样的,

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

有两个页面 A 和 B,A 是一个 Fragment ,是一个列表的展示页;B 是其他的页面。首先,A 会更新页面,并且为了防止连续更新,再每次更新之前需要检查一个布尔值,只有为 false 的时候才允许从网络加载数据。每次加载数据之前会将该布尔值置为 true,拿到了结果之后置为 false. 这里拿到的结果是借助 LiveData 来通知给页面进行更新的。

现在,A 打开了 B,B 中对列表中的数据进行了更新,然后发了一条类似于广播的消息。此时,A 接收了消息并进行数据加载。过了一段时间,B 准备退出,再退出的时候又对列表中的项目进行了更新,所以此时又发出了一条消息。

B 关闭了,我们回到了 A 页面。但是,此时,我们发现 A 页面中的数据只包含了第一次的数据更新,第二次的数据更新没有体现在列表中。

用代码来描述的话大致是下面这样,

    // 类 A
    public class A extends Fragment {
    
        private boolean loading = false;

        private MyViewModel vm;

        // ......

        /**
         * Register load observer.
         */
        public void registerObservers() {
            vm.getData().observe(this, resources -> {
                loading = false;
                // ... show in list
            })
        }

        /**
         * Load data from server.
         */
        public void loadData() {
            if (loading) return;
            loading = true;
            vm.load();
        }

        /**
         * On receive message.
         */
        public void onReceive() {
            loadData();
        }
    }

    public class B extends Activity {

        public void doBusiness1() {
            sendMessage(MSG); // Send message when on foreground.
        }

        @Override
        public void onBackpressed() {
            // ....
            sendMessage(MSG); // Send message when back
        }
    }

    public class MyViewModel extends ViewModel {

        private MutableLiveData<Resoucres<Object>> data;

        public MutableLiveData<Resoucres<Object>> getData() {
            if (data == null) {
                data = new MutableLiveData<>();
            }
            return data;
        }

        public void load() {
            Object result = AsyncGetData.getData(); // Get data
            if (data != null) {
                data.setValue(Resouces.success(result));
            }
        }
    }

A 打开了 B 之后,A 处于后台,B 处于前台。此时,B 调用 doBusiness1() 发送了一条消息 MSG,A 中在 onReceive() 中收到消息,并调用 loadData() 加载数据。然后,B 处理完了业务,准备退出的时候发现其他数据发生了变化,所以又发了一条消息,然后 onReceive() 中收到消息,并调用 loadData(). 但此时发现 loading 为 true. 所以,我们后来对数据的修改没有体现到列表上面。

2、问题的原因

如果用上面的示例代码作为例子,那么出现问题的原因就是当 A 处于后台的时候。虽然调用了 loadData() 并且从网络中拿到了数据,但是调用 data.setValue() 方法的时候无法通知到 A 中。所以,loading = false 这一行无法被调用到。第二次发出通知的时候,一样调用到了 loadData(),但是因为此时 loading 为 true,所以并没有执行加载数据的操作。而当从 B 中完全回到 A 的时候,第一次加载的数据被 A 接收到。所以,列表中的数据是第一次加载时的数据,第二次加载事件丢失了。

解决这个问题的方法当然比较简单,可以当接收到事件的时候使用布尔变量监听,然后回到页面的时候发现数据发生变化再执行数据加载:

    // 类 A
    public class A extends Fragment {
    
        private boolean dataChanged;

        /**
         * On receive message.
         */
        public void onReceive() {
            dataChanged = true;
        }

        @Override
        public void onResume() {
            // ...
            if (dataChanged) {
                loadData();
            }
        }
    }

对于上面的问题,当我们调用了 setValue() 之后将调用到 LiveData 类的 setValue() 方法,

    @MainThread
    protected void setValue(T value) {
        assertMainThread("setValue");
        mVersion++;
        mData = value;
        dispatchingValue(null);
    }

这里表明该方法必须在主线程中被调用,最终事件的分发将会交给 dispatchingValue() 方法来执行:

    private void dispatchingValue(@Nullable ObserverWrapper initiator) {
        if (mDispatchingValue) {
            mDispatchInvalidated = true;
            return;
        }
        mDispatchingValue = true;
        do {
            mDispatchInvalidated = false;
            if (initiator != null) {
                considerNotify(initiator);
                initiator = null;
            } else {
                for (Iterator<Map.Entry<Observer<T>, ObserverWrapper>> iterator =
                        mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
                    // 发送事件
                    considerNotify(iterator.next().getValue());
                    if (mDispatchInvalidated) {
                        break;
                    }
                }
            }
        } while (mDispatchInvalidated);
        mDispatchingValue = false;
    }

然后,会调用 considerNotify() 方法来最终将事件传递出去,

    private void considerNotify(ObserverWrapper observer) {
        // 这里会因为当前的 Fragment 没有处于 active 状态而退出方法
        if (!observer.mActive) {
            return;
        }
        if (!observer.shouldBeActive()) {
            observer.activeStateChanged(false);
            return;
        }
        if (observer.mLastVersion >= mVersion) {
            return;
        }
        observer.mLastVersion = mVersion;
        observer.mObserver.onChanged((T) mData);
    }

这里会因为当前的 Fragment 没有处于 active 状态而退出 considerNotify() 方法,从而消息无法被传递出去。

3、LiveData 的通知机制

LiveData 的通知机制并不复杂,它的类主要包含在 livedata-core 包下面,总共也就 3 个类。LiveData 是一个抽象类,它有一个默认的实现就是 MutableLiveData.

LiveData 主要依靠内部的变量 mObservers 来缓存订阅的对象和订阅信息。其定义如下,使用了一个哈希表进行缓存和映射,

private SafeIterableMap<Observer<T>, ObserverWrapper> mObservers = new SafeIterableMap<>();

每当我们调用一次 observe() 方法的时候就会有一个映射关系被加入到哈希表中,

    public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<T> observer) {
        if (owner.getLifecycle().getCurrentState() == DESTROYED) {
            // 持有者当前处于被销毁状态,因此可以忽略此次观察
            return;
        }
        LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
        ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
        if (existing != null && !existing.isAttachedTo(owner)) {
            throw new IllegalArgumentException("Cannot add the same observer"
                    + " with different lifecycles");
        }
        if (existing != null) {
            return;
        }
        owner.getLifecycle().addObserver(wrapper);
    }

从上面的代码我们可以看出,添加到映射关系中的类会先被包装成 LifecycleBoundObserver 对象。然后使用该对象对 owner 的生命周期进行监听。

这的 LifecycleBoundObserverObserverWrapper 两个类的定义如下,

    class LifecycleBoundObserver extends ObserverWrapper implements GenericLifecycleObserver {
        @NonNull final LifecycleOwner mOwner;

        LifecycleBoundObserver(@NonNull LifecycleOwner owner, Observer<T> observer) {
            super(observer);
            mOwner = owner;
        }

        @Override
        boolean shouldBeActive() {
            return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED);
        }

        @Override
        public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
            if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
                removeObserver(mObserver);
                return;
            }
            activeStateChanged(shouldBeActive());
        }

        @Override
        boolean isAttachedTo(LifecycleOwner owner) {
            return mOwner == owner;
        }

        @Override
        void detachObserver() {
            mOwner.getLifecycle().removeObserver(this);
        }
    }

    private abstract class ObserverWrapper {
        final Observer<T> mObserver;
        boolean mActive;
        int mLastVersion = START_VERSION;

        ObserverWrapper(Observer<T> observer) {
            mObserver = observer;
        }

        abstract boolean shouldBeActive();

        boolean isAttachedTo(LifecycleOwner owner) {
            return false;
        }

        void detachObserver() {}

        void activeStateChanged(boolean newActive) {
            if (newActive == mActive) {
                return;
            }
            mActive = newActive;
            boolean wasInactive = LiveData.this.mActiveCount == 0;
            LiveData.this.mActiveCount += mActive ? 1 : -1;
            if (wasInactive && mActive) {
                onActive();
            }
            if (LiveData.this.mActiveCount == 0 && !mActive) {
                onInactive();
            }
            if (mActive) {
                dispatchingValue(this);
            }
        }
    }

上面的类中我们先来关注 LifecycleBoundObserver 中的 onStateChanged() 方法。该方法继承自 LifecycleObserver. 这里的 Lifecycle.Event 是一个枚举类型,定义了一些与生命周期相关的枚举值。所以,当 Activity 或者 Fragment 的生命周期发生变化的时候会回调这个方法。从上面我们也可以看出,该方法内部又调用了基类的 activeStateChanged() 方法,该方法主要用来更新当前的 Observer 是否处于 Active 的状态。我们上面无法通知也是因为在这个方法中 mActive 被置为 false 造成的。

继续看 activeStateChanged() 方法,我们可以看出在最后的几行中,它调用了 dispatchingValue(this) 方法。所以,当 Fragment 从处于后台切换到前台之后,会将当前缓存的值通知给观察者。

那么值是如何缓存的,以及缓存了多少值呢?回到之前的 setValue()dispatchingValue() 方法中,我们发现值是以一个单独的变量进行缓存的,

    private volatile Object mData = NOT_SET;

因此,在我们的示例中,当页面从后台切换到前台的时候,只能将最后一次缓存的结果通知给观察者就真相大白了。

总结

从上面的分析中,我们对 LiveData 总结如下,

  1. 当调用 observe() 方法的时候,我们的观察者将会和 LifecycleOwner (Fragment 或者 Activity) 一起被包装到一个类中,并使用哈希表建立映射关系。同时,还会对 Fragment 或者 Activity 的生命周期方法进行监听,依次来达到监听观察者是否处于 active 状态的目的。
  2. 当 Fragment 或者 Activity 处于后台的时候,其内部的观察者将处于非 active 状态,此时使用 setValue() 设置的值会缓存到 LiveData 中。但是这种缓存只能缓存一个值,新的值会替换旧的值。因此,当页面从后台恢复到前台的时候只有最后设置的一个值会被传递给观察者。
  3. 2 中的当 Fragment 或者 Activity 从后台恢复的时候进行通知也是通过监听其生命周期方法实现的。
  4. 调用了 observe() 之后,Fragment 或者 Activity 被缓存了起来,不会造成内存泄漏吗?答案是不会的。因为 LiveData 可以对其生命周期进行监听,当其处于销毁状态的时候,该映射关系将被从缓存中移除。

以上。

另外
有什么技术问题欢迎加我交流 qilebeaf
本人10多年大厂软件开发经验,精通Android,Java,Python,前端等开发,空余时间承接软件开发设计、课程设计指导、解决疑难bug、AI大模型搭建,AI绘图应用等。
欢迎砸单# Java 注解及其两种使用方法

一般的,注解在 Android 中有两种应用方式,一种方式是基于反射的,即在程序的运行期间获取类信息进行反射调用;另一种是使用注解处理,在编译期间生成许多代码,然后在运行期间通过调用这些代码来实现目标功能。

在本篇文章中,我们会先重温一下 Java 的注解相关的知识,然后分别介绍一下上面两种方式的实际应用。

1、Java 注解回顾

1. Java 注解的基础知识

Java 中的注解分成标准注解和元注解。标准注解是 Java 为我们提供的预定义的注解,共有四种:@Override@Deprecated@SuppressWarnnings@SafeVarags。元注解是用来提供给用户自定义注解用的,共有五种(截止到Java8):@Target@Retention@Documented@Inherited@Repeatable,这里我们重点介绍这五种元注解。

不过,首先我们还是先看一下一个基本的注解的定义的规范。下面我们自定义了一个名为UseCase的注解,可以看出我们用到了上面提及的几种元注解:

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target(value={METHOD, FIELD})
    public @interface UseCase {
        public int id();
        public String description() default "default value";
    }

这是一个普通的注解的定义。从上面我们也可以总结出,在定义注解的时候,有以下几个地方需要注意:

  1. 使用 @interface 声明并且指定注解的名称;
  2. 注解的定义类似于接口中的方法的定义,但要注意两者之间本质上是不同的;
  3. 可以通过 default 为指定的元素指定一个默认值,如果用户没有为其指定值,就使用默认值。

2. 元注解

好的,看完了一个基本的注解的定义,我们来看一下上面用到的 Java 元注解的含义。

@Target

@Target 用来指定注解能够修饰的对象的类型。因为 @Target 本身也是一个注解,所以你可以在源码中查看它的定义。该注解接收的参数是一个 ElementType 类型的数组,所以,就是说我们自定义的注解可以应用到多种类型的对象,而对象的类型由 ElementType 定义。ElementType 是一个枚举,它的枚举值如下:

  • TYPE:类、接口或者enum声明
  • FIELD:域声明,包括enum实例
  • METHOD:方法声明
  • PARAMETER:参数声明
  • CONSTRUCTOR:构造器声明
  • LOCAL_VARIABLE:局部变量声明
  • ANNOTATION_TYPE:注解声明
  • PACKAGE:包声明
  • TYPE_PARAMETER:类型参数声明
  • TYPE_USE:使用类型

所以,比如根据上面的内容,我们可以直到我们的自定义注解 @UseCase 只能应用于方法和字段。

@Retention

用来指定注解的保留策略,比如有一些注解,当你在自己的代码中使用它们的时候你会把它写在方法上面,但是当你反编译之后却发现这些注解不在了;而有些注解反编译之后依然存在,发生这种情况的原因就是在使用该注解的时候指定了不同的参数。

@Target 相同的是这个注解也使用枚举来指定值的类型,不同的是它只能指定一个值,具体可以看源码。这里它使用的是 RetentionPolicy 枚举,它的几个值的含义如下:

  • SOURCE:注解将被编译器丢弃
  • CLASS:注解在class文件中使用,但会被JVM丢弃
  • RUNTIME:VM将在运行期保留注解,故可以通过反射读取注解的信息

当我们在 Android 中使用注解的时候,一种是在运行时使用的,所以我们要用 RUNTIME;另一种是在编译时使用的,所以我们用 CLASS

@Documented、@Inherited 和 @Repeatable

这三个元注解的功能比较简单和容易理解,这里我们一起给出即可:

  • @Documented 表示此注解将包含在 javadoc 中;
  • @Inherited 表示允许子类继承父类的注解;
  • @Repeatable 是 Java8 中新增的注解,表示指定的注解可以重复应用到指定的对象上面。

上文,我们回顾了 Java 中注解相关的知识点,相信你已经对注解的内容有了一些了解,那么我们接下来看一下注解在实际开发中的两种应用方式。

2、注解的两种使用方式

在我开始为我的开源项目 马克笔记 编写数据库的时候,我考虑了使用注解来为数据库对象指定字段的信息,并根据这心信息来拼接出创建数据库表的 SQL 语句。当时也想用反射来动态为每个字段赋值的,但是考虑到反射的性能比较差,最终放弃了这个方案。但是,使用注解处理的方式可以完美的解决我们的问题,即在编译的时候动态生成一堆代码,实际赋值的时候调用这些方法来完成。这前后两种方案就是我们今天要讲的注解的两种使用方式。

2.1 基于反射使用注解

这里为了演示基于反射的注解的使用方式,我们写一个小的 Java 程序,要实现的目的是:定义两个个注解,一个应用于方法,一个应用于字段,然后我们使用这两个注解来定义一个类。我们想要在代码中动态地打印出使用了注解的方法和字段的信息和注解信息。

这里我们先定义两个注解,应用于字段的 @Column 注解和应用于方法 @Important 注解:

    @Target(value = {ElementType.FIELD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Column {
        String name();
    }

    @Target(value = {ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface WrappedMethod {
        // empty
    }

然后我们定义了一个Person类,并使用注解为其中的部分方法和字段添加注解:

    private static class Person {

        @Column(name = "id")
        private int id;

        @Column(name = "first_name")
        private String firstName;

        @Column(name = "last_name")
        private String lastName;

        private int temp;

        @WrappedMethod()
        public String getInfo() {
            return id + " :" + firstName + " " + lastName;
        }

        public String method() {
            return "Nothing";
        }
    }

然后,我们使用Person类来获取该类的字段和方法的信息,并输出具有注解的部分:

    public static void main(String...args) {
        Class<?> c = Person.class;
        Method[] methods = c.getDeclaredMethods();
        for (Method method : methods) {
            if (method.getAnnotation(WrappedMethod.class) != null) {
                System.out.print(method.getName() + " ");
            }
        }
        System.out.println();
        Field[] fields = c.getDeclaredFields();
        for (Field field : fields) {
            Column column = field.getAnnotation(Column.class);
            if (column != null) {
                System.out.print(column.name() + "-" + field.getName() + ", ");
            }
        }
    }

输出结果:

getInfo
id-id, first_name-firstName, last_name-lastName, 

在上面的代码的执行结果,我们可以看出:使用了注解和反射之后,我们成功的打印出了使用了注解的字段。这里我们需要先获取指定的类的 Class 类型,然后用反射获取它的所有方法和字段信息并进行遍历,通过判断它们的 getAnnotation() 方法的结果来确定这个方法和字段是否使用了指定类型的注解。

上面的代码可以解决一些问题,但同时,我们还有一些地方需要注意:

  1. 如果指定的方法或者字段名被混淆了怎么办? 对于一些可以自定义名称的情景,我们可以在注解中加入参数为该字段或者方法指定一个名称;
  2. 上面使用了很多的反射,这会影响程序的性能吗? 使用注解的方式肯定性能不会高,但是如果注解的使用没有那么频繁,上面方法不会有特别大的性能损耗,比如拼接 SQL 这样的操作,可能只需要执行一次。不过,根本的解决办法是使用注解的第二种使用方式!

2.2 基于 annotationProcessor 使用注解

也许你之前已经使用过 ButterKnife 这样的注入框架,不知道你是否记得在 Gradle 中引用它的时候加入了下面这行依赖:

    annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'

这里的 annotationProcessor 就是我们这里要讲的注解处理。本质上它会在编译的时候,在你调用 ButterKnife.bind(this); 方法的那个类所在的包下面生成一些类,当调用 ButterKnife.bind(this); 的时候实际上就完成了为使用注解的方法和控件绑定的过程。也就是,本质上还是调用了 findViewById(),只是这个过程被隐藏了,不用你来完成了,仅此而已。

下面,我们就使用注解处理的功能来制作一个类似于 ButterKnife 的简单库。不过,在那之前我们还需要做一些准备——一些知识点需要进行说明。即 Javapoet和AbstractProcessor

Javapoet & AbstractProcessor

Javapoet 是一个用来生成 .java 文件的 Java API,由 Square 开发,你可以在它的 Github 主页中了解它的基本使用方法。它的好处就是对方法、类文件和代码等的拼接进行了封装,有了它,我们就不用再按照字符串的方式去拼接出一段代码了。相比于直接使用字符串的方式,它还可以生成代码的同时直接 import 对应的引用,可以说是非常方便、快捷的一个库了。

这里的 AbstractProcessor 是用来生成类文件的核心类,它是一个抽象类,一般使用的时候我们只要覆写它的方法中的4个就可以了。下面是这些方法及其定义:

  1. init:在生成代码之前被调用,可以从它参数 ProcessingEnvironment 获取到非常多有用的工具类;
  2. process:用于生成代码的 Java 方法,可以从参数 RoundEnvironment 中获取使用指定的注解的对象的信息,并包装成一个 Element 类型返回;
  3. getSupportedAnnotationTypes:用于指定该处理器适用的注解;
  4. getSupportedSourceVersion:用来指定你使用的 Java 的版本。

这几个方法中,除了 process,其他方法都不是必须覆写的方法。这里的 getSupportedAnnotationTypesgetSupportedSourceVersion 可以使用注 @SupportedAnnotationTypes@SupportedSourceVersion 来替换,但是不建议这么做。因为前面的注解接收的参数是字符串,如果你使用了混淆可能就比较麻烦,后面的注解只能使用枚举,相对欠缺了灵活性。

另一个我们需要特别说明的地方是,继承 AbstractProcessor 并实现了我们自己的处理器之后还要对它进行注册才能使用。一种做法是在与 java 同的目录下面创建一个 resources 文件夹,并在其中创建 META-INF/service 文件夹,然后在其中创建一个名为javax.annotation.processing.Processor 的文件,并在其中写上我们的处理器的完整路径。另一种做法是使用谷歌的 @AutoService 注解,你只需要在自己的处理器上面加上 @AutoService(Processor.class) 一行代码即可。当然,前提是你需要在自己的项目中引入依赖:

    compile 'com.google.auto.service:auto-service:1.0-rc2'

按照后面的这种方式一样会在目录下面生成上面的那个文件,只是这个过程不需要我们来操作了。你可以通过查看buidl出的文件来找到生成的文件。

MyKnife 的最终结果

在定制之前,我们先看一下程序的最终执行结果,也许这样会更有助于理解整个过程的原理。我们程序的最终的执行结果是,在编译的时候,在使用我们的工具的类的相同级别的包下面生成一个类。如下图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这里的 me.shouheng.libraries 是我们应用 MyKnife 的包,这里我们在它下面生成了一个名为 MyKnifeActivity$$Injector 的类,它的定义如下:

    public class MyKnifeActivity$$Injector implements Injector<MyKnifeActivity> {
      @Override
      public void inject(final MyKnifeActivity host, Object source, Finder finder) {
        host.textView=(TextView)finder.findView(source, 2131230952);
        View.OnClickListener listener;
        listener = new View.OnClickListener() {
          @Override
          public void onClick(View view) {
            host.OnClick();
          }
        };
        finder.findView(source, 2131230762).setOnClickListener(listener);
      }
    }

因为我们应用 MyKnife 的类是 MyKnifeActivity,所以这里就生成了名为 MyKnifeActivity$$Injector 的类。通过上面的代码,可以看出它实际上调用了 Finder 的方法来为我们的控件 textView 赋值,然后使用控件的 setOnClickListener() 方法为点击事件赋值。这里的 Finder 是我们封装的一个对象,用来从指定的源中获取控件的类,本质上还是调用了指定源的 findViewById() 方法。

然后,与 ButterKnife 类似的是,在使用我们的工具的时候,也需要在 Activity 的 onCreate() 中调用 bind() 方法。这里我们看下这个方法做了什么操作:

    public static void bind(Object host, Object source, Finder finder) {
        String className = host.getClass().getName();
        try {
            Injector injector = FINDER_MAPPER.get(className);
            if (injector == null) {
                Class<?> finderClass = Class.forName(className + "$$Injector");
                injector = (Injector) finderClass.newInstance();
                FINDER_MAPPER.put(className, injector);
            }
            injector.inject(host, source, finder);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
    }

从上面的代码中可以看出,调用 bind() 方法的时候会从 FINDER_MAPPER 尝试获取指定 类名$$Injector 的文件。所以,如果说我们应用 bind() 的类是 MyKnifeActivity,那么这里获取到的类将会是 MyKnifeActivity$$Injector。然后,当我们调用 inject 方法的时候就执行了我们上面的注入操作,来完成对控件和点击事件的赋值。这里的 FINDER_MAPPER 是一个哈希表,用来缓存指定的 Injector 的。所以,从上面也可以看出,这里进行值绑定的时候使用了反射,所以,在应用框架的时候还需要对混淆进行处理。

OK,看完了程序的最终结果,我们来看一下如何生成上面的那个类文件。

API 和注解的定义

首先,我们需要定义注解用来提供给用户进行事件和控件的绑定,

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

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

如上面的代码所示,可以看出我们分别用了 ElementType.FIELDElementType.METHOD 指定它们是应用于字段和方法的,然后用了 RetentionPolicy.CLASS 标明它们不会被保留到程序运行时。

然后,我们需要定义 MyKnife,它提供了一个 bind() 方法,其定义如下:

    public static void bind(Object host, Object source, Finder finder) {
        String className = host.getClass().getName();
        try {
            Injector injector = FINDER_MAPPER.get(className);
            if (injector == null) {
                Class<?> finderClass = Class.forName(className + "$$Injector");
                injector = (Injector) finderClass.newInstance();
                FINDER_MAPPER.put(className, injector);
            }
            injector.inject(host, source, finder);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
    }

这里的三个参数的含义分别是:host 是调用绑定方法的类,比如 Activity 等;source是从用来获取绑定的值的数据源,一般理解是从 source 中获取控件赋值给 host 中的字段,通常两者是相同的;最后一个参数 finder 是一个接口,是获取数据的方法的一个封装,有两默认的实现,一个是 ActivityFinder,一个是 ViewFinder,分别用来从 Activity 和 View 中查找控件。

我们之前已经讲过 bind() 方法的作用,即使用反射根据类名来获取一个 Injector,然后调用它的 inject() 方法进行注入。这里的 Injector 是一个接口,我们不会写代码去实现它,而是在编译的时候让编译器直接生成它的实现类。

代码的生成过程

在介绍 Javapoet 和 AbstractProcessor 的时候,我们提到过 Element,它封装了应用注解的对象(方法、字段或者类等)的信息。我们可以从 Element 中获取这些信息并将它们封装成一个对象来方便我们调用。于是就产生了 BindViewFieldOnClickMethod 两个类。它们分别用来描述使用 @BindView 注解和使用 @OnClick 注解的对象的信息。此外,还有一个 AnnotatedClass,它用来描述使用注解的整个类的信息,并且其中定义了List<BindViewField>List<OnClickMethod>,分别用来存储该类中应用注解的字段和方法的信息。

与生成文件和获取注解的对象信息相关的几个字段都是从 AbstractProcessor 中获取的。如下面的代码所示,我们可以从 AbstractProcessor 的 init() 方法的 ProcessingEnvironment 中获取到 ElementsFilerMessager。它们的作用分别是:Elements 类似于一个工具类,用来从 Element 中获取注解对象的信息;Filer 用来支持通过注释处理器创建新文件;Messager 提供注释处理器用来报告错误消息、警告和其他通知的方式。

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        elements = processingEnvironment.getElementUtils();
        messager = processingEnvironment.getMessager();
        filer = processingEnvironment.getFiler();
    }

然后在 AbstractProcessor 的 process() 方法中的 RoundEnvironment 参数中,我们又可以获取到指定注解对应的 Element 信息。代码如下所示:

    private Map<String, AnnotatedClass> map = new HashMap<>();

    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        map.clear();
        try {
            // 分别用来处理我们定义的两种注解
            processBindView(roundEnvironment);
            processOnClick(roundEnvironment);
        } catch (IllegalArgumentException e) {
            return true;
        }

        try {
            // 为缓存的各个使用注解的类生成类文件
            for (AnnotatedClass annotatedClass : map.values()) {
                annotatedClass.generateFinder().writeTo(filer);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return true;
    }

    // 从RoundEnvironment中获取@BindView注解的信息
    private void processBindView(RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getElementsAnnotatedWith(BindView.class)) {
            AnnotatedClass annotatedClass = getAnnotatedClass(element);
            BindViewField field = new BindViewField(element);
            annotatedClass.addField(field);
        }
    }

    // 从RoundEnvironment中获取@OnClick注解的信息
    private void processOnClick(RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getElementsAnnotatedWith(OnClick.class)) {
            AnnotatedClass annotatedClass = getAnnotatedClass(element);
            OnClickMethod method = new OnClickMethod(element);
            annotatedClass.addMethod(method);
        }
    }

    // 获取使用注解的类的信息,先尝试从缓存中获取,缓存中没有的话就实例化一个并放进缓存中
    private AnnotatedClass getAnnotatedClass(Element element) {
        TypeElement encloseElement = (TypeElement) element.getEnclosingElement();
        String fullClassName = encloseElement.getQualifiedName().toString();
        AnnotatedClass annotatedClass = map.get(fullClassName);
        if (annotatedClass == null) {
            annotatedClass = new AnnotatedClass(encloseElement, elements);
            map.put(fullClassName, annotatedClass);
        }
        return annotatedClass;
    }

上面的代码的逻辑是,在调用 process() 方法的时候,会根据传入的 RoundEnvironment 分别处理两种注解。两个注解的相关信息都会被解析成 List<BindViewField>List<OnClickMethod>,然后把使用注解的整个类的信息统一放置在 AnnotatedClass 中。为了提升程序的效率,这里用了缓存来存储类信息。最后,我们调用了 annotatedClass.generateFinder() 获取一个JavaFile,并调用它的 writeTo(filer) 方法生成类文件。

上面的代码重点在于解析使用注解的类的信息,至于如何根据类信息生成类文件,我们还需要看下 AnnotatedClassgenerateFinder() 方法,其代码如下所示。这里我们用了之前提到的 Javapoet 来帮助我们生成类文件:

    public JavaFile generateFinder() {
        // 这里用来定义inject方法的签名
        MethodSpec.Builder builder = MethodSpec.methodBuilder("inject")
                .addModifiers(Modifier.PUBLIC)
                .addAnnotation(Override.class)
                .addParameter(TypeName.get(typeElement.asType()), "host", Modifier.FINAL)
                .addParameter(TypeName.OBJECT, "source")
                .addParameter(TypeUtils.FINDER, "finder");
        // 这里用来定义inject方法中@BindView注解的绑定过程
        for (BindViewField field : bindViewFields) {
            builder.addStatement("host.$N=($T)finder.findView(source, $L)",
                    field.getFieldName(),
                    ClassName.get(field.getFieldType()),
                    field.getViewId());
        }
        // 这里用来定义inject方法中@OnClick注解的绑定过程
        if (onClickMethods.size() > 0) {
            builder.addStatement("$T listener", TypeUtils.ONCLICK_LISTENER);
        }
        for (OnClickMethod method : onClickMethods) {
            TypeSpec listener = TypeSpec.anonymousClassBuilder("")
                    .addSuperinterface(TypeUtils.ONCLICK_LISTENER)
                    .addMethod(MethodSpec.methodBuilder("onClick")
                            .addAnnotation(Override.class)
                            .addModifiers(Modifier.PUBLIC)
                            .returns(TypeName.VOID)
                            .addParameter(TypeUtils.ANDROID_VIEW, "view")
                            .addStatement("host.$N()", method.getMethodName())
                            .build())
                    .build();
            builder.addStatement("listener = $L", listener);
            for (int id : method.getIds()) {
                builder.addStatement("finder.findView(source, $L).setOnClickListener(listener)", id);
            }
        }
        // 这里用来获取要生成的类所在的包的信息
        String packageName = getPackageName(typeElement);
        String className = getClassName(typeElement, packageName);
        ClassName bindClassName = ClassName.get(packageName, className);

        // 用来最终组装成我们要输出的类
        TypeSpec finderClass = TypeSpec.classBuilder(bindClassName.simpleName() + "$$Injector")
                .addModifiers(Modifier.PUBLIC)
                .addSuperinterface(ParameterizedTypeName.get(TypeUtils.INJECTOR, TypeName.get(typeElement.asType())))
                .addMethod(builder.build())
                .build();
        return JavaFile.builder(packageName, finderClass).build();
    }

上面就是我们用来最终生成类文件的方法,这里用了 Javapoet ,如果对它不是很了解可以到 Github 上面了解一下它的用法。

这样我们就完成了整个方法的定义。

使用 MyKnife

使用我们定义的 MyKnife ,我们只需要在 Gradle 里面引入我们的包即可:

    implementation project(':knife-api')
    implementation project(':knife-annotation')
    annotationProcessor project(':knife-compiler')

也许你在有的地方看到过要使用 android-apt 引入注解处理器,其实这里的annotationProcessor 与之作用是一样的。这里推荐使用 annotationProcessor,因为它更加简洁,不需要额外的配置,也是官方推荐的使用方式。

然后,我们只需要在代码中使用它们就可以了:

public class MyKnifeActivity extends CommonActivity<ActivityMyKnifeBinding> {

    @BindView(id = R.id.tv)
    public TextView textView;

    @OnClick(ids = {R.id.btn})
    public void OnClick() {
        ToastUtils.makeToast("OnClick");
    }

    @Override
    protected int getLayoutResId() {
        return R.layout.activity_my_knife;
    }

    @Override
    protected void doCreateView(Bundle savedInstanceState) {
        MyKnife.bind(this);
        textView.setText("This is MyKnife demo!");
    }
}

这里有几个地方需要注意:

  1. 使用注解的方法和字段需要至少是 protected,因为我们使用了直接引用的方式,而生成的文件和上面的类包相同,所以至少应该保证包级别访问权限;
  2. 上面使用注解的方式只能在当前 Module 作为 application 的时候使用,作为 library 的时候无法使用,这是因为只有当 Module 作为 application 的时候,R文件中的 id 是 final 的,作为 library 的时候是非 final 的。
总结

这里我们总结一下按照第二种方式使用注解的时候需要步骤:

  1. 首先,我们需要按照自己的需要考虑如何定义注解。
  2. 然后,我们需要实现 AbstractProcessor ,覆写各个方法,注册,并在 process 方法中完成生成类文件的操作。

3、总结

以上就是注解的两种比较常见的使用方式。第一种是通过反射来进行的,因为反射本身的效率比较低,所以比较适用于发射比较少的场景;第二种方式是在编译期间通过编译器生成代码来实现的,相比于第一种,它还是可能会用到反射的,但是不必在运行时对类的每个方法和字段进行遍历,因而效率高得多。

以上。

获取源码:Android-references

另外
有什么技术问题欢迎加我交流 qilebeaf
本人10多年大厂软件开发经验,精通Android,Java,Python,前端等开发,空余时间承接软件开发设计、课程设计指导、解决疑难bug、AI大模型搭建,AI绘图应用等。
欢迎砸单

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1870284.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

AI绘图软件:设计师的创意加速器

在人工智能的浪潮中&#xff0c;AI绘图软件工具已成为设计师和创意工作者的得力助手&#xff0c;它们不仅加速了复杂绘图任务的完成&#xff0c;还激发了无限创意。本文将为您介绍几款AI绘图软件工具&#xff0c;它如何成为提升工作效率和创意灵感的关键。 1. StartAI&#xf…

ISO37001认证:防范贿赂风险的强大工具

随着全球反贿赂法规的日益严格&#xff0c;各类组织面临的贿赂风险和合规压力不断增加。ISO37001反贿赂管理体系认证应运而生&#xff0c;为组织提供了一个系统化的管理框架&#xff0c;帮助其有效发现、预防和管控贿赂风险。本文将详细探讨ISO37001认证的作用和意义&#xff0…

小白学python(第三天)

小伙伴&#xff0c;大家好呀&#xff0c;昨天的内容吸收的好&#xff1f;昨天有小伙伴私信我&#xff0c;建议我在博文中加点练习题&#xff0c;可以看出这位童鞋很想学好这门语言哈&#xff0c;那我也尽量满足大家的要求。 从控制台输入 语法格式&#xff1a; 变量名 input…

神经网络学习笔记9-简单的反向传播和线性回归

tensor在pytorch中是一个非常重要的类型 假如需要计算梯度&#xff0c;就将tensor中 requires_grad设为true loss是一个张量&#xff0c;在做运算时构建运算图&#xff0c;因此不要直接进行&#xff0c;会将将梯度存入w&#xff0c;当反向传播完成 后&#xff0c;该计算图会…

C# 23设计模式备忘

创建型模式&#xff1a;单例&#xff08;Singleton&#xff09;模式&#xff1a;某个类只能生成一个实例&#xff0c;该类提供了一个全局访问点供外部获取该实例&#xff0c;其拓展是有限多例模式。 原型&#xff08;Prototype&#xff09;模式&#xff1a;将一个对象作为原型&…

地铁中的CAN通信--地铁高效安全运转原理

目前地铁采用了自动化的技术来实现控制,有ATC(列车自动控制)系统可以实现列车自动驾驶、自动跟踪、自动调度;SCADA(供电系统管理自动化)系统可以实现主变电所、牵引变电所、降压变电所设备系统的遥控、遥信、遥测;BAS(环境监控系统)和FAS(火灾报警系统)可以实现车站…

IDEA版本推荐

推荐版本&#xff1a; IDEA 2024.1.4 下载链接&#xff1a;IDEA下载 &#xff08;下载时可以往下拖&#xff0c;选到自己想要的版本哦&#xff09; 本人由于项目开发需要&#xff0c;陆续用过几个版本的IDEA&#xff0c;包括&#xff1a; IDEA 2020.2.4 。这是在看韩顺平老师…

六西格玛绿带可以跳过,直接学六西格玛黑带吗?真实情况告诉你

在现代企业管理中&#xff0c;六西格玛&#xff08;Six Sigma&#xff09;已经成为提升质量和效率的重要工具。对于很多企业而言&#xff0c;培养内部的六西格玛专家&#xff0c;特别是黑带&#xff08;Black Belt&#xff09;&#xff0c;是推动持续改进的关键。然而&#xff…

十常侍乱政 | 第2集 | 愿领精兵五千,斩关入内,册立新君,诛杀宦党,扫清朝廷,以安天下 | 三国演义 | 逐鹿群雄

&#x1f64b;大家好&#xff01;我是毛毛张! &#x1f308;个人首页&#xff1a; 神马都会亿点点的毛毛张 &#x1f4cc;这篇博客是毛毛张分享三国演义文学剧本中的经典台词和语句&#xff0c;本篇分享的是《三国演义》第Ⅰ部分《群雄逐鹿》的第2️⃣集《十常侍乱政治》&am…

代码随想录算法训练营第三十六天|62.不同路径、 63. 不同路径 II、343.整数拆分(可跳过)、96.不同的二叉搜索树(可跳过)

62.不同路径 题目链接&#xff1a;62.不同路径 文档讲解&#xff1a;代码随想录 状态&#xff1a;还行 思路&#xff1a;当前状态的只有可能是从上面或者左边过来的&#xff0c;所以 dp[i][j] dp[i-1] dp[j-1] 题解&#xff1a; public int uniquePaths(int m, int n) {if (…

Android Kotlin 中的闭包函数

闭包函数是现代编程语言中一个重要的概念&#xff0c;Kotlin 作为一种现代的 JVM 语言&#xff0c;自然也支持闭包函数。本文将详细介绍闭包函数的概念、在Kotlin 中的使用方法&#xff0c;以及一些常见的应用场景。 什么是闭包函数&#xff1f; 闭包函数&#xff0c;也称为闭…

50etf期权合约一手多少钱你知道吗?

今天带你了解50etf期权合约一手多少钱你知道吗&#xff1f;50etf期权有不同价值的合约&#xff0c;每手50etf期权合约从几元到几百元再到上千元的都有&#xff0c;具体需要根据投资者选择了什么价值的合约。 50etf期权权利金 50ETF期权合约的权利金是买方需要缴纳的费用&…

上古世纪战争台服官网地址+台服预约+预创建角色教程

上古世纪战争台服上线啦&#xff0c;在《上古世纪战争》中&#xff0c;通过主要势力和地区&#xff0c;剧情和角色可以想起原作。《上古世纪战争》的主要背景为&#xff0c;原大陆消失之后&#xff0c;完成移民的种族们定居在诺伊大陆之后遇到的多个势力之间的冲突。同时&#…

视觉灵感的探索和分享平台

做设计没灵感&#xff1f;大脑一片空白&#xff1f;灵感是创作的源泉&#xff0c;也是作品的灵魂所在。工作中缺少灵感&#xff0c;这是每个设计师都会经历的苦恼&#xff0c;那当我们灵感匮乏的时候&#xff0c;该怎么办呢&#xff1f;别急&#xff0c;即时设计、SurfCG、Lapa…

ulimit报错

问题 执行命令“ulimit -c 2048 ”时报错&#xff1a;“bash: ulimit: core file size: cannot modify limit: Operation not permitted” 原因 权限不够。 解决办法 执行命令“sudo gedit /etc/security/limits.conf”打开文件limits.conf文件内添加内容如下&#xff1a;…

Dataease配置Nginx代理

Dataease配置Nginx代理 一.修改前端静态资源地址和后端接口地址 **1.**修改应用程序的上下文路径 配置文件地址&#xff1a;backend/src/main/resources 找到文件application-whole.properties&#xff0c;做如下修改&#xff1a; **2.**修改前端静态资源路径和打包配置 配…

Linux创建目录——mkdir命令,du命令,touch用法,创建tree拓扑图

1. mkdir 命令 格式 mkdir - 参数 路径 / 目录名 参数 -p &#xff1a;快速创建多级目录&#xff08;递归目录&#xff09; -v &#xff1a;显示创建目录的详细过程 例&#xff1a; [rootserver ~] # mkdir t1 [rootserver ~] # mkdir t2 t3 t4 [rootserver ~] # mk…

[数据集][目标检测]电缆钢丝绳线缆缺陷检测数据集VOC+YOLO格式1800张3类别

数据集格式&#xff1a;Pascal VOC格式YOLO格式(不包含分割路径的txt文件&#xff0c;仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数)&#xff1a;1800 标注数量(xml文件个数)&#xff1a;1800 标注数量(txt文件个数)&#xff1a;1800 标注…

Python学习打卡:day17

day17 笔记来源于&#xff1a;黑马程序员python教程&#xff0c;8天python从入门到精通&#xff0c;学python看这套就够了 目录 day17121、Python 操作 MySQL 基础使用pymysql创建到 MySQL 的数据库链接执行 SQL 语句执行非查询性质的SQL语句执行查询性质的SQL语句 122、Pyth…

Python实践项目讲解:如何用制作一个桌面宠物

制作一个桌面宠物&#xff08;Desktop Pet&#xff09;在Python中通常涉及多个步骤&#xff0c;包括创建宠物的图形界面、添加动画效果、处理用户交互等。下面是一个简化的步骤指南&#xff0c;帮助你开始使用Python制作桌面宠物&#xff1a; 选择图形库&#xff1a; Tkinter&…