痛点
在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才有子元素,我们看一下ButterKnife
的bind(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
正好实现了接口Unbinder
的unbind
方法,会在这个方法里棉做一些成员变量置空操作。会在生成的辅助类的构造方法中初始化我们有由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);
}
}
查看源码FragmentManager
的void 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)
网友评论