对ButterKnife一点小改进的尝试(一)

2019-12-17  本文已影响0人  RxCode

痛点

Activity或者Dialog中使用butterknife绑定的时候需要将ButterKnife.bind(this);放置在setContentView(R.layout.simple_activity)之后,如果放在之前,如下所示:

@Override 
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.simple_activity);
    ButterKnife.bind(this);    
}

如果我们将这两句调换位置,这样写

@Override 
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ButterKnife.bind(this);
    setContentView(R.layout.simple_activity);
}

运行程序会出现类似这样的错误。

2019-12-17 19:24:39.789 9094-9094/? E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.butterknife, PID: 9094
    java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.butterknife/com.example.butterknife.library.SimpleActivity}: java.lang.IllegalStateException: Required view 'titleTv' with ID 2131230748 for field 'title' was not found. If this view is optional add '@Nullable' (fields) or '@Optional' (methods) annotation.
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3270)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3409)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:83)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2016)
        at android.os.Handler.dispatchMessage(Handler.java:107)
        at android.os.Looper.loop(Looper.java:214)
        at android.app.ActivityThread.main(ActivityThread.java:7356)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)
     Caused by: java.lang.IllegalStateException: Required view 'titleTv' with ID 2131230748 for field 'title' was not found. If this view is optional add '@Nullable' (fields) or '@Optional' (methods) annotation.
        at butterknife.internal.Utils.findRequiredView(Utils.java:84)
        at butterknife.internal.Utils.findRequiredViewAsType(Utils.java:96)
        at com.example.butterknife.library.SimpleActivity_ViewBinding.<init>(SimpleActivity_ViewBinding.java:37)
        at java.lang.reflect.Constructor.newInstance0(Native Method)
        at java.lang.reflect.Constructor.newInstance(Constructor.java:343)
        at butterknife.ButterKnife.bind(ButterKnife.java:203)
        at butterknife.ButterKnife.bind(ButterKnife.java:106)
        at com.example.butterknife.library.SimpleActivity.onCreate(SimpleActivity.java:69)
        at android.app.Activity.performCreate(Activity.java:7802)
        at android.app.Activity.performCreate(Activity.java:7791)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1299)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3245)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3409) 
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:83) 
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135) 
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95) 
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2016) 
        at android.os.Handler.dispatchMessage(Handler.java:107) 
        at android.os.Looper.loop(Looper.java:214) 
        at android.app.ActivityThread.main(ActivityThread.java:7356) 
        at java.lang.reflect.Method.invoke(Native Method) 
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492) 
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930) 

原因很简单,因为只有调用了setContentView,DecorView的contentview才有子元素,我们看一下ButterKnifebind(Activity target)bind(Dialog target)的实现:

  public static Unbinder bind(@NonNull Activity target) {
        View sourceView = target.getWindow().getDecorView();
        return bind(target, sourceView);
    }

   public static Unbinder bind(@NonNull Dialog target) {
        View sourceView = target.getWindow().getDecorView();
        return bind(target, sourceView);
    }
   public static Unbinder bind(@NonNull Object target, @NonNull View source) {
        Class<?> targetClass = target.getClass();
        if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());
        //拿到由ButterKnife生成辅助类的XXX_ViewBinding的构造函数
        Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);

        if (constructor == null) {
            return Unbinder.EMPTY;
        }

        //noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
        try {
          //生成辅助类对象
            return constructor.newInstance(target, source);
        } catch (IllegalAccessException e) {
            throw new RuntimeException("Unable to invoke " + constructor, e);
        } catch (InstantiationException e) {
            throw new RuntimeException("Unable to invoke " + constructor, e);
        } catch (InvocationTargetException e) {
            Throwable cause = e.getCause();
            if (cause instanceof RuntimeException) {
                throw (RuntimeException) cause;
            }
            if (cause instanceof Error) {
                throw (Error) cause;
            }
            throw new RuntimeException("Unable to create binding instance.", cause);
        }
    }

    @Nullable
    @CheckResult
    @UiThread
    private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
        Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
        if (bindingCtor != null || BINDINGS.containsKey(cls)) {
            if (debug) Log.d(TAG, "HIT: Cached in binding map.");
            return bindingCtor;
        }
        String clsName = cls.getName();
        if (clsName.startsWith("android.") || clsName.startsWith("java.")
                || clsName.startsWith("androidx.")) {
            if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
            return null;
        }
        try {
          //加载由ButterKnife生成辅助类XXX_ViewBinding
            Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
            //noinspection unchecked
            bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);//获取构造函数
            if (debug) Log.d(TAG, "HIT: Loaded binding class and constructor.");
        } catch (ClassNotFoundException e) {
            if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());
            bindingCtor = findBindingConstructorForClass(cls.getSuperclass());
        } catch (NoSuchMethodException e) {
            throw new RuntimeException("Unable to find binding constructor for " + clsName, e);
        }
        BINDINGS.put(cls, bindingCtor);//缓存
        return bindingCtor;
    }

可以看出他们都调用了bind(Object target,View source)方法并返回一个Unbinder对象。ButterKnife生成的辅助类XXX_ViewBinding正好实现了接口Unbinderunbind方法,会在这个方法里棉做一些成员变量置空操作。会在生成的辅助类的构造方法中初始化我们有由ButterKnife提供的注解修饰的各个成员变量。以@BindView修饰的各个View为例,会在辅助类的构造方法中通过activity的DecorView通过findViewById初始化这些变量。

public class SimpleActivity_ViewBinding implements Unbinder {
 public SimpleActivity_ViewBinding(final SimpleActivity target, View source) {
    this.target = target;
    target.title = Utils.findRequiredViewAsType(source, R.id.titleTv, "field 'title'", TextView.class);
    target.subtitle = Utils.findRequiredViewAsType(source, R.id.subtitle, "field 'subtitle'", TextView.class);
    view = Utils.findRequiredView(source, R.id.hello, "field 'hello', method 'sayHello', and method 'sayGetOffMe'");
    target.hello = Utils.castView(view, R.id.hello, "field 'hello'", Button.class);
 }
}

如果我们忘了在ButterKnife.bind()方法前面调用setContentView或者将setContentView方法放到了ButterKnife.bind()之后,因为并没有将布局文件里面的View添加到decorView里面,所以也就找不到相应的View。

ButterKnife只是提供了通过@BindView传入view的Id来绑定View,我们希望能提供一种传入View的布局Id来自动化的setContentView,就下面像这样,而不用写setContentView(R.layout.xxxxx)

@BindLayout(R.layout.simple_activity)
class SampleActivity extends Activty {
    @BindView(R.id.hello_dialog)
    Button mButton;
    @BindString(R.string.app_name) 
    String butterKnife;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //不需要再添加这一句setContentView(R.layout.xxxxx)
        ButterKnife.bind(this);
        mButton.setText(butterKnife);
    }
   
   @OnClick(R.id.hello_dialog)
    void onButtonClick() {
        Toast.makeText(getContext(), "Hello Button", Toast.LENGTH_SHORT).show();
    }
}

改进

为此,在ButterKnife原有的@BindView@BindString@BindColor@BindDrawable@BindInt等注解基础上引入新的注解@BindLayout,定义如下:

@Retention(RUNTIME)
@Target(TYPE)
public @interface BindLayout {
    @LayoutRes int value();
}

之所以将这个注解的target定义为TYPE是考虑到如果修饰在方法上的话,必然要进一步通过类对象进一步反射获取方法,然后再接着查找标注有@BindLayout的的方法,如果在某个类很多方法上都放这个注解,意义并不是很大,顶多只有在oncreate方法上才有意义。

后面有两个思路,一个是在ButterKnife.bind(this)方法里面解析注解并调用setContentView,另一个是在注解处理器生成辅助类里面做这样的事情,本文先采用第一个思路,在ButterKnife.bind(this)里面做修改

  public static Unbinder bind(@NonNull Activity target) {
        View sourceView = target.getWindow().getDecorView();
        injectActivityContentViewIfNeeded(target,sourceView);
        return bind(target, sourceView);
    }
    @NonNull
    @UiThread
    public static Unbinder bind(@NonNull Dialog target) {
        View sourceView = target.getWindow().getDecorView();
        injectDialogContentViewIfNeeded(target,sourceView);
        return bind(target, sourceView);
    }

针对activity和dialog分别做处理

   private static void injectActivityContentViewIfNeeded(@NonNull Activity target, View sourceView) {
        ViewGroup viewGroup = sourceView.findViewById(android.R.id.content);
       //表示在Activity类增加了BindLayout注解并且在 ButterKnife.bind(this);之前并未调用setContentView方法
        if (viewGroup != null && viewGroup.getChildCount() == 0
                && target.getClass().getAnnotation(BindLayout.class) != null) {
            BindLayout bindLayout = target.getClass().getAnnotation(BindLayout.class);
            target.setContentView(bindLayout.value());
        }
    }

    private static void injectDialogContentViewIfNeeded(@NonNull Dialog target, View sourceView) {
        ViewGroup viewGroup = sourceView.findViewById(android.R.id.content);
        //表示在Dialog类增加了BindLayout注解并且在 ButterKnife.bind(this);之前并未调用setContentView方法
        if (viewGroup != null && viewGroup.getChildCount() == 0
                && target.getClass().getAnnotation(BindLayout.class) != null) {
            BindLayout bindLayout = target.getClass().getAnnotation(BindLayout.class);
            target.setContentView(bindLayout.value());
        }
    }

针对Fragment,这种方法不太实用,因为onViewCreated的参数里面的View是在onCreateView方法中返回来的

@BindLayout(R.layout.simple_fragment)
public class MyFragment extends Fragment {
    @BindView(R.id.tv_fragment)
    TextView tvFragment;

/*    
    假设这段方法注释掉,onViewCreated方法中的View参数为空,通过 ButterKnife.bind(this, view)必然无法拿到tvFragment。
    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
        return inflater.inflate(R.layout.simple_fragment, container, false);
    }
*/

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        ButterKnife.bind(this, view);
    }
}

查看源码FragmentManagervoid moveToState(Fragment f, int newState, int transit, int transitionStyle,boolean keepActive)void ensureInflatedFragmentView(Fragment f)方法代码片段可以看到只有当onCreateView方法返回View不为空才会回调onViewCreated方法

void moveToState(Fragment f, int newState, int transit, int transitionStyle,boolean keepActive) {
                           // 省略部分代码
                            f.mContainer = container;
                            //performCreateView执行的是Fragment的onCreateView方法
                            f.mView = f.performCreateView(f.performGetLayoutInflater(
                                    f.mSavedFragmentState), container, f.mSavedFragmentState);
                            //只有不为空才会执行
                            if (f.mView != null) {
                                f.mView.setSaveFromParentEnabled(false);
                                if (container != null) {
                                    container.addView(f.mView);
                                }
                                if (f.mHidden) {
                                    f.mView.setVisibility(View.GONE);
                                }
                               //执行Fragment的onViewCreated方法
                                f.onViewCreated(f.mView, f.mSavedFragmentState);
                                dispatchOnFragmentViewCreated(f, f.mView, f.mSavedFragmentState,
                                        false);
                                // Only animate the view if it is visible. This is done after
                                // dispatchOnFragmentViewCreated in case visibility is changed
                                f.mIsNewlyAdded = (f.mView.getVisibility() == View.VISIBLE)
                                        && f.mContainer != null;
                            }
                        }
                       //省略其余代码
}

  void ensureInflatedFragmentView(Fragment f) {
        if (f.mFromLayout && !f.mPerformedCreateView) {
            f.mView = f.performCreateView(f.performGetLayoutInflater(
                    f.mSavedFragmentState), null, f.mSavedFragmentState);
            if (f.mView != null) {
                f.mView.setSaveFromParentEnabled(false);
                if (f.mHidden) f.mView.setVisibility(View.GONE);
                f.onViewCreated(f.mView, f.mSavedFragmentState);
                dispatchOnFragmentViewCreated(f, f.mView, f.mSavedFragmentState, false);
            }
        }
    }

当然有一种思路通过代码插桩,在onCreateView生成调用inflater.inflate(R.layout.simple_fragment, container, false)这段代码。这就需要判断@BindLayout注解修饰的类是否为Fragment的子类,并且onCreateView里面不存在这样的代码inflater.inflate(R.layout.simple_fragment, container, false)

上一篇 下一篇

猜你喜欢

热点阅读