Loading...
墨滴

Nullifier

2021/10/14  阅读:82  主题:默认主题

Databinding原理

前言

最近在做包体积优化的技术需求所以在网上搜了些相关文章。其中有一篇文章提供了一个思路就是通过DataBinding的BindingAdapter来解决项目中drawable文件爆炸的问题以此来减少包体积。因为之前没怎么用过DataBinding所以借这个机会学习学习DataBinding的原理

我们先看下本地版项目中drawable文件目录的现状:

可以看到我们项目中的shape文件已经处于一个爆炸的状态。因为这个定义的文件不能复用所以后边同学只能基于前边的文件进行CV修改,久而久之就成了这样。虽然对这些文件优化可能减少不了多少大小,但是确实可以让我们的项目变得干爽。

DataBinding最主要的两个功能部分就是双向绑定和BindingAdapter扩展属性。接下来我们从最简单的demo开始,深入学习分析DataBinding。

双向绑定原理

demo代码

这是我们的Demo代码:

MainActivity.kt
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mainBinding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        model = ViewModelProvider(this).get(MainActivityModel::class.java)
        
        findViewById<Button>(R.id.bt_test).setOnClickListener(this)
        book = Book("book""12")
        mainBinding.book = book
    }
ViewExpression.kt
    @BindingAdapter("android:setShapeDrawable")
    fun setShapeDrawable(view: View, bitmapPath: String?) {
    
        Log.e("ViewExpression""setShapeDrawable bitmapPath $bitmapPath")
        Log.e("ViewExpression""setShapeDrawable bitmapPath decode ${Uri.decode(bitmapPath)}")
    
        val shapeBean = Gson().fromJson(Uri.decode(bitmapPath), ShapeBean::class.java)
        val gradientDrawable = GradientDrawable()
    
        if(TextUtils.equals("rectangle",shapeBean.shape)){
            // 形状-圆角矩形
            gradientDrawable.shape = GradientDrawable.RECTANGLE
        }
        // 圆角
        gradientDrawable.cornerRadius = shapeBean.corners.radius
        gradientDrawable.setColor(Color.parseColor(shapeBean.color))
        gradientDrawable.setStroke(shapeBean.stroke.width, Color.parseColor(shapeBean.stroke.color))
        view.background = gradientDrawable
    }
activity_main.xml
    <?xml version="1.0" encoding="utf-8"?>
    <layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools">
    
        <data>
    
            <import type="com.example.databindingdemo.activity.Book" />
    
            <import type="com.example.databindingdemo.activity.MainActivityModel" />
    
            <variable
                name="book"
                type="com.example.databindingdemo.activity.Book" />
    
            <variable
                name="mainActivityModel"
                type="com.example.databindingdemo.activity.MainActivityModel" />
        </data>
    
        <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            tools:context=".activity.MainActivity">
    
            <TextView
                android:id="@+id/tv_test2"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@={mainActivityModel.price}"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toTopOf="parent" />
    
            <TextView
                android:id="@+id/tv_test"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@{book.price}"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toTopOf="parent" />
    
            <ImageView
                android:layout_width="200dp"
                android:layout_height="100dp"
                android:setShapeDrawable="@{&quot;%7B%0A%20%20%20%20%22stroke%22%3A%7B%0A%20%20%20%20%20%20%20%20%22width%22%3A30%2C%0A%20%20%20%20%20%20%20%20%22color%22%3A%22%23ff0000%22%0A%20%20%20%20%7D%2C%0A%20%20%20%22color%22%3A%22%2300ff00%22%2C%0A%20%20%20%20%22corners%22%3A%7B%0A%20%20%20%20%20%20%20%20%22radius%22%3A50%0A%20%20%20%20%7D%2C%0A%20%20%20%20%22shape%22%3A%22rectangle%22%0A%7D&quot;}"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintTop_toTopOf="parent" />
    
            <ImageView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintTop_toTopOf="parent" />
    
            <Button
                android:id="@+id/bt_test"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="测试"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toBottomOf="@id/tv_test" />
    
        </androidx.constraintlayout.widget.ConstraintLayout>
    </layout>

DataBindingUtil解读

我们先从DataBindingUtil开始看起

    public static <T extends ViewDataBinding> T setContentView(@NonNull Activity activity,
            int layoutId, @Nullable DataBindingComponent bindingComponent) {
        activity.setContentView(layoutId);
        View decorView = activity.getWindow().getDecorView();
        ViewGroup contentView = (ViewGroup) decorView.findViewById(android.R.id.content);
        return bindToAddedViews(bindingComponent, contentView, 0, layoutId);
    }

首先 activity.setContentView(layoutId);是之前activity加载布局的逻辑。相较于之前这里多了一步从DecorView上获取布局然后调用bindToAddedViews(bindingComponent, contentView, 0, layoutId);方法。

private static <T extends ViewDataBinding> T bindToAddedViews(DataBindingComponent component,
            ViewGroup parent, int startChildren, int layoutId) {
        final int endChildren = parent.getChildCount();
        final int childrenAdded = endChildren - startChildren;
        if (childrenAdded == 1) {
            final View childView = parent.getChildAt(endChildren - 1);
            return bind(component, childView, layoutId);
        } else {
            final View[] children = new View[childrenAdded];
            for (int i = 0; i < childrenAdded; i++) {
                children[i] = parent.getChildAt(i + startChildren);
            }
            return bind(component, children, layoutId);
        }
    }
    
     static <T extends ViewDataBinding> T bind(DataBindingComponent bindingComponent, View root,
            int layoutId) {
        return (T) sMapper.getDataBinder(bindingComponent, root, layoutId);
    }

代码很简单,递归遍历view然后调用bind方法执行绑定。

 static <T extends ViewDataBinding> T bind(DataBindingComponent bindingComponent, View root,
            int layoutId) {
        return (T) sMapper.getDataBinder(bindingComponent, root, layoutId);
    }

最终可以看到DataBinderMapper里获取的一个DataBinder返回了出去。那我们就继续跟踪sMapper的创建。

我们接着在DataBindingUtil类里边翻,最上边我们可以看到sMapper是一个直接new出来的DataBinderMapperImpl对象。而DataBinderMapperImpl的构造函数里就会直接加上我们生成的自己的中间DataBinderMapperImpl文件。

private static DataBinderMapper sMapper = new DataBinderMapperImpl();

注意以下包名:

package androidx.databinding;

public class DataBinderMapperImpl extends MergedDataBinderMapper {
  DataBinderMapperImpl() {
    addMapper(new com.example.databindingdemo.DataBinderMapperImpl());
  }
}

总结下:

  1. DataBindingUtil会持有一个叫sMapper的变量,而他对应的字节码DataBinderMapperImpl是编译时候生成的。
  2. 编译生成DataBinderMapperImpl会在构造函数中加载我们真实使用的DataBinderMapperImpl实现类。

我们在来一张图来总体了解下:

DataBinderMapperImpl解读

我们继续看我们之前追踪到的DataBinderMapperImpl类里的getDataBinder方法,是编译生成我们自己包名下边的DataBinderMapperImpl!

@Override
  public ViewDataBinding getDataBinder(DataBindingComponent component, View view, int layoutId) {
    int localizedLayoutId = INTERNAL_LAYOUT_ID_LOOKUP.get(layoutId);
    if(localizedLayoutId > 0) {
      final Object tag = view.getTag();
      if(tag == null) {
        throw new RuntimeException("view must have a tag");
      }
      switch(localizedLayoutId) {
        case  LAYOUT_ACTIVITYMAIN: {
          if ("layout/activity_main_0".equals(tag)) {
            return new ActivityMainBindingImpl(component, view);
          }
          throw new IllegalArgumentException("The tag for activity_main is invalid. Received: " + tag);
        }
      }
    }
    return null;
  }

这里有个关键问题就是从view里找出了一个tag,而且最主要的是这个tag如果为空的话会抛出异常报错。也就是这个tag是个很重要的东西,但是我们我们并没有设置过这个tag,而且下边的代码也提示了这个tag名字是"layout/activity_main_0",如果匹配到就返回ActivityMainBindingImpl具体实现类。

所以我们就以这个突破点去找在哪设置的这个tag。

很遗憾studio直接搜索是搜索不到的,直接公布结果是在编译后的activity_main.xml文件里,这个文件在app/build/intermediates/incremental/mergeDebugResources/stripped.dir/layout/这个目录下。

<Targets>
        <Target tag="layout/activity_main_0"
            view="androidx.constraintlayout.widget.ConstraintLayout">
            <Expressions />
            <location endLine="66" endOffset="55" startLine="20" startOffset="4" />
        </Target>
        <Target id="@+id/tv_test2" tag="binding_1" view="TextView">
            <Expressions>
                <Expression attribute="android:text" text="mainActivityModel.price">
                    <Location endLine="29" endOffset="53" startLine="29" startOffset="12" />
                    <TwoWay>true</TwoWay>
                    <ValueLocation endLine="29" endOffset="51" startLine="29" startOffset="29" />
                </Expression>
            </Expressions>
            <location endLine="32" endOffset="55" startLine="25" startOffset="8" />
        </Target>
        <Target id="@+id/tv_test" tag="binding_2" view="TextView">
            <Expressions>
                <Expression attribute="android:text" text="book.price">
                    <Location endLine="38" endOffset="39" startLine="38" startOffset="12" />
                    <TwoWay>false</TwoWay>
                    <ValueLocation endLine="38" endOffset="37" startLine="38" startOffset="28" />
                </Expression>
            </Expressions>
            <location endLine="42" endOffset="55" startLine="34" startOffset="8" />
        </Target>
        <Target tag="binding_3" view="ImageView">
            <Expressions>
                <Expression
                    attribute="android:setShapeDrawable"
                    text="&quot;%7B%0A%20%20%20%20%22stroke%22%3A%7B%0A%20%20%20%20%20%20%20%20%22width%22%3A30%2C%0A%20%20%20%20%20%20%20%20%22color%22%3A%22%23ff0000%22%0A%20%20%20%20%7D%2C%0A%20%20%20%22color%22%3A%22%2300ff00%22%2C%0A%20%20%20%20%22corners%22%3A%7B%0A%20%20%20%20%20%20%20%20%22radius%22%3A50%0A%20%20%20%20%7D%2C%0A%20%20%20%20%22shape%22%3A%22rectangle%22%0A%7D&quot;">
                    <Location endLine="47" endOffset="405" startLine="47" startOffset="12" />
                    <TwoWay>false</TwoWay>
                    <ValueLocation endLine="47" endOffset="403" startLine="47" startOffset="36" />
                </Expression>
            </Expressions>
            <location endLine="49" endOffset="55" startLine="44" startOffset="8" />
        </Target>
        <Target id="@+id/bt_test" view="Button">
            <Expressions />
            <location endLine="64" endOffset="63" startLine="57" startOffset="8" />
        </Target>
    </Targets>

可以看到这个tag是编译的时候databinding给我们布局默认加上的。同时这个xml文件还记录了一些其他信息,我们可以一起看下:

  • Expressions:表示View的data-binding表达式
  • TwoWay:表示是否双向绑定,如果双向绑定那布局表达式需要'@={}'
  • Location:这个比较简单代码的位置。

说到这大家肯定对DataBinding生成的各种文件蒙圈了,所以总结下DataBinding给我们自动生成了哪些文件和他们的作用:

  • DataBinderMapperImpl(sdk):用来添加项目ViewDataBinding的映射表,构造方法里会将我们项目里的DataBinderMapperImpl加进去。
  • DataBinderMapperImpl(demo):记录的项目里有哪些布局文件进行了DataBinding。
  • ActivityMainBindingImpl:真正的绑定关系处理类马上后边会介绍
  • activity_mian.xml:正常的资源编译文件
  • activity_main-layout.xml:就是我们刚才看到的文件,记录了组件对应的绑定信息,如表达式是否双向绑定等。

我们继续看getDataBinder方法的逻辑,判断tag匹配后就会给我们返回一个ActivityMainBindingImpl对象,那我们就继续研究ActivityMainBindingImpl这个类。

ActivityMainBindingImpl解读

我们先来看这个构造方法:

public ActivityMainBindingImpl(@Nullable androidx.databinding.DataBindingComponent bindingComponent, @NonNull View root) {
        this(bindingComponent, root, mapBindings(bindingComponent, root, 5, sIncludes, sViewsWithIds));
    }
    private ActivityMainBindingImpl(androidx.databinding.DataBindingComponent bindingComponent, View root, Object[] bindings) {
        super(bindingComponent, root, 2
            , (android.widget.Button) bindings[4]
            , (android.widget.TextView) bindings[2]
            , (android.widget.TextView) bindings[1]
            );
        this.mboundView0 = (androidx.constraintlayout.widget.ConstraintLayout) bindings[0];
        this.mboundView0.setTag(null);
        this.mboundView3 = (android.widget.ImageView) bindings[3];
        this.mboundView3.setTag(null);
        this.tvTest.setTag(null);
        this.tvTest2.setTag(null);
        setRootTag(root);
        // listeners
        invalidateAll();
    }

重点是mapBindings(bindingComponent, root, 5, sIncludes, sViewsWithIds)这个方法。

private static void mapBindings(DataBindingComponent bindingComponent, View view,
            Object[] bindings, IncludedLayouts includes, SparseIntArray viewsWithIds,
            boolean isRoot) {
        final int indexInIncludes;
        final ViewDataBinding existingBinding = getBinding(view);
        if (existingBinding != null) {
            return;
        }
        Object objTag = view.getTag();
        final String tag = (objTag instanceof String) ? (String) objTag : null;
        boolean isBound = false;
        if (isRoot && tag != null && tag.startsWith("layout")) {
            final int underscoreIndex = tag.lastIndexOf('_');
            if (underscoreIndex > 0 && isNumeric(tag, underscoreIndex + 1)) {
                final int index = parseTagInt(tag, underscoreIndex + 1);
                if (bindings[index] == null) {
                    bindings[index] = view;
                }
                indexInIncludes = includes == null ? -1 : index;
                isBound = true;
            } else {
                indexInIncludes = -1;
            }
        } else if (tag != null && tag.startsWith(BINDING_TAG_PREFIX)) {
            int tagIndex = parseTagInt(tag, BINDING_NUMBER_START);
            if (bindings[tagIndex] == null) {
                bindings[tagIndex] = view;
            }
            isBound = true;
            indexInIncludes = includes == null ? -1 : tagIndex;
        } else {
            // Not a bound view
            indexInIncludes = -1;
        }
        if (!isBound) {
            final int id = view.getId();
            if (id > 0) {
                int index;
                if (viewsWithIds != null && (index = viewsWithIds.get(id, -1)) >= 0 &&
                        bindings[index] == null) {
                    bindings[index] = view;
                }
            }
        }

        if (view instanceof  ViewGroup) {
            final ViewGroup viewGroup = (ViewGroup) view;
            final int count = viewGroup.getChildCount();
            int minInclude = 0;
            for (int i = 0; i < count; i++) {
                final View child = viewGroup.getChildAt(i);
                boolean isInclude = false;
                if (indexInIncludes >= 0 && child.getTag() instanceof String) {
                    String childTag = (String) child.getTag();
                    if (childTag.endsWith("_0") &&
                            childTag.startsWith("layout") && childTag.indexOf('/') > 0) {
                        // This *could* be an include. Test against the expected includes.
                        int includeIndex = findIncludeIndex(childTag, minInclude,
                                includes, indexInIncludes);
                        if (includeIndex >= 0) {
                            isInclude = true;
                            minInclude = includeIndex + 1;
                            final int index = includes.indexes[indexInIncludes][includeIndex];
                            final int layoutId = includes.layoutIds[indexInIncludes][includeIndex];
                            int lastMatchingIndex = findLastMatching(viewGroup, i);
                            if (lastMatchingIndex == i) {
                                bindings[index] = DataBindingUtil.bind(bindingComponent, child,
                                        layoutId);
                            } else {
                                final int includeCount =  lastMatchingIndex - i + 1;
                                final View[] included = new View[includeCount];
                                for (int j = 0; j < includeCount; j++) {
                                    included[j] = viewGroup.getChildAt(i + j);
                                }
                                bindings[index] = DataBindingUtil.bind(bindingComponent, included,
                                        layoutId);
                                i += includeCount - 1;
                            }
                        }
                    }
                }
                if (!isInclude) {
                    mapBindings(bindingComponent, child, bindings, includes, viewsWithIds, false);
                }
            }
        }
    }

这里我们可以看到它将布局中的含有databinding赋值的tag控件一一存入bindings的Object的数组中并返回。。而在构造方法中这个数组又拿出来给对应的view进行赋值。这也就是为什么我们可以直接从DataBindingUtil获取的Bind中拿出来组件直接用而不用在find一遍的原因。

接下来看invalidateAll()方法。

    @Override
    public void invalidateAll() {
        synchronized(this) {
                mDirtyFlags = 0x10L;
        }
        requestRebind();
    }
    protected void requestRebind() {
        if (mContainingBinding != null) {
            mContainingBinding.requestRebind();
        } else {
            final LifecycleOwner owner = this.mLifecycleOwner;
            if (owner != null) {
                Lifecycle.State state = owner.getLifecycle().getCurrentState();
                if (!state.isAtLeast(Lifecycle.State.STARTED)) {
                    return; // wait until lifecycle owner is started
                }
            }
            synchronized (this) {
                if (mPendingRebind) {
                    return;
                }
                mPendingRebind = true;
            }
            if (USE_CHOREOGRAPHER) {
                mChoreographer.postFrameCallback(mFrameCallback);
            } else {
                mUIThreadHandler.post(mRebindRunnable);
            }
        }
    }

上面代码调用过来就是为了执行mRebindRunnable。在看该Runnable中的executePendingBindings()方法

private final Runnable mRebindRunnable = new Runnable() {
        @Override
        public void run() {
            synchronized (this) {
                mPendingRebind = false;
            }
            processReferenceQueue();

            if (VERSION.SDK_INT >= VERSION_CODES.KITKAT) {
                // Nested so that we don't get a lint warning in IntelliJ
                if (!mRoot.isAttachedToWindow()) {
                    // Don'
t execute the pending bindings until the View
                    // is attached again.
                    mRoot.removeOnAttachStateChangeListener(ROOT_REATTACHED_LISTENER);
                    mRoot.addOnAttachStateChangeListener(ROOT_REATTACHED_LISTENER);
                    return;
                }
            }
            executePendingBindings();
        }
    };
private void executeBindingsInternal() {
        if (mIsExecutingPendingBindings) {
            requestRebind();
            return;
        }
        if (!hasPendingBindings()) {
            return;
        }
        mIsExecutingPendingBindings = true;
        mRebindHalted = false;
        if (mRebindCallbacks != null) {
            mRebindCallbacks.notifyCallbacks(this, REBIND, null);

            // The onRebindListeners will change mPendingHalted
            if (mRebindHalted) {
                mRebindCallbacks.notifyCallbacks(this, HALTED, null);
            }
        }
        if (!mRebindHalted) {
            executeBindings();
            if (mRebindCallbacks != null) {
                mRebindCallbacks.notifyCallbacks(this, REBOUND, null);
            }
        }
        mIsExecutingPendingBindings = false;
    }

上面也是对一些状态进行了判断添加一下回调,主要的还是executeBindings()方法。

 @Override
    protected void executeBindings() {
        long dirtyFlags = 0;
        synchronized(this) {
            dirtyFlags = mDirtyFlags;
            mDirtyFlags = 0;
        }
        java.lang.String bookPrice = null;
        java.lang.String mainActivityModelPriceGet = null;
        com.example.databindingdemo.activity.MainActivityModel mainActivityModel = mMainActivityModel;
        com.example.databindingdemo.activity.Book book = mBook;
        androidx.databinding.ObservableField<java.lang.String> mainActivityModelPrice = null;

        if ((dirtyFlags & 0x16L) != 0) {

                if (mainActivityModel != null) {
                    // read mainActivityModel.price
                    mainActivityModelPrice = mainActivityModel.getPrice();
                }
                updateRegistration(1, mainActivityModelPrice);


                if (mainActivityModelPrice != null) {
                    // read mainActivityModel.price.get()
                    mainActivityModelPriceGet = mainActivityModelPrice.get();
                }
        }
        if ((dirtyFlags & 0x19L) != 0) {



                if (book != null) {
                    // read book.price
                    bookPrice = book.getPrice();
                }
        }
        // batch finished
        if ((dirtyFlags & 0x10L) != 0) {
            // api target 1

            com.example.databindingdemo.ViewExpressionKt.setShapeDrawable(this.mboundView3, "%7B%0A%20%20%20%20%22stroke%22%3A%7B%0A%20%20%20%20%20%20%20%20%22width%22%3A30%2C%0A%20%20%20%20%20%20%20%20%22color%22%3A%22%23ff0000%22%0A%20%20%20%20%7D%2C%0A%20%20%20%22color%22%3A%22%2300ff00%22%2C%0A%20%20%20%20%22corners%22%3A%7B%0A%20%20%20%20%20%20%20%20%22radius%22%3A50%0A%20%20%20%20%7D%2C%0A%20%20%20%20%22shape%22%3A%22rectangle%22%0A%7D");
            androidx.databinding.adapters.TextViewBindingAdapter.setTextWatcher(this.tvTest2, (androidx.databinding.adapters.TextViewBindingAdapter.BeforeTextChanged)null, (androidx.databinding.adapters.TextViewBindingAdapter.OnTextChanged)null, (androidx.databinding.adapters.TextViewBindingAdapter.AfterTextChanged)null, tvTest2androidTextAttrChanged);
        }
        if ((dirtyFlags & 0x19L) != 0) {
            // api target 1

            androidx.databinding.adapters.TextViewBindingAdapter.setText(this.tvTest, bookPrice);
        }
        if ((dirtyFlags & 0x16L) != 0) {
            // api target 1

            androidx.databinding.adapters.TextViewBindingAdapter.setText(this.tvTest2, mainActivityModelPriceGet);
        }
    }

哇,到这步我们终于看到了头。当(dirtyFlags & 0x19L) != 0时候把数据设置给tvTest上(M->V),当(dirtyFlags & 0x10L) != 0的时候给tvTest2设置了监听,在监听回调里把数据设置回了model(V->M),这也就是所谓的双向绑定!

private androidx.databinding.InverseBindingListener tvTest2androidTextAttrChanged = new androidx.databinding.InverseBindingListener() {
        @Override
        public void onChange() {
            // Inverse of mainActivityModel.price.get()
            //         is mainActivityModel.price.set((java.lang.String) callbackArg_0)
            java.lang.String callbackArg_0 = androidx.databinding.adapters.TextViewBindingAdapter.getTextString(tvTest2);
            // localize variables for thread safety
            // mainActivityModel.price.get()
            java.lang.String mainActivityModelPriceGet = null;
            // mainActivityModel.price != null
            boolean mainActivityModelPriceJavaLangObjectNull = false;
            // mainActivityModel.price
            androidx.databinding.ObservableField<java.lang.String> mainActivityModelPrice = null;
            // mainActivityModel != null
            boolean mainActivityModelJavaLangObjectNull = false;
            // mainActivityModel
            com.example.databindingdemo.activity.MainActivityModel mainActivityModel = mMainActivityModel;
            mainActivityModelJavaLangObjectNull = (mainActivityModel) != (null);
            if (mainActivityModelJavaLangObjectNull) {
                mainActivityModelPrice = mainActivityModel.getPrice();

                mainActivityModelPriceJavaLangObjectNull = (mainActivityModelPrice) != (null);
                if (mainActivityModelPriceJavaLangObjectNull) {
                    mainActivityModelPrice.set(((java.lang.String) (callbackArg_0)));
                }
            }
        }
    };

至此我们已经了解到了databinding如何实现双向绑定的原理。刚才在源码里各种绕来绕去大家肯定又蒙圈了,我们再用一张图来梳理梳理:

  • mapBindings:这个方法在构造方法里执行,主要从布局中解析出来tag判断是否是databinding绑定关系,如果是的话返回到一个object[]数组里,外边用这个数组给绑定对象view赋值并做其他操作。
  • 执行runnable:主要做了两件事情,一个是给rootView添加addOnAttachStateChangeListener()事件,用来监听。其次是调用executePendingBindings()。
  • executeBindings():上述调用逻辑最终会调用到ActivityMainBindingImpl的executeBindings(),这里是真正进行处理双向绑定和BindAdapter逻辑的地方。如果是单向绑定那就只是把数据设置给view,如果是双向绑定就会给view添加一个监听事件。

BindingAdapter原理

解读

BindingAdapter可以理解为一种布局属性扩展的工具。我们通过他可以很方便的给组件加上自定义的属性,同时它也支持表达式。所以我们就有了很大的扩展空间。

在上一节讲到ActivityMainBindingImpl的executeBindings方法中有这样一段代码

if ((dirtyFlags & 0x10L) != 0) {
            // api target 1

            com.example.databindingdemo.ViewExpressionKt.setShapeDrawable(this.mboundView3, "%7B%0A%20%20%20%20%22stroke%22%3A%7B%0A%20%20%20%20%20%20%20%20%22width%22%3A30%2C%0A%20%20%20%20%20%20%20%20%22color%22%3A%22%23ff0000%22%0A%20%20%20%20%7D%2C%0A%20%20%20%22color%22%3A%22%2300ff00%22%2C%0A%20%20%20%20%22corners%22%3A%7B%0A%20%20%20%20%20%20%20%20%22radius%22%3A50%0A%20%20%20%20%7D%2C%0A%20%20%20%20%22shape%22%3A%22rectangle%22%0A%7D");
        }

这坨代码从哪来的呢?这坨代码是我在ViewExpression类里写了一个BindingAdapter

@BindingAdapter("android:setShapeDrawable")
fun setShapeDrawable(view: View, bitmapPath: String?) {

    Log.e("ViewExpression""setShapeDrawable bitmapPath $bitmapPath")
    Log.e("ViewExpression""setShapeDrawable bitmapPath decode ${Uri.decode(bitmapPath)}")

    val shapeBean = Gson().fromJson(Uri.decode(bitmapPath), ShapeBean::class.java)
    val gradientDrawable = GradientDrawable()

    if(TextUtils.equals("rectangle",shapeBean.shape)){
        // 形状-圆角矩形
        gradientDrawable.shape = GradientDrawable.RECTANGLE
    }
    // 圆角
    gradientDrawable.cornerRadius = shapeBean.corners.radius
    gradientDrawable.setColor(Color.parseColor(shapeBean.color))
    gradientDrawable.setStroke(shapeBean.stroke.width, Color.parseColor(shapeBean.stroke.color))
    view.background = gradientDrawable
}

可以看到databinding会在在生成的中间代码里调用我们加了BindingAdapter注解的方法的调用。这也就是我们为什么可以在布局xml文件里使用自定义表达式的原因。

痛点解决

我们又回到文章开头的问题,我们怎么通过BindingAdapter把他改造下,以后开发的同学就直接像配置属性一样配置一个shape而不用在定义文件。

最直接的办法就是像我的demo里一样,把配置转换成一个uri字符串的形式,然后在ViewExpression里做解析加载。但是这种比较麻烦而且语义特别不明显。

%7B%0A%20%20%20%20%22stroke%22%3A%7B%0A%20%20%20%20%20%20%20%20%22width%22%3A30%2C%0A%20%20%20%20%20%20%20%20%22color%22%3A%22%23ff0000%22%0A%20%20%20%20%7D%2C%0A%20%20%20%22color%22%3A%22%2300ff00%22%2C%0A%20%20%20%20%22corners%22%3A%7B%0A%20%20%20%20%20%20%20%20%22radius%22%3A50%0A%20%20%20%20%7D%2C%0A%20%20%20%20%22shape%22%3A%22rectangle%22%0A%7D

你能一眼看到这个shape文件定义了哪些属性,并且他们的值是什么吗?并不能,我们在进行次简单改造吧!

ViewExpression.kt

@BindingAdapter("setShape")
fun setShape(view: View,shapeBuilder: ShapeBuilder) {
    Log.e("ViewExpression""setShape")
    view.background = shapeBuilder.mGradientDrawable
}

activity_main.xml

        <ImageView
            setShape="@{ShapeBuilder.create().setCornerRadius(10).setShapeType(0).setStroke(10,Color.parseColor(&quot;#f00f0f&quot;)).setOrientation(GradientDrawable.Orientation.BR_TL)}"
            android:layout_width="200dp"
            android:layout_height="100dp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

ShapeBuilder.java

package com.example.databindingdemo;

import android.graphics.drawable.GradientDrawable;
import android.util.Log;


public class ShapeBuilder {
    public GradientDrawable mGradientDrawable;

    private ShapeBuilder() {
        mGradientDrawable = new GradientDrawable();
    }

    public static ShapeBuilder create() {
        return new ShapeBuilder();
    }

    public ShapeBuilder setShapeType(int shapeType) {
        mGradientDrawable.setShape(shapeType);
        return this;
    }

    public ShapeBuilder setCornerRadius(int radius) {
        mGradientDrawable.setCornerRadius(radius);
        return this;
    }

    public ShapeBuilder setStroke(int width, int color) {
        mGradientDrawable.setStroke(width, color);
        return this;
    }

    /**
     * 暂不支持数组
     * @param colors
     * @return
     */
    public ShapeBuilder setColors(int[] colors) {
        if (colors != null && colors.length > 0) {
            mGradientDrawable.setColors(colors);
        }
        return this;
    }

    public ShapeBuilder setColors(int color) {
        if (color !=0) {
            mGradientDrawable.setColor(color);
        }
        return this;
    }

    public ShapeBuilder setOrientation(GradientDrawable.Orientation orientation) {
        Log.e("ShapeBuilder""setOrientation "+orientation);
        mGradientDrawable.setOrientation(orientation);
        return this;
    }

    /**
     * 暂不支持
     * @param startColor
     * @return
     */
    public ShapeBuilder setStartColor(int startColor) {
        return this;
    }
    /**
     * 暂不支持
     * @param endColor
     * @return
     */
    public ShapeBuilder setEndColor(int endColor) {
        return this;
    }
    /**
     * 暂不支持
     * @param angle
     * @return
     */
    public ShapeBuilder setAngle(int angle) {
        return this;
    }
}

经过我们的改造可以看到使用起来更加简便明确。虽然GradientDrawable没有给部分属性开启代码设置接口如angle、startColor、endColor,但是已经基本上满足了我们平常的开发需求。

更进一步

本文主要从初学者角度来学习和分析DataBinding并基于BindAdapter解决项目中的通过。对于大佬来说肯定会说“就这?”。那么在此附上两篇文章以供大佬挑战。文章主要通过讲述DataBinding的处理器如何解析加载编织代码,可以从更深层次的角度去理解DataBinding。这里由于篇幅和写作能力暂时就不展开啦。

(译)深入理解Data Binding原理

Android-DataBinding-原理-编译期

Nullifier

2021/10/14  阅读:82  主题:默认主题

作者介绍

Nullifier