Android DataBinding 全面解析【源码篇2】

目录

一、可观察数据容器

[1.1 可观察字段(ObservableFields)](#1.1 可观察字段(ObservableFields))

继承关系:

成员变量和核心方法:

[问题1: 为什么字段必须是 public final ?](#问题1: 为什么字段必须是 public final ?)

[问题2:set() 方法中 notifyChange() 干了什么](#问题2:set() 方法中 notifyChange() 干了什么)

完整流程:

[1.2 可观察字段(ObservableInt 等)](#1.2 可观察字段(ObservableInt 等))

[ObservableBoolean vs ObservableField](#ObservableBoolean vs ObservableField)

[问题1:为什么 ObservableField 不同时实现 Parcelable?](#问题1:为什么 ObservableField 不同时实现 Parcelable?)

[1.3 可观察对象 (BaseObservable)](#1.3 可观察对象 (BaseObservable))

[问题1:@Bindable 注解干了什么?](#问题1:@Bindable 注解干了什么?)

[1.4 可观察集合(ObservableArrayMap/ObservableArrayList)](#1.4 可观察集合(ObservableArrayMap/ObservableArrayList))

ObservableArrayMap:

ObservableArrayList:

二、可观察数据容器监听的绑定

[2.1 ObservableFields](#2.1 ObservableFields)

[2.2 BaseObservable](#2.2 BaseObservable)

[2.3 updateRegistration() 方法](#2.3 updateRegistration() 方法)

三、绑定类型

[3.1 单向绑定 语法:@{}](#3.1 单向绑定 语法:@{})

[3.1.1 基础使用示例](#3.1.1 基础使用示例)

[2.1.2 源码分析](#2.1.2 源码分析)

[3.2 双向绑定 语法:@={}](#3.2 双向绑定 语法:@={})

[3.2.1 基础使用示例](#3.2.1 基础使用示例)

[3.2.2 源码分析](#3.2.2 源码分析)

[3.3 事件绑定](#3.3 事件绑定)

[3.3.1 基本使用示例](#3.3.1 基本使用示例)

[3.3.2 源码分析](#3.3.2 源码分析)

四、注解

[4.1 @BindingAdapter](#4.1 @BindingAdapter)

[4.1.1 注解定义源码](#4.1.1 注解定义源码)

[4.1.2 编译时注解处理](#4.1.2 编译时注解处理)

[4.2 @BindingConversion](#4.2 @BindingConversion)


系列入口导航:Android Jetpack 概述

上一篇文章我们从源码的角度大致分析了 DataBinding 初始化流程,接下来我们聊聊【使用篇】对应功能剩下没说完的。

一、可观察数据容器

1.1 可观察字段(ObservableFields)

ObservableField 是一个可观察的对象包装器,用于在数据绑定中实现 UI 自动更新当字段值发生变化时,会自动通知绑定的 UI 组件刷新 ,常常在 ViewModel 中使用。

使用方式如下:

java 复制代码
// public final 修饰,泛型指定类型
public final ObservableField<String> name = new ObservableField<>();
public final ObservableInt age = new ObservableInt(); // 避免装箱

为什么需要使用 public final来修饰,接下来我们看看源码。

ObservableFields 源码

java 复制代码
package androidx.databinding;

import androidx.annotation.Nullable;

import java.io.Serializable;

/**
 * An object wrapper to make it observable.
 * <p>
 * Observable field classes may be used instead of creating an Observable object. It can also
 * create a calculated field, depending on other fields:
 * <pre><code>public class MyDataObject {
 *     private Context context;
 *     public final ObservableField&lt;String&gt; first = new ObservableField&lt;String&gt;();
 *     public final ObservableField&lt;String&gt; last = new ObservableField&lt;String&gt;();
 *     public final ObservableField&lt;String&gt; display =
 *         new ObservableField&lt;String&gt;(first, last) {
 *             &#64;Override
 *             public String get() {
 *                 return context.getResources().getString(R.string.name, first.get, last.get());
 *             }
 *         };
 *     public final ObservableInt age = new ObservableInt();
 * }</code></pre>
 * Fields of this type should be declared final because bindings only detect changes in the
 * field's value, not of the field itself.
 *
 * @param <T> The type parameter for the actual object.
 * @see ObservableParcelable
 */
public class ObservableField<T> extends BaseObservableField implements Serializable {
    static final long serialVersionUID = 1L;
    private T mValue;

    /**
     * Wraps the given object and creates an observable object
     *
     * @param value The value to be wrapped as an observable.
     */
    public ObservableField(T value) {
        mValue = value;
    }

    /**
     * Creates an empty observable object
     */
    public ObservableField() {
    }

    /**
     * Creates an ObservableField that depends on {@code dependencies}. Typically,
     * ObservableFields are passed as dependencies. When any dependency
     * notifies changes, this ObservableField also notifies a change.
     *
     * @param dependencies The Observables that this ObservableField depends on.
     */
    public ObservableField(Observable... dependencies) {
        super(dependencies);
    }

    /**
     * @return the stored value.
     */
    @Nullable
    public T get() {
        return mValue;
    }

    /**
     * Set the stored value.
     *
     * @param value The new value
     */
    public void set(T value) {
        if (value != mValue) {
            mValue = value;
            notifyChange();
        }
    }
}

继承关系:

java 复制代码
public class ObservableField<T> extends BaseObservableField implements Serializable
  • 继承 BaseObservableField:提供依赖管理通知机制
  • 实现 Serializable:支持序列化,可在 Intent 或 Bundle 中传递

成员变量和核心方法:

java 复制代码
private T mValue;  // 存储实际数据
static final long serialVersionUID = 1L;  // 序列化版本ID

//get()方法
@Nullable
public T get() {
    return mValue;
}


//set()方法
public void set(T value) {
    if (value != mValue) {  // 引用比较
        mValue = value;
        notifyChange();      // 触发通知
    }
}

关注到 set() 其实比较的泛型的引用,如果引用没变但内部状态变了,ObservableField 不会触发通知,UI 也不会更新String 类型的指向的是字符串常量池,所以没有影响。但是假如是下面的情况,UI就不会自动发生变化。

java 复制代码
ObservableField<User> field = new ObservableField<>();
User user = new User("张三", 20);
field.set(user);  // field 持有 user 对象的引用

// 修改内部属性
user.name = "李四";
field.get().name = "李四";  // 同样效果

// set() 方法内部
// value != mValue ? 
// user 引用没变,所以 value == mValue
// 不调用 notifyChange()
// UI 不更新 ❌

问题1: 为什么字段必须是 public final ?

这是 DataBinding 最容易被误解的地方。看源码注释:

bash 复制代码
Fields of this type should be declared final 
because bindings only detect changes in the field's value, 
not of the field itself.


"此类型的字段应声明为 final,
因为绑定只检测字段值(指字段所引用的对象)的变化,
而不检测字段本身(指字段的引用)的变化。"

核心原因:DataBinding 监听的是"对象内部的变化",而不是"字段引用的变化"。

java 复制代码
// ✅ 正确做法:声明为 final
public final ObservableField<String> name = new ObservableField<>("Alice");

// ❌ 错误做法:非 final
public ObservableField<String> name = new ObservableField<>("Alice");

DataBinding 在编译时会生成绑定代码(如 ActivityMainBindingImpl),这些代码会在 executeBindings() 方法直接引用你的字段

java 复制代码
// 生成的绑定代码(简化版)
@Override
protected void executeBindings() {
    // 直接读取 mViewModel.name 这个 ObservableField 对象
    ObservableField<String> nameField = mViewModel.name;
    // 然后注册监听器到 nameField 上
    if (nameField != null) {
        nameField.addOnPropertyChangedCallback(...);
    }
}

如果 name 字段不是 final ,你可能在运行时把它替换成另一个 ObservableField 对象

java 复制代码
// 假设 name 不是 final
public ObservableField<String> name = new ObservableField<>("Alice");

// 后来你做了这样的操作
viewModel.name = new ObservableField<>("Bob"); // 整个对象被替换了!

这时问题就来了:

  • DataBinding 在初始化时监听了旧的 ObservableField 对象

  • 你把字段指向了新的 ObservableField 对象

  • 但 DataBinding 还持有旧对象的监听,完全不知道新对象的存在

  • UI 不会更新,出现诡异 bug

结论final 确保了字段引用永不改变,DataBinding 可以安全地监听这个固定的 ObservableField 实例。

问题2:set() 方法中 notifyChange() 干了什么

研究这个问题之前,我们先跳过 BaseObservableField 来看看BaseObservable

java 复制代码
package androidx.databinding;

import androidx.annotation.NonNull;

/**
 * A convenience class that implements {@link android.databinding.Observable} interface and provides
 * {@link #notifyPropertyChanged(int)} and {@link #notifyChange} methods.
 */
public class BaseObservable implements Observable {
    private transient PropertyChangeRegistry mCallbacks;

    public BaseObservable() {
    }

    @Override
    public void addOnPropertyChangedCallback(@NonNull OnPropertyChangedCallback callback) {
        synchronized (this) {
            if (mCallbacks == null) {
                mCallbacks = new PropertyChangeRegistry();
            }
        }
        mCallbacks.add(callback);
    }

    @Override
    public void removeOnPropertyChangedCallback(@NonNull OnPropertyChangedCallback callback) {
        synchronized (this) {
            if (mCallbacks == null) {
                return;
            }
        }
        mCallbacks.remove(callback);
    }

    /**
     * Notifies listeners that all properties of this instance have changed.
     */
    public void notifyChange() {
        synchronized (this) {
            if (mCallbacks == null) {
                return;
            }
        }
        mCallbacks.notifyCallbacks(this, 0, null);
    }

    /**
     * Notifies listeners that a specific property has changed. The getter for the property
     * that changes should be marked with {@link Bindable} to generate a field in
     * <code>BR</code> to be used as <code>fieldId</code>.
     *
     * @param fieldId The generated BR id for the Bindable field.
     */
    public void notifyPropertyChanged(int fieldId) {
        synchronized (this) {
            if (mCallbacks == null) {
                return;
            }
        }
        mCallbacks.notifyCallbacks(this, fieldId, null);
    }
}

notifyChange() 的核心语义是通知所有监听器,这个实例的 所有 属性都已更改。

完整流程:

用个简单的例子模拟下流程。

java 复制代码
public class MyViewModel {
    public final ObservableField<String> firstName = new ObservableField<>("John");
    public final ObservableField<String> lastName = new ObservableField<>("Doe");
    
    public final ObservableField<String> fullName = new ObservableField<>(firstName, lastName) {
        @Override
        public String get() {
            return firstName.get() + " " + lastName.get();
        }
    };
}

firstNamelastName 此时执行的是 ObservableField(T value) 的构造方法,此时这个两个对象是没有注册监听器的,你发送notifyChange() 是不会有监听器响应的,具体在哪注册监听器,咱们后面再说。这里主要针对的是fullName对应的是 ObservableField(Observable... dependencies)方法

java 复制代码
public ObservableField(T value) {
    mValue = value;
}

public ObservableField(Observable... dependencies) {
    super(dependencies);
}
java 复制代码
/**
 * 可观察字段的抽象基类
 * 支持依赖其他 Observable 对象,当依赖对象变化时自动触发自身通知
 */
abstract class BaseObservableField extends BaseObservable {
    
    /**
     * 无参构造器
     * 创建一个独立的可观察字段,不依赖任何其他 Observable
     */
    public BaseObservableField() {
    }
    
    /**
     * 带依赖的构造器
     * 创建一个依赖其他 Observable 对象的字段
     * 当任何一个依赖对象发生变化时,当前对象也会触发变化通知
     * 
     * @param dependencies 依赖的可观察对象数组
     *                     可以是 ObservableField, ObservableInt, ObservableBoolean 等
     */
    public BaseObservableField(Observable... dependencies) {
        // 检查依赖数组是否有效(不为 null 且长度不为 0)
        if (dependencies != null && dependencies.length != 0) {
            // 创建依赖回调实例
            DependencyCallback callback = new DependencyCallback();
            
            // 遍历所有依赖对象
            for (int i = 0; i < dependencies.length; i++) {
                // 为每个依赖对象添加属性变化监听器
                // 当任何一个依赖变化时,callback.onPropertyChanged() 会被调用
                dependencies[i].addOnPropertyChangedCallback(callback);
            }
        }
    }
    
    /**
     * 依赖回调内部类
     * 用于监听所有依赖对象的变化
     */
    class DependencyCallback extends Observable.OnPropertyChangedCallback {
        
        /**
         * 当任何一个被监听的 Observable 属性发生变化时调用
         * 
         * @param sender     发生变化的可观察对象
         * @param propertyId 变化的属性 ID(通常为 BR.xxx 或 0 表示整个对象变化)
         */
        @Override
        public void onPropertyChanged(Observable sender, int propertyId) {
            // 触发当前对象的变化通知
            // 这会通知所有监听当前对象的观察者:当前对象也发生了变化
            // 实际效果:依赖变化 → 当前对象也被标记为"已变化"
            notifyChange();
        }
    }
}

BaseObservableField(Observable... dependencies) 这段代码的关键逻辑是:

  • 如果传入了依赖对象(dependencies 不为空)
  • 创建一个 DependencyCallback实例(同一个实例,不是每个依赖都新建一个)
  • 遍历所有依赖,把自己注册为每一个依赖的监听器

这意味着: 当任意一个依赖对象(比如 firstName) 发生变化时,都会回调到同一个 DependencyCallback 的 onPropertyChanged

1.2 可观察字段(ObservableInt 等)

对比之前的 ObservableField ,可以很清晰的看到多了Parcelable两个对应方法,是Parcelable 协议的完整实现

java 复制代码
public class ObservableInt extends BaseObservableField implements Parcelable, Serializable {
    static final long serialVersionUID = 1L;
    private int mValue;

    public ObservableInt(int value) {
        mValue = value;
    }

    public ObservableInt() {
    }

    public ObservableInt(Observable... dependencies) {
        super(dependencies);
    }

    public int get() {
        return mValue;
    }

    public void set(int value) {
        if (value != mValue) {
            mValue = value;
            notifyChange();
        }
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(mValue);
    }

    public static final Parcelable.Creator<ObservableInt> CREATOR
            = new Parcelable.Creator<ObservableInt>() {

        @Override
        public ObservableInt createFromParcel(Parcel source) {
            return new ObservableInt(source.readInt());
        }

        @Override
        public ObservableInt[] newArray(int size) {
            return new ObservableInt[size];
        }
    };
}

ObservableBoolean vs ObservableField<Boolean>

java 复制代码
// ObservableBoolean - 有 Parcelable 支持
ObservableBoolean bool1 = new ObservableBoolean(true);
// ✅ 可以跨进程传递
intent.putExtra("bool", bool1);

// ObservableField<Boolean> - 无 Parcelable 支持
ObservableField<Boolean> bool2 = new ObservableField<>(true);
// ❌ 不能直接通过 Intent 传递
// intent.putExtra("bool", bool2);  // 编译错误

// 但可以通过 Serializable 勉强传递(性能差)
intent.putExtra("bool", (Serializable) bool2);  // 不推荐

同时在之前的章节中我们就知道,ObservableBoolean 的 性能是比 ObservableField<Boolean>要高的。因为 ObservableBoolean 没有自动拆箱装箱的过程。

问题1:为什么 ObservableField<T> 不同时实现 Parcelable?

因为 ObservableField<T> 是一个泛型类,而 ObservableInt 等是针对基本类型的特化类。在 Android 中,泛型类型无法可靠地实现 Parcelable,而基本类型可以

  • Parcelable 要求类型完全确定:实现 Parcelable 必须在编译时就明确知道每个字段的具体类型,以便生成固定的写入和读取逻辑。ObservableField<T> 的泛型参数 T 可以是任意类型,编译时无法确定,因此无法实现通用的序列化方案。
  • Parcel 的读写是类型相关的Parcel 为每种基础类型提供了独立的读写方法(writeInt、writeString、writeParcelable 等)。泛型 T 无法在编译时映射到具体的读写方法,运行时判断会导致性能下降和代码复杂,且无法保证 T 类型一定支持序列化。而 ObservableInt 内部是确定的 int 类型,可以直接使用 writeInt / readInt,实现简单可靠。

其实官方提供了 ObservableParcelable<T> 来弥补 ObservableField<T> 没有实现 Parcelable。

1.3 可观察对象 (BaseObservable)

目前我们知道了下面的继承关系 ObservableFields <--- BaseObservableField <--- BaseObservable。让数据类继承BaseObservable,并在getter上添加@Bindable注解,在setter中通知变更。

java 复制代码
public class User extends BaseObservable {
    private String name;
    private int age;

    @Bindable 
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
        notifyPropertyChanged(BR.name);
    }

    @Bindable
    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
        notifyPropertyChanged(BR.age);
    }

}

回看之前的代码,可以看到 BaseObservable 有两个通知方法,之前分析的源码都是通过notifyChange()来通知;如今我们可以通过 notifyPropertyChanged() 实现定向更新,数据量复杂庞大的情况下,很适合使用这种方式。

java 复制代码
// BaseObservable 提供了两个方法
public class BaseObservable implements Observable {
    
    // 粗粒度:通知所有属性都变了
    public void notifyChange() {
        mCallbacks.notifyCallbacks(this, 0, null);  // propertyId = 0
    }
    
    // 细粒度:通知特定属性变了
    public void notifyPropertyChanged(int fieldId) {  // 指定具体属性ID
        mCallbacks.notifyCallbacks(this, fieldId, null);
    }
}

问题1:@Bindable 注解干了什么?

java 复制代码
// android.databinding.Bindable
@Retention(RetentionPolicy.RUNTIME)  // 运行时保留
@Target(ElementType.METHOD)           // 只能用在方法上
public @interface Bindable {
    // 可选参数:指定绑定的属性名
    String[] value() default {};
}

识别注解后干了什么呢?

java 复制代码
// Data Binding 的注解处理器
@AutoService(Processor.class)
public class DataBindingProcessor extends AbstractProcessor {
    
    @Override
    public boolean process(Set<? extends TypeElement> annotations, 
                          RoundEnvironment env) {
        // 查找所有 @Bindable 注解
        Set<? extends Element> bindableElements = 
            env.getElementsAnnotatedWith(Bindable.class);
        
        for (Element element : bindableElements) {
            // 提取方法名 "getName"
            String methodName = element.getSimpleName().toString();
            // 转换为属性名 "name"
            String propertyName = getPropertyName(methodName);
            // 生成 BR 字段
            generateBRField(propertyName);
        }
        return true;
    }
    
    private String getPropertyName(String methodName) {
        // getName() -> name
        // isEnabled() -> enabled
        if (methodName.startsWith("get")) {
            return decapitalize(methodName.substring(3));
        } else if (methodName.startsWith("is")) {
            return decapitalize(methodName.substring(2));
        }
        return methodName;
    }
}

说白了,就是根据你的方法名称,生成对应的 BR 字段。@Bindable 就是生成 BR.name 的关键。

1.4 可观察集合(ObservableArrayMap/ObservableArrayList)

适用于列表或Map这种结构不确定的数据 。其实跟之前 ObservableFields 的实现逻辑差不多,只不过类型换成了适配 ArrayMapArrayList

ObservableArrayMap:

java 复制代码
public class ObservableArrayMap<K, V> extends ArrayMap<K, V> implements ObservableMap<K, V> {

    private transient MapChangeRegistry mListeners;

    @Override
    public void addOnMapChangedCallback(
            OnMapChangedCallback<? extends ObservableMap<K, V>, K, V> listener) {
        if (mListeners == null) {
            mListeners = new MapChangeRegistry();
        }
        mListeners.add(listener);
    }

    @Override
    public void removeOnMapChangedCallback(
            OnMapChangedCallback<? extends ObservableMap<K, V>, K, V> listener) {
        if (mListeners != null) {
            mListeners.remove(listener);
        }
    }

    @Override
    public void clear() {
        boolean wasEmpty = isEmpty();
        if (!wasEmpty) {
            super.clear();
            notifyChange(null);
        }
    }

    public V put(K k, V v) {
        V val = super.put(k, v);
        notifyChange(k);
        return v;
    }

    @Override
    public boolean removeAll(Collection<?> collection) {
        boolean removed = false;
        for (Object key : collection) {
            int index = indexOfKey(key);
            if (index >= 0) {
                removed = true;
                removeAt(index);
            }
        }
        return removed;
    }

    @Override
    public boolean retainAll(Collection<?> collection) {
        boolean removed = false;
        for (int i = size() - 1; i >= 0; i--) {
            Object key = keyAt(i);
            if (!collection.contains(key)) {
                removeAt(i);
                removed = true;
            }
        }
        return removed;
    }

    @Override
    public V removeAt(int index) {
        K key = keyAt(index);
        V value = super.removeAt(index);
        if (value != null) {
            notifyChange(key);
        }
        return value;
    }

    @Override
    public V setValueAt(int index, V value) {
        K key = keyAt(index);
        V oldValue = super.setValueAt(index, value);
        notifyChange(key);
        return oldValue;
    }

    private void notifyChange(Object key) {
        if (mListeners != null) {
            mListeners.notifyCallbacks(this, 0, key);
        }
    }
}
java 复制代码
ObservableArrayMap<String, Object> user = new ObservableArrayMap<>();
user.put("name", "张三");
user.put("age", 20);
binding.setUser(user);

ObservableArrayList:

java 复制代码
public class ObservableArrayList<T> extends ArrayList<T> implements ObservableList<T> {
    private transient ListChangeRegistry mListeners = new ListChangeRegistry();

    @Override
    public void addOnListChangedCallback(OnListChangedCallback listener) {
        if (mListeners == null) {
            mListeners = new ListChangeRegistry();
        }
        mListeners.add(listener);
    }

    @Override
    public void removeOnListChangedCallback(OnListChangedCallback listener) {
        if (mListeners != null) {
            mListeners.remove(listener);
        }
    }

    @Override
    public boolean add(T object) {
        super.add(object);
        notifyAdd(size() - 1, 1);
        return true;
    }

    @Override
    public void add(int index, T object) {
        super.add(index, object);
        notifyAdd(index, 1);
    }

    @Override
    public boolean addAll(Collection<? extends T> collection) {
        int oldSize = size();
        boolean added = super.addAll(collection);
        if (added) {
            notifyAdd(oldSize, size() - oldSize);
        }
        return added;
    }

    @Override
    public boolean addAll(int index, Collection<? extends T> collection) {
        boolean added = super.addAll(index, collection);
        if (added) {
            notifyAdd(index, collection.size());
        }
        return added;
    }

    @Override
    public void clear() {
        int oldSize = size();
        super.clear();
        if (oldSize != 0) {
            notifyRemove(0, oldSize);
        }
    }

    @Override
    public T remove(int index) {
        T val = super.remove(index);
        notifyRemove(index, 1);
        return val;
    }

    @Override
    public boolean remove(Object object) {
        int index = indexOf(object);
        if (index >= 0) {
            remove(index);
            return true;
        } else {
            return false;
        }
    }

    @Override
    public T set(int index, T object) {
        T val = super.set(index, object);
        if (mListeners != null) {
            mListeners.notifyChanged(this, index, 1);
        }
        return val;
    }

    @Override
    protected void removeRange(int fromIndex, int toIndex) {
        super.removeRange(fromIndex, toIndex);
        notifyRemove(fromIndex, toIndex - fromIndex);
    }

    private void notifyAdd(int start, int count) {
        if (mListeners != null) {
            mListeners.notifyInserted(this, start, count);
        }
    }

    private void notifyRemove(int start, int count) {
        if (mListeners != null) {
            mListeners.notifyRemoved(this, start, count);
        }
    }
}
java 复制代码
ObservableArrayList<Object> user = new ObservableArrayList<>();
user.add("张三"); // index 0
user.add(20);     // index 1
binding.setUser(user);

二、可观察数据容器监听的绑定

上一章节,我们基于 基础数据对象 (POJO) 来探究 DataBinding 的源码流程。本章讲解的观察者对象的相关内容还没有提及到,这里我们将其补全。当使用可观察数据容器时,对比之前的你会发现多了一个方法**updateRegistration() ,**这个方法就是注册的关键。

2.1 ObservableFields

java 复制代码
public class User1 {
    public final ObservableField<String> name = new ObservableField<>();
    public final ObservableInt age = new ObservableInt(); // 避免装箱

    public User1(String name, int age) {
        this.name.set(name);
        this.age.set(age);
    }
}

当我在XML只使用了 name ,而没有使用 age :在 ActivityMainBindingImpl.java 中的executeBindings() 发现了 updateRegistration(0, user1Name);

  • updateRegistration() 方法在 executeBindings() 中
  • 只生成 XML 中实际使用到的字段的监听器代码

2.2 BaseObservable

java 复制代码
public class User extends BaseObservable 

当我使用 BaseObservable 的时候情况就完全不一样了:

  • updateRegistration() 方法在 setUser() 中
  • 在XML中使用的字段没有找到对于的updateRegistration()
方式 updateRegistration 位置 注册次数
BaseObservable setUser() 方法中 1次(设置时)
ObservableField executeBindings() 方法中 可能多次(每次执行绑定时检查)

2.3 updateRegistration() 方法

java 复制代码
// 在 ViewDataBinding 基类中
protected boolean updateRegistration(int localFieldId, Observable observable) {
    return updateRegistration(localFieldId, observable, CREATE_PROPERTY_LISTENER);
}

具体代码:

java 复制代码
/**
 * 更新指定本地字段的观察者注册状态
 * 
 * @param localFieldId    本地字段的唯一ID,用于标识需要观察的字段
 * @param observable      新的可观察对象(如 ObservableField、LiveData 等)
 * @param listenerCreator 用于创建弱监听器的工厂对象
 * @return true 表示观察者状态发生变化(新增或更换了观察对象),false 表示无变化(同一对象)
 */
private boolean updateRegistration(int localFieldId, Object observable,
                                   ViewDataBinding.CreateWeakListener listenerCreator) {
    // 情况1:新传入的可观察对象为 null
    // 此时需要移除该字段上原有的任何监听器
    if (observable == null) {
        return unregisterFrom(localFieldId);
    }

    // 从缓存数组中取出该字段当前的弱监听器(可能为 null)
    ViewDataBinding.WeakListener listener = mLocalFieldObservers[localFieldId];

    // 情况2:当前没有任何监听器(首次注册)
    if (listener == null) {
        registerTo(localFieldId, observable, listenerCreator); // 注册新的监听器
        return true; // 表示发生了注册动作
    }

    // 情况3:已有监听器,检查其指向的目标对象是否就是当前传入的 observable
    // 注意:WeakListener 内部持有对 observable 的弱引用,getTarget() 可能返回 null(已被GC)
    if (listener.getTarget() == observable) {
        return false; // 同一个对象,无需重复注册,什么也不做
    }

    // 情况4:已有监听器,但目标对象不同(或原来的对象已被回收,返回 null)
    // 需要先移除旧的监听器,再为新对象创建并注册监听器
    unregisterFrom(localFieldId); // 解绑旧的 observable
    registerTo(localFieldId, observable, listenerCreator); // 绑定新的 observable
    return true; // 表示发生了替换
}
情况 条件 行为 返回值
取消注册 observable == null 调用 unregisterFrom 移除监听 true(发生了变更)
首次注册 listener == null 调用 registerTo 新增监听 true
对象未变 listener.getTarget() == observable 什么都不做 false(无变更)
对象已变 listener != null 且指向不同对象 先解绑旧的,再绑定新的 true

三、绑定类型

3.1 单向绑定 语法:@{}

3.1.1 基础使用示例

XML 复制代码
<!-- activity_main.xml -->
<layout>
    <data>
        <variable name="user" type="com.example.User"/>
    </data>
    <TextView
        android:text="@{user.name}"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
</layout>

2.1.2 源码分析

在之前的学习我们知道了,在DataBinding通知更新后最终会执行到executeBindings() 方法

java 复制代码
// 当你调用 setUser 后,最终会触发 executeBindings
binding.setUser(user)
    ↓
requestRebind()  // 调度刷新
    ↓
Choreographer 下一帧
    ↓
executePendingBindings()
    ↓
executeBindings()  // ← 这里执行实际赋值

DataBinding 编译后会生成ActivityMainBindingImpl.java,核心代码:

java 复制代码
public class ActivityMainBindingImpl extends ActivityMainBinding {
    
    private TextView mboundView0;  // 缓存TextView引用
    
    @Override
    protected void executeBindings() {
        long dirtyFlags = 0;
        synchronized(this) {
            dirtyFlags = mDirtyFlags;
            mDirtyFlags = 0;  // 清空脏标记
        }
        
        // 1. 获取数据对象
        com.example.User user = mUser;
        
        // 2. 从user中取出name值
        java.lang.String userName = null;
        if ((dirtyFlags & 0x3L) != 0) {  // 检查user.name是否脏
            if (user != null) {
                userName = user.getName();  // ← 调用getter获取值
            }
        }
        
        // 3. 赋值到TextView上
        if ((dirtyFlags & 0x3L) != 0) {
            // 关键:通过BindingAdapter设置文本
            androidx.databinding.adapters.TextViewBindingAdapter.setText(
                this.mboundView0,  // TextView
                userName           // 要设置的文本
            );
        }
    }
}

这里可以看到,如果判断数据发生了变化就使用 **TextViewBindingAdapter.setText()**实现对文本的更新。

所以,@{user.name} 的值是在 executeBindings() 中通过 user.getName() 获取,然后通过 TextViewBindingAdapter.setText() 最终调用 textView.setText() 完成赋值的。

3.2 双向绑定 语法:@={}

3.2.1 基础使用示例

XML 复制代码
<!-- activity_main.xml -->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable 
            name="user1" 
            type="com.example.User1"/>
    </data>
    
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
    
        <!-- 双向绑定:EditText 既能显示又能修改数据 -->
        <EditText
            android:text="@={user1.name}"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>
        
        
    </LinearLayout>
</layout>

3.2.2 源码分析

DataBinding 的单向绑定最终会执行到 executeBindings() 方法。而双向绑定 在单向绑定的基础上,增加了 反向监听 机制。

DataBinding 编译后会生成 ActivityMainBindingImpl.java,核心代码:

java 复制代码
/**
 * DataBinding 编译生成的绑定实现类
 * 原始布局文件包含:<EditText android:text="@={user1.name}" />
 */
public class ActivityMain2BindingImpl extends ActivityMain2Binding {

    // ==================== 静态缓存区 ====================
    @Nullable
    private static final androidx.databinding.ViewDataBinding.IncludedLayouts sIncludes;
    @Nullable
    private static final android.util.SparseIntArray sViewsWithIds;
    static {
        sIncludes = null;      // 没有引用的布局文件
        sViewsWithIds = null;  // 没有需要特殊处理的 View ID
    }
    
    // ==================== View 引用 ====================
    @NonNull
    private final android.widget.LinearLayout mboundView0;  // 根布局
    @NonNull
    private final android.widget.EditText mboundView1;      // 双向绑定的 EditText
    
    // ==================== 反向绑定监听器(双向绑定的核心)====================
    /**
     * 反向绑定监听器:当 EditText 的文本发生变化时被调用
     * 作用:将 View 的变化同步回 ViewModel
     */
    private androidx.databinding.InverseBindingListener mboundView1androidTextAttrChanged = 
        new androidx.databinding.InverseBindingListener() {
            @Override
            public void onChange() {
                // 步骤1:从 EditText 中获取当前文本
                // getTextString() 内部调用 view.getText().toString()
                java.lang.String callbackArg_0 = 
                    androidx.databinding.adapters.TextViewBindingAdapter.getTextString(mboundView1);
                
                // 步骤2:局部变量声明(线程安全考虑)
                boolean user1NameJavaLangObjectNull = false;
                java.lang.String user1NameGet = null;
                com.example.myjetpack.User1 user1 = mUser1;           // 获取 ViewModel 对象
                androidx.databinding.ObservableField<java.lang.String> user1Name = null;
                boolean user1JavaLangObjectNull = false;
                
                // 步骤3:检查 ViewModel 是否为空
                user1JavaLangObjectNull = (user1) != (null);
                if (user1JavaLangObjectNull) {
                    // 获取 ObservableField 属性
                    user1Name = user1.name;
                    
                    user1NameJavaLangObjectNull = (user1Name) != (null);
                    if (user1NameJavaLangObjectNull) {
                        // 步骤4:核心!将 View 的值设置回 ViewModel
                        // 这会触发 ObservableField 的通知机制
                        user1Name.set(((java.lang.String) (callbackArg_0)));
                    }
                }
            }
        };
    
    // ==================== 构造函数 ====================
    public ActivityMain2BindingImpl(@Nullable androidx.databinding.DataBindingComponent bindingComponent, 
                                    @NonNull View root) {
        this(bindingComponent, root, mapBindings(bindingComponent, root, 2, sIncludes, sViewsWithIds));
    }
    
    /**
     * 私有构造函数,初始化 View 引用
     * @param bindingComponent 绑定组件(可用于自定义适配器)
     * @param root 根 View
     * @param bindings 解析出的 View 数组
     */
    private ActivityMain2BindingImpl(androidx.databinding.DataBindingComponent bindingComponent, 
                                     View root, 
                                     Object[] bindings) {
        super(bindingComponent, root, 1);
        
        // 初始化 View 引用(索引来自 mapBindings 的解析结果)
        this.mboundView0 = (android.widget.LinearLayout) bindings[0];
        this.mboundView0.setTag(null);
        
        this.mboundView1 = (android.widget.EditText) bindings[1];
        this.mboundView1.setTag(null);
        
        setRootTag(root);
        
        // 标记所有绑定都需要初始化
        invalidateAll();
    }
    
    // ==================== 生命周期方法 ====================
    /**
     * 标记所有绑定为脏,强制重新绑定
     * 设置标志位 0x4L,表示需要重新设置监听器
     */
    @Override
    public void invalidateAll() {
        synchronized(this) {
            mDirtyFlags = 0x4L;  // 设置需要设置监听器的标志
        }
        requestRebind();  // 请求在下一帧执行绑定
    }
    
    /**
     * 检查是否有待处理的绑定更新
     */
    @Override
    public boolean hasPendingBindings() {
        synchronized(this) {
            if (mDirtyFlags != 0) {
                return true;
            }
        }
        return false;
    }
    
    // ==================== 变量设置方法 ====================
    /**
     * 通过变量 ID 设置变量(BR 文件生成的常量)
     * @param variableId 变量 ID(如 BR.user1)
     * @param variable 变量值
     */
    @Override
    public boolean setVariable(int variableId, @Nullable Object variable) {
        boolean variableSet = true;
        if (BR.user1 == variableId) {
            setUser1((com.example.myjetpack.User1) variable);
        } else {
            variableSet = false;
        }
        return variableSet;
    }
    
    /**
     * 设置 ViewModel 对象
     * 设置标志位 0x2L,表示 user1 对象发生了变化
     */
    public void setUser1(@Nullable com.example.myjetpack.User1 User1) {
        this.mUser1 = User1;
        synchronized(this) {
            mDirtyFlags |= 0x2L;  // 标记 user1 对象需要重新读取
        }
        notifyPropertyChanged(BR.user1);  // 通知属性变化
        super.requestRebind();            // 请求重新绑定
    }
    
    // ==================== 属性变化监听(观察者模式)====================
    /**
     * 当 ObservableField 属性发生变化时被调用
     * @param localFieldId 局部字段 ID(0 代表 user1.name)
     * @param object 变化的对象(ObservableField)
     * @param fieldId 字段 ID
     */
    @Override
    protected boolean onFieldChange(int localFieldId, Object object, int fieldId) {
        switch (localFieldId) {
            case 0:  // user1.name 属性
                return onChangeUser1Name((androidx.databinding.ObservableField<java.lang.String>) object, fieldId);
        }
        return false;
    }
    
    /**
     * user1.name 属性变化时的回调
     * 设置标志位 0x1L,表示 name 属性需要重新读取
     */
    private boolean onChangeUser1Name(androidx.databinding.ObservableField<java.lang.String> User1Name, int fieldId) {
        if (fieldId == BR._all) {  // BR._all 表示整个对象的变化
            synchronized(this) {
                mDirtyFlags |= 0x1L;  // 标记 name 属性需要更新
            }
            return true;
        }
        return false;
    }
    
    // ==================== 执行绑定的核心方法 ====================
    /**
     * 执行实际的绑定操作(单向 + 双向)
     * 这是 DataBinding 的核心方法,在每一帧被调用
     */
    @Override
    protected void executeBindings() {
        long dirtyFlags = 0;
        synchronized(this) {
            dirtyFlags = mDirtyFlags;  // 获取当前脏标志
            mDirtyFlags = 0;           // 重置脏标志
        }
        
        java.lang.String user1NameGet = null;
        com.example.myjetpack.User1 user1 = mUser1;
        androidx.databinding.ObservableField<java.lang.String> user1Name = null;
        
        // ========== 阶段1:读取数据模型 ==========
        // 0x7L = 111 (二进制),包含标志位 0x1L、0x2L、0x4L
        if ((dirtyFlags & 0x7L) != 0) {
            // 从 ViewModel 读取 user1 对象
            if (user1 != null) {
                user1Name = user1.name;  // 获取 ObservableField
            }
            
            // 注册观察者:监听 user1Name 的变化
            // localFieldId = 0 对应 case 0: onChangeUser1Name()
            updateRegistration(0, user1Name);
            
            // 读取实际的值
            if (user1Name != null) {
                user1NameGet = user1Name.get();  // 调用 ObservableField.get()
            }
        }
        
        // ========== 阶段2:更新 UI(单向绑定方向)==========
        // 将数据模型的值设置到 View 上
        if ((dirtyFlags & 0x7L) != 0) {
            // 调用适配器设置文本(ViewModel → View)
            androidx.databinding.adapters.TextViewBindingAdapter.setText(
                this.mboundView1, 
                user1NameGet
            );
        }
        
        // ========== 阶段3:设置反向监听器(双向绑定方向)==========
        // 只在初始化时执行一次,后续更新不会重复设置
        if ((dirtyFlags & 0x4L) != 0) {
            // 为 EditText 设置 TextWatcher,用于监听文本变化
            // 当文本变化时,会回调 mboundView1androidTextAttrChanged.onChange()
            androidx.databinding.adapters.TextViewBindingAdapter.setTextWatcher(
                this.mboundView1,
                null,  // BeforeTextChanged 回调
                null,  // OnTextChanged 回调
                null,  // AfterTextChanged 回调
                mboundView1androidTextAttrChanged  // ← 反向绑定监听器(核心)
            );
        }
    }
    
    // ==================== 脏标志位管理 ====================
    /**
     * 脏标志位(64位整数)
     * 0xffffffffffffffffL = 所有位都是1,表示初始状态所有标志位都脏
     * 
     * 标志位映射:
     * flag 0 (0x1L): user1.name 属性变化
     * flag 1 (0x2L): user1 对象变化
     * flag 2 (0x4L): 需要设置监听器(特殊标志,只在初始化时使用)
     */
    private long mDirtyFlags = 0xffffffffffffffffL;
    
    /* flag mapping
        flag 0 (0x1L): user1.name
        flag 1 (0x2L): user1
        flag 2 (0x3L): null
    flag mapping end*/
    
}

流程如下

bash 复制代码
    // ==================== 注释说明 ====================
    /**
     * 完整工作流程:
     * 
     * 1. 初始化阶段:
     *    - 构造函数调用 invalidateAll(),设置 mDirtyFlags = 0x4L
     *    - requestRebind() 触发 executeBindings()
     *    - executeBindings() 中检测到 0x4L 标志,调用 setTextWatcher() 设置监听器
     * 
     * 2. 用户输入阶段(View → ViewModel):
     *    - 用户修改 EditText 文本
     *    - TextWatcher 回调 → mboundView1androidTextAttrChanged.onChange()
     *    - 调用 user1.name.set(newValue) 更新 ViewModel
     *    - ObservableField.set() 触发 onChangeUser1Name()
     *    - 设置 mDirtyFlags |= 0x1L,调用 requestRebind()
     * 
     * 3. 刷新 UI 阶段(ViewModel → View):
     *    - executeBindings() 再次执行
     *    - 检测到 0x1L 标志,读取新的 user1NameGet
     *    - 调用 setText() 更新 EditText
     *    - 注意:此时不会再次触发 TextWatcher(防循环机制)
     * 
     * 4. 外部更新阶段(代码修改 ViewModel):
     *    - 调用 setUser1() 或 user1.name.set()
     *    - 设置对应的脏标志(0x2L 或 0x1L)
     *    - 触发 executeBindings() 刷新 UI
     *    - 不会重新设置 TextWatcher(因为没有 0x4L 标志)
     */

**setTextWatcher 会在初始化调用一次,**不会随着set 的触发反复调用。

java 复制代码
// 1. 用户在 EditText 输入 "A"
EditText.afterTextChanged("A")
    ↓
// 2. 反向绑定:更新 Model
user.setName("A")
    ↓
// 3. Model 变化触发通知
notifyPropertyChanged(BR.name)
    ↓
// 4. 单向绑定:更新 UI
textView.setText("A")
    ↓
// 5. setText 又会触发 TextWatcher
EditText.afterTextChanged("A")  // 再次触发,形成循环
    ↓
// 无限循环...

到底会不会无限循环下去,看看 TextViewBindingAdapter.setText。里面会跟原生数据比较,并不会导致循环。

java 复制代码
    @BindingAdapter("android:text")
    public static void setText(TextView view, CharSequence text) {
        final CharSequence oldText = view.getText();
        if (text == oldText || (text == null && oldText.length() == 0)) {
            return;
        }
        if (text instanceof Spanned) {
            if (text.equals(oldText)) {
                return; // No change in the spans, so don't set anything.
            }
        } else if (!haveContentsChanged(text, oldText)) {
            return; // No content changes, so don't set anything.
        }
        view.setText(text);
    }

3.3 事件绑定

3.3.1 基本使用示例

java 复制代码
public class Handler {
    // 方法签名必须匹配 View.OnClickListener
    public void onButtonClick(View view) {
        // 处理点击事件
        Log.d("TAG", "Button clicked");
    }

    // 也可以带多个参数
    public void onButtonClick(View view, int id) {
        // 但 DataBinding 会自动匹配
    }
}
XML 复制代码
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
            name="handler"
            type="com.example.Handler" />
    </data>
    
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        
        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Click Me"
            android:onClick="@{handler::onButtonClick}" />
            
    </LinearLayout>
</layout>

3.3.2 源码分析

java 复制代码
@Override
protected void executeBindings() {
    long dirtyFlags = 0;
    synchronized(this) {
        dirtyFlags = mDirtyFlags;
        mDirtyFlags = 0;  // 清空脏标记
    }
    
    // 1. 获取当前 handler
    com.example.myjetpack.Handler handler = mHandler;
    
    // 2. 声明监听器变量
    android.view.View.OnClickListener handlerOnButtonClickAndroidViewViewOnClickListener = null;
    
    // 3. 检查脏标记,决定是否重新创建监听器
    if ((dirtyFlags & 0x3L) != 0) {
        if (handler != null) {
            // 4. 创建或复用监听器,并设置 handler
            handlerOnButtonClickAndroidViewViewOnClickListener = 
                (((mHandlerOnButtonClickAndroidViewViewOnClickListener == null) 
                    ? (mHandlerOnButtonClickAndroidViewViewOnClickListener = new OnClickListenerImpl()) 
                    : mHandlerOnButtonClickAndroidViewViewOnClickListener)
                .setValue(handler));
        }
    }
    
    // 5. 设置监听器到 Button
    if ((dirtyFlags & 0x3L) != 0) {
        this.button.setOnClickListener(handlerOnButtonClickAndroidViewViewOnClickListener);
    }
}

编译时将方法引用转换为 View.OnClickListener,通过单例监听器复用机制绑定到 Button。

四、注解

4.1 @BindingAdapter

具体使用这里就不多说了,之前的章节有。包括上面说到的 TextViewBindingAdapter.setText() 等,都是使用 @BindingAdapter 注解的方式实现的。

4.1.1 注解定义源码

java 复制代码
package android.databinding;

@Target(Method)
@Retention(RetentionPolicy.RUNTIME)
public @interface BindingAdapter {
    /**
     * 属性名称数组,支持同时绑定多个属性
     */
    String[] value();
    
    /**
     * 是否需要在属性变化时重新调用
     * 默认 true,表示当绑定的属性值发生变化时会触发方法调用
     */
    boolean requireAll() default true;
}

4.1.2 编译时注解处理

java 复制代码
// DataBindingProcessor 核心处理逻辑(简化版)
public class DataBindingProcessor extends AbstractProcessor {
    
    @Override
    public boolean process(Set<? extends TypeElement> annotations, 
                          RoundEnvironment roundEnv) {
        
        // 收集所有@BindingAdapter注解的方法
        Set<? extends Element> elements = 
            roundEnv.getElementsAnnotatedWith(BindingAdapter.class);
        
        for (Element element : elements) {
            ExecutableElement method = (ExecutableElement) element;
            BindingAdapter annotation = 
                method.getAnnotation(BindingAdapter.class);
            
            // 解析注解参数
            String[] attributes = annotation.value();
            boolean requireAll = annotation.requireAll();
            
            // 生成适配器代码
            generateBindingAdapter(method, attributes, requireAll);
        }
        return true;
    }
    
    private void generateBindingAdapter(ExecutableElement method,
                                       String[] attributes,
                                       boolean requireAll) {
        // 生成类似以下代码:
        // android.databinding.adapters.TextViewBindingAdapter
    }
}

我们看下 @BindingAdapter(value = {"fadeIn", "duration"}, requireAll = false) :行代码定义了自定义 XML 属性与 Java/Kotlin 方法之间的映射关系

简单来说,它告诉 Android 系统:"当你在 XML 布局文件中给某个 View 设置了 app:fadeInapp:duration 属性时,请调用这个 Java 方法来处理。"

XML 复制代码
<View
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:fadeIn="@{true}"
    app:duration="@{500}" />
XML 属性 Java 方法参数 说明
app:fadeIn boolean shouldFade 对应第一个属性
app:duration int duration 对应第二个属性
(自动注入) View view 第一个参数永远是该属性绑定的 View 本身
特性 requireAll = true (默认) requireAll = false
触发条件 必须同时在 XML 中声明所有定义的属性 只要 XML 中声明了其中任意一个属性即可触发
缺失处理 如果缺少任何一个属性,编译直接报错 缺失的属性将传入对应类型的默认值
典型用途 属性之间强关联(缺一不可,如 layout_widthlayout_height 属性可选,或者有默认行为(如你的例子: duration 可选)

Data Binding 并不关心你的静态方法叫什么(例如叫 setFadeIn、applyAnimation 甚至 abc 都可以)。它唯一识别的是注解 @BindingAdapter("...") 括号里的字符串

4.2 @BindingConversion

java 复制代码
public class ColorConversions {
    
    // 场景1:颜色资源ID -> ColorDrawable
    @BindingConversion
    public static ColorDrawable convertColorToDrawable(int colorResId) {
        return new ColorDrawable(colorResId);
    }
    
    // 场景2:颜色字符串 -> ColorDrawable
    @BindingConversion
    public static ColorDrawable convertColorStringToDrawable(String colorHex) {
        if (colorHex == null) return null;
        try {
            int color = Color.parseColor(colorHex);
            return new ColorDrawable(color);
        } catch (IllegalArgumentException e) {
            return null;
        }
    }
    
    // 场景3:整型颜色值 -> ColorStateList
    @BindingConversion
    public static ColorStateList convertColorToColorStateList(int color) {
        return ColorStateList.valueOf(color);
    }
}

习惯建议 :通常采用convert[TypeA]To[TypeB] 的形式。

  • 比如:convertStringToColor 或 dateToText。

  • 因为它是静态全局的,清晰的命名有助于你在代码搜索时快速定位。

其实他也不关心命名是什么,只根据传参 TypeA和 返回类型 TypeB决定,起名字只是为了方便查找。

维度 @BindingAdapter @BindingConversion
匹配依据 注解里的 字符串名称 方法的 参数和返回值类型
XML 关联 必须在 XML 中写出对应的属性名 隐式触发,XML 中看不见转换器名字
冲突判定 属性名冲突时报错 输入/输出类型组合重复时报错
作用域 针对特定 View 的特定属性 全局生效,只要类型匹配就尝试转换
相关推荐
守月满空山雪照窗2 小时前
图形 API 体系解析:Android Vulkan / OpenGL 与主流图形 API 对比
android
我命由我123452 小时前
Android 开发,getSystemService 警告信息:Must be one of: Context. POWER_SERVICE ...
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
zopple2 小时前
Laravel3.x经典特性回顾
android·java·数据库
Digitally2 小时前
8 种方法:将视频从手机传输到电脑(安卓 /iOS)
android·智能手机·电脑
LiuYaoheng2 小时前
【Android】Handler 全面解析
android
华盛AI2 小时前
Lovable开发平台,生成安卓和iOS都能运行的原生App方案(用Kotlin或者Switf编写)
android·ios·kotlin
Fate_I_C2 小时前
Kotlin 基础语法快速回顾
android·开发语言·kotlin
Digitally2 小时前
如何通过简单步骤将iPhone上的eSIM转移到安卓手机
android·智能手机·iphone
HookJames2 小时前
让 FlyingPress 的 Preload 队列变少,减轻 PHP 和数据库压力
android·数据库·php