前言
最近学年设计地图app遇到了一个小的需求,那就是地图的POI搜索功能,当用户点击搜索的时候,会弹出一个dialog让用户进行输入,但是当输入的内容为空的时候,必须提醒用户让用户重新输入,但是现在有个问题,就是Android中利用AlertDialog.builder创建的dialog不管你是点击任何按钮或者点击屏幕dialog的外部,对话框都会无条件退出,那么怎么让我的dialog在用户不合法操作的时候不退出呢?那就可以用到反射机制。
什么是反射?
反射(reflection)是java里面的一种机制,它允许你在程序运行的时候获取类的信息(通过class对象),什么是类的类型信息呢?假如有个类A{},那么A类的类型信息可以简单地理解为:
- A类中定义了哪些成员属性
- A类中定义了哪些成员方法
- A类中定义了哪些构造方法
反射允许我们在运行的时候获得类的所有上述信息来进行操作,这些操作可以是在运行时修改某个对象的私有属性,调用对象的私有方法,和调用对象的构造方法创建新的对象。这样说你可能还是不容易理解,请继续往下看。
反射的基石——Class对象
Class对象又是什么鬼?
首先刚才在上面说到了,反射机制可以允许我们在运行时获得某个类的信息,那么我们怎么来表示这个类的信息呢?或者说,我们这个类的信息是封装在什么地方的呢?
答案就是Class对象,每个类都对应了一个Class对象,你可以简单地理解为:每个类都对应了自己唯一的Class对象,而这个Class对象包含了关于这个类的全部信息(有哪些属性,哪些方法)。
首先,你必须清楚,在java这个面向对象的世界里,一切都是对象,一切都是对象,一切都是对象~我们的类也是对象(Class类的对象),类的成员方法也是对象(Method类的对象),构造方法是Constructor类的对象,类的属性也是对象(Field类的对象)。也就是说我们平时写java程序的时候,不管你是使用API里的类还是自己写的类,他们都是Class类的对象。
现在我们来理解java的类是在运行时期动态加载的这句话的意思,也就是说,java程序在运行的时候不会把所有的类全部加载到jvm中去,而是在你需要的时候再通过类加载器加载到jvm中去的,那么什么时候才是需要这个类的时候呢?最常见的就是当你调用这个类的构造方法的时候,jvm会加载该类的字节码(.class文件)到jvm,而在加载该类的过程中,就会由jvm产生一个该类的Class对象。这个对象代表了这个类的所有信息,是这个类在运行时期的唯一标识。
这里谈到了jvm的类加载过程,到现在为止我只是简单地浏览了一遍,可能上面的步骤说的不是很详细,在这里推荐一本书——《Java虚拟机精讲》-高祥龙著,将的挺好的,适合想要初步了解JVM的人群。
怎么获取Class对象?
既然Class对象是jvm在加载类的时候创建的,它又封装了类的详细信息,那么怎么获得某个类的Class对象呢?
Class对象的获取和普通对象有所区别,不能通过Class a=new Class()这种方式,java不允许我们自行创建Class对象的实例,它是由jvm自动创建,而我们只能通过以下三种方式获取到Class对象:
- Class.forname(classname)
- obj.getclass()
- classname.class
下面通过一个例子来一一解释以上三种方式,假如现在要获取String类的Class对象:
- 第一种:Class.forname()
这是通过Class类的静态方法forname方法获取Class对象,形参classname形式必须为包名+类名。那么我们通过这种方式获取A的Class对象的方式为:Class.forname(Java.lang.String)。 -
第二种:obj.getclass()
当你已经拥有了一个该类的对象的时候,就可以调用obj.getclass()方法来获取Class对象,比如已经得到String类的对象A,就可以执行A.getclass()来获取String类的Class对象(getclass()方法为Object类的成员方法)。 -
第三种:classname.class
classname.class又叫做类字面常量。这种方式获取String类的Class对象就为String.class。
classname.class方式和Class.forname()方式的区别
Class.forname()方式,倘若在执行该方法的时候该类并没有被加载,则会启动类加载器对类进行加载;而classname.class方式不会导致该类的加载。(来自java编程思想第四版),二者都会导致类的加载,但是Class.forname()会进行类的初始化(在类加载的连接阶段执行类的静态代码块和静态成员的初始化),但是classname.class不会导致该步骤的执行,这种方式直到第一次引用类的静态成员或者调用静态方法的时候才会执行类的初始化。
获取到了Class对象之后我可以做什么?
获取到了类的Class对象,你就相当于获取了这个类的很多信息,可以调用Class对象的getMethods()方法获取到该类的所有方法(包括从父类继承的方法),也可以调用getConstructor()方法获取该类的所有构造方法,还可以调用getField()方法获得该类的全部成员属性(包括私有属性)
比如我们可以通过以下简单代码在运行时获取String类的信息:
public static void main(String[] args) throws ClassNotFoundException {
Class<?> stringInfo= Class.forName("java.lang.String");
for (Method method : stringInfo.getMethods()) {
System.out.println(method);
}
for (Field field : stringInfo.getFields()) {
System.out.println(field);
}
for (Constructor<?> constructor : stringInfo.getConstructors()) {
System.out.println(constructor);
}
}
上面的代码简单地获取了String类的所有方法和属性,运行的截图如下:
这只是Class对象的最简单使用,但是还没牵扯到反射机制的使用,Class对象的更详细方法以及使用请参考java官方在线文档
通过Class对象使用反射机制
在得到类的Class对象之后,我们可以进行的操作还远远不止上面的那些操作,还可以通过该对象得到特定的方法引用并且把它赋值给Method类型的引用(因为任何一个方法都是Method类的实例),然后通过调用Method类的invoke()方法,同样可以达到调用对应的成员方法的效果,那么下面我们通过反射机制来调用String类的方法:
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
String aString="hello world";
Class<?> stringInfo= aString.getClass();
try {
Method refectMethod=stringInfo.getDeclaredMethod("toUpperCase");
String bString=(String) reflectMethod.invoke(aString);
System.out.println(bString);
} catch (NoSuchMethodException | SecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
同样很简单,首先获得String类的Class对象,然后通过该对象获得toUpperCase方法,此时refectMethod()就代表了String类的toUpperCase(),再调用Method的invoke()方法,该方法的原型声明为:public Object invoke(Object obj, Object… args),形参分别表示需要操作的对象,以及方法的参数,由于toUpperCase()方法本身不带参数,所以调用invoke()只需传入操作的对象aString即可。
运行效果:

上述就是一个极为简单的例子,但是反射的运用肯定还远不止这些,共勉。
安卓dialog退出过程分析
既然点击了按钮就会导致dialog消失,那么我们就来看看在AlertDialog源码中按钮点击之后都发生了什么:
按钮监听事件是在builder的setPossitiveButton()中设置的,那么来到builder的源码,会发现该方法的实现:

这里完成的就是对p.mPositiveButtonListener的赋值,也就是只要你为按钮设置了监听器,那么这个值就肯定被赋值为你的监听器了,这个时候发现源代码中有一个p,在AS中右键查看源码,p为AlertController.AlertParams类型,进入该类的源码(AlertParams是一个静态内部类),发现在apply()方法中有如下代码(只截取了该方法源码的一部分),该方法在调用builder.create()的时候会被调用:
public void apply(AlertController dialog) {
if (mPositiveButtonText != null) {
dialog.setButton(DialogInterface.BUTTON_POSITIVE, mPositiveButtonText,
mPositiveButtonListener, null);
}
if (mNegativeButtonText != null) {
dialog.setButton(DialogInterface.BUTTON_NEGATIVE, mNegativeButtonText,
mNegativeButtonListener, null);
}
if (mNeutralButtonText != null) {
dialog.setButton(DialogInterface.BUTTON_NEUTRAL, mNeutralButtonText,
mNeutralButtonListener, null);
}
}
可以看到在apply()方法中调用dialog的setButton()方法设置了按钮的参数(分别对应setButton方法的四个参数),setButton()方法是AlertParams类所在的外部类AlertController中的方法,实现如下:
public void setButton(int whichButton, CharSequence text,
DialogInterface.OnClickListener listener, Message msg) {
if (msg == null && listener != null) {
msg = mHandler.obtainMessage(whichButton, listener);
}
switch (whichButton) {
case DialogInterface.BUTTON_POSITIVE:
mButtonPositiveText = text;
mButtonPositiveMessage = msg;
break;
case DialogInterface.BUTTON_NEGATIVE:
mButtonNegativeText = text;
mButtonNegativeMessage = msg;
break;
case DialogInterface.BUTTON_NEUTRAL:
mButtonNeutralText = text;
mButtonNeutralMessage = msg;
break;
default:
throw new IllegalArgumentException("Button does not exist");
}
}
在该方法中对各个按钮进行了设置,apply()方法传入的message参数为null,该message对象在本方法中进行了赋值(if语句中),赋值之后的message对象封装了我们最开始设置的监听器对象,用于接下来在按钮点击之后执行
接下来看点击事件的监听器源码(其中会调用到我们自己的监听器):
private final View.OnClickListener mButtonHandler = new View.OnClickListener() {
@Override
public void onClick(View v) {
final Message m;
if (v == mButtonPositive && mButtonPositiveMessage != null) {
m = Message.obtain(mButtonPositiveMessage);
} else if (v == mButtonNegative && mButtonNegativeMessage != null) {
m = Message.obtain(mButtonNegativeMessage);
} else if (v == mButtonNeutral && mButtonNeutralMessage != null) {
m = Message.obtain(mButtonNeutralMessage);
} else {
m = null;
}
if (m != null) {
m.sendToTarget();
}
// Post a message so we dismiss after the above handlers are executed
mHandler.obtainMessage(ButtonHandler.MSG_DISMISS_DIALOG, mDialog)
.sendToTarget();
}
};
各个按钮的点击事件已经在setButtons()方法中注册,从以上代码发现,在dialog中的按钮点击之后,首先会判断点击的按钮时哪一个,然后获取到对应的message对象,调用m.sendToTarget()通过异步的方式通知Handler的handleMessage方法执行我们最开始设置的监听器中的onClick()方法。
重点来了在执行m.sendToTarget()的下面还有两行代码,通过发送MSG_DISMISS_DIALOG消息,在Handler的handleMessage()方法中调用dismiss()方法退出dialog,这就是为什么点击按钮之后对话框无条件退出的原因。
源码中handleMessage()方法的实现如下:
private static final class ButtonHandler extends Handler {
// Button clicks have Message.what as the BUTTON{1,2,3} constant
private static final int MSG_DISMISS_DIALOG = 1;
private WeakReference<DialogInterface> mDialog;
public ButtonHandler(DialogInterface dialog) {
mDialog = new WeakReference<>(dialog);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case DialogInterface.BUTTON_POSITIVE:
case DialogInterface.BUTTON_NEGATIVE:
case DialogInterface.BUTTON_NEUTRAL:
((DialogInterface.OnClickListener) msg.obj).onClick(mDialog.get(), msg.what);
break;
case MSG_DISMISS_DIALOG:
((DialogInterface) msg.obj).dismiss();
}
}
}
可以看到最后一行调用了dismiss()方法退出对话框,跟踪到dismiss()方法你会发现内部是通过调用dismissDialog()方法退出对话框的,该方法实现代码:
void dismissDialog() {
if (mDecor == null || !mShowing) {
return;
}
if (mWindow.isDestroyed()) {
Log.e(TAG, "Tried to dismissDialog() but the Dialog's window was already destroyed!");
return;
}
try {
mWindowManager.removeViewImmediate(mDecor);
} finally {
if (mActionMode != null) {
mActionMode.finish();
}
mDecor = null;
mWindow.closeAllPanels();
onStop();
mShowing = false;
sendDismissMessage();
}
}
在第一个if语句里判断了mShowing的值,这里的mDecor在Dialog类前面已经赋值过了(你可以查看源码),所以关键就在mShowing的值,当对话框已经显示其值就为true,但是如果设置mShowing为false,该方法就会马上return,也就不会执行后续的销毁对话框的代码,但是跟踪mShowing发现它是私有私有私有成员属性,那么怎么在运行时设置一个对象中私有属性的值呢?yeah,反射,反射,反射机制!
前面说的比较冗长,简单地说,对话框消失的过程可以这样理解,在我们创建dialog的时候一般会调用setPositiveButton()方法传入我们自己的监听器,然后在create()(show会首先执行create)的时候该监听器会被赋值给dialog内部AlertParams对象的mPositiveButtonListener属性,然后该对象的apply()方法将该赋值后的mPositiveButtonListener封装在一个message对象中,在按钮被点击之后,就会获取我们先前message对象中封装的监听器,进而调用该监听器的onClick()方法执行我们在setPositiveButton()中传入的监听器逻辑,同时发送消息调用dismiss()方法让对话框消失。
阻止对话框退出
既然原理都已经知道了,那就直接运行时判断,用户输入有误的话就通过反射修改mShowing的值为false,再手动调用dismiss(),由于mShowing为false,对话框就不会消失,代码如下:
public void onClick(DialogInterface dialog, int which) {
Field field = null;
try {
field = dialog.getClass().getSuperclass().getSuperclass().getDeclaredField("mShowing");
field.setAccessible(true);
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
if (mEditText.getText().toString().equals("")) {
try {
field.set(dialog, false);
dialog.dismiss();
mTextInputLayout.setError("亲,还没输入噢0.0");
} catch (Exception e) {
e.printStackTrace();
}
}
- 参考资料 CSDN博客
支付宝打赏
