6.5 窗体相关的异常
这种Crash很有名,原因基本都是在执行dismiss方法销毁对话框的时候,Activity已经不再存在。但是随着场景的不同,抛出的异常信息却又大不相同。本节我还会顺带讲一下在非主线程操作UI导致的异常。
6.5.1 窗口句柄泄露
异常中的关键字:
android.view.WidnowLeaded:Activity xxx has leaked window com.android.internal.policy.impl.PhoneWindow$DecorView{xxxx}that was originally added here.
发生频率:★★
我们试着写这样一段代码,来重现这个异常:
Dialog dialog; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_s1_scenario1); AlertDialog.Builder info = new AlertDialog.Builder(this); info.setTitle("Dialog").setPositiveButton("OK", null) .setMessage("This is a Dialog"); dialog = info.show(); finish(); }
以上代码在运行时必然崩溃,这是因为,最后finish语句销毁了当前Activity,但是在它基础上创建的AlertDialog对话框还在,窗口句柄泄露,未能及时销毁。
finish()语句是我故意写的,是为了重现这个异常。现实中当然不会这么写代码,往往是因为我们在非主线程中的某些操作不当而产生了一个严重的异常,从而强制关闭当前Activity。而在关闭的同时,却没能及时调用dismiss来解除对ProgressDialog等的引用,从而系统抛出了上述崩溃信息。
可以再写一个Demo,来模拟Activity被销毁的情景:
Dialog dialog; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_s1_scenario2); AlertDialog.Builder info = new AlertDialog.Builder(this); info.setTitle("Dialog").setPositiveButton("OK", null) .setMessage("This is a Dialog"); dialog = info.show(); findViewById(R.id.btnStartThread).setOnClickListener( new View.OnClickListener() { @Override public void onClick(View v) { new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } dialog.dismiss(); } }).start(); } }); }
同时,要设置这个Activity支持横竖屏旋转,这时就会产生崩溃信息了。
相应的解决办法是,重写Activity的onDestroy方法,在方法中调用dismiss来解除对ProgressDialog等的引用:
@Override public void onDestroy() { super.onDestroy(); // 成败就在这句话,注释了就会 Crash dialog.dismiss(); }
6.5.2 View not attached to window manager
异常中的关键字:
java.lang.IllegalArgumentException:View not attached to window manager at
android.view.WindowManagerImpl.findViewLocked(WindowManagerImpl.java:356)at
android.view.WindowManagerImpl.removeView(WindowManagerImpl.java:201)at
android.view.Window$LocalWindowManager.removeView(Window.java:400)at
android.app.Dialog.dismissDialog(Dialog.java:268)at
android.app.Dialog.access$000(Dialog.java:69)at
android.app.Dialog$1.run(Dialog.java:103)at
android.app.Dialog.dismiss(Dialog.java:252)
发生频率:★★★★★
发生这类Exception的场景是,有一个费时的线程任务,在任务开始的时候显示一个对话框,然后当任务完成了再销毁对话框,在此期间如果Activity因为某种原因被杀掉且又重新启动了,那么当Dialog调用dismiss方法的时候WindowManager检查发现Dialog所属的Activity已经不存在了,所以会报View not attached to window manager。 [1]
要想避免此类Exception,就要正确的使用对话框,也要正确的使用线程,有以下几点需要注意。
1)正确使用对话框。不要在非UI线程中使用对话框创建,显示和取消对话框。
那么对于异步操作显示对话框怎么办呢?Activity都有相应的操作对话框的回调,比如:
·onCreateDialog()
·showDialog()
·dimissDialog()
·removeDialog()
以上这些都是Activity的方法,因此使用起来更方便,也不用显示创建和操控Dialog对象,一切都由框架操控,相对来说比较安全。
2)一定要让对话框对象在Activity的可控制范围之内和生命周期之内。比如对话框一定要是Activity的成员变量,并且在让对话框变量活跃在Activity的onCreate()和onDestroy()这两个方法之间。
写一个引发此Crash的例子:
private ProgressDialog mProgressDialog; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_s10); mProgressDialog = new ProgressDialog(this); mProgressDialog.show(); new Handler().postDelayed(new Runnable() { @Override public void run() { mProgressDialog.dismiss(); } }, 1000); finish(); }
后来我在网上看到另一种完美的解决方案 [2] :从ProgressDialog中派生出SafeProgress-Dialog子类,通过覆写dismiss方法,在ProgressDialog.dismiss方法执行之前判断Activity是否存在。
class SafeProgressDialog extends ProgressDialog { Activity mParentActivity; public SafeProgressDialog(Context context) { super(context); mParentActivity = (Activity) context; } @Override public void dismiss() { if (mParentActivity != null && !mParentActivity.isFinishing()) { super.dismiss(); } } }
6.5.3 窗体在不恰当的时候获取了焦点
异常中的关键字:
java.lang.NullPointerException:android.widget.PopupWindow$PopupViewCo ntainer.dispatchKeyEvent
发生频率:★★
这个问题是因为在PopupWindow显示之前,就把焦点赋予了它,结果当然会Crash了。
这类问题只在Android 2.3版本才会偶然出现,我看到Android系统4.0的源码修改了方法,在底层对这个问题进行了规避。 [3]
但是对于2.3的Android系统,我们还是要进行兼容。相应的解决方法是,在创建PopupWindow的时候不立即调用setFocusable(true),而是在showAtLocation后再调用setFocusable(true),同时,在调用dismiss的时候,调用setFocusable(false)。 [4]
注意
PopupWindow调用setFocusable(true)是为了让它里面的控件能够实现监听事件。
6.5.4 token null is not for an application
异常中的关键字:
android.view.WindowManager$BadTokenException:Unable to add window--token null is not for an application
发生频率:★★★
在实现Android浮窗时,有时会报这个异常,根据以往的经验,出现这问题一般是我们的Context不正确。以下代码会报这个异常:
new AlertDialog.Builder(getApplicationContext()) .setIcon(android.R.drawable.ic_dialog_alert) .setTitle("Warnning") .setMessage("Hello world!") .show();
问题出在AlertDialog.Builder(mcontext)这句话,所接受的参数不能是getApplication-Context()获得的Context,而应该是Activity实例,因为只有一个Activity才能添加一个窗体,如下所示:
new AlertDialog.Builder(S4Activity.this) .setIcon(android.R.drawable.ic_dialog_alert) .setTitle("Warnning") .setMessage("Hello world!") .show();
6.5.5 permission denied for this window type
异常中的关键字:
Android.view.WindowManager$BadTokenException:Unable to add window android.view.ViewRootImpl$W@411da608--permission denied for this window type
发生频率:★★★
在使用WindowManager.LayoutParams.TYPE_SYSTEM_ALERT涉及window type权限问题。
这种错误多发生在使用WindowManager自定义弹出框时,没有设置权限。
相应的解决方案是,在AndroidManifest.xml配置文件中添加以下两个uses-permission:
<!-- 显示系统窗口权限 --> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> <!-- 在屏幕最顶部显示 addview --> <uses-permission android:name="android.permission.SYSTEM_OVERLAY_WINDOW" />
前者允许应用使用TYPE_SYSTEM_ALERT来打开窗口,并将窗口显示于其他应用的顶端;后者允许使用窗体覆盖在window上。
6.5.6 is your activity running
异常中的关键字:
android.view.WindowManager$BadTokenException:Unable to add window--token android.app.LocalActivityManager$LocalActivityRecord@45a58ee0 is not valid;is your activity running?
发生频率:★★★★
当我回来,你已不在。说的就是这个Crash。
这种Crash与弹出框Dialog密切相关,是由于Activity A依附于另一个Activity B的,当被依附的Activity B产生错误的时候,Activity A因为没有了靠山而产生错误(或者是调用了一个已经被finish()的Activity)。
比如,在onCreate方法中,想要弹出PopupWindow,如下所示:
public class S6CrashActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_s6_crash); PopupWindow popupWindow = new PopupWindow( getLayoutInfiater().infiate( R.layout.activity_s6_crash, null), ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); popupWindow.showAtLocation( findViewById(R.id.btnScenario1), Gravity.CENTER, 0, 0); popupWindow.update(); } }
我们看一下PopupWindow的showAtLocation方法:
void android.widget.PopupWindow.showAtLocation( View parent, int gravity, int x, int y)
当参数parent为空时,就会报上述的错误,说token为空了,无效了,由于popupwindow要依附于一个activity,而activity的onCreate()还没执行完,那么肯定会出错了。
因此,我们要做的就是让这个showAtLocation的调用再晚一点,这里使用handler来解决这个问题,如下所示:
public class S6CrashFixActivity extends Activity { private PopupWindow popupWindow; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); popupWindow = new PopupWindow(getLayoutInfiater().infiate( R.layout.activity_s6, null), WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT); new Thread() { public void run() { try { handler.sendEmptyMessageDelayed(0, 1000); } catch (Exception e) { e.printStackTrace(); } } }.start(); } private Handler handler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case 1000: popupWindow.showAtLocation( findViewById(R.id.btnScenario1), Gravity.CENTER | Gravity.CENTER, 0, 0); popupWindow.update(); } super.handleMessage(msg); } }; }
6.5.7 添加窗体失败
异常中的关键字:
java.lang.RuntimeException:Adding window failed at
android.view.ViewRootImpl.setView(ViewRootImpl.java:511)at
android.view.WindowManagerImpl.addView(WindowManagerImpl.java:301)at
android.view.WindowManagerImpl.addView(WindowManagerImpl.java:215)at……
发生频率:★
这个Crash我不能复现,只能在线上看到异常信息。不知道发生的原因,也暂时没有解决方案。
检查Android系统源码,这个Crash是在ViewRoot的setView方法中捕获到的,如下所示:
try { res = sWindowSession.add(mWindow, mWindowAttributes, getHostVisibility(), mAttachInfo.mContentInsets); } catch(RemoteException ex) { mAdded = false; mView = null; mAttachInfo.mRootView = null; unscheduleTraversals(); throw new RuntimeException("Adding windows failed", ex); }
考虑到窗体类Crash的完整性,我没有把这个Crash归类到6.9不明觉厉中。还请知道其中缘由的朋友不吝赐教。
6.5.8 AlertDialog.resolveDialogTheme
异常中的关键字:
java.lang.NullPointerException at
android.app.AlertDialog.resolveDialogTheme(AlertDialog.java:142)at
android.app.AlertDialog$Builder.<init>(AlertDialog.java:359)at
com.radzik.devadmin.MainActivity$5.onClick(MainActivity.java:140)at
android.view.View.performClick(View.java:4084)……
发生频率:★★★
这是一个很有趣的异常。我在重现is your activity running这个异常时,阴差阳错发现了这个新的异常,上网一查,这类Crash发生次数还是蛮多的。
场景1: 在B页面写了一个show方法,控制AlertDialog.Builder的弹出和隐藏。在A页面却要调用B页面的show方法,于是就崩溃了,代码如下所示:
public class S8Activity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_s8); Button btnCrash1 = (Button) findViewById(R.id.btnCrash1); btnCrash1.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { AnotherS8Activity s8 = new AnotherS8Activity(); s8.show(); } }); } } public class AnotherS8Activity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); } public void show() { AlertDialog.Builder dialog = new AlertDialog.Builder( AnotherS8Activity.this); dialog.setTitle("Test"); dialog.setMessage("Hello World"); dialog.setPositiveButton("OK", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.cancel(); } }); dialog.show(); } }
这种崩溃的解决方案有以下几种:
·最简单的解决方案,就是把AnotherActivity中的show方法,复制到S8Activity中。
·也可以把这个show方法放在BaseActivity中。
·创建一个单独的类,把AnotherActivity中的show方法转移过去,只要传递正确的context参数即可。
场景2: 在TabActivity中切换Tab时,容易产生这个Crash。这是因为,在new对话框的时候,参数content指定成了this,即指向当前子Activity的content。但子Activity是动态创建的,不能保证一直存在。其父Activity的content则是稳定存在的,所以将this替换为getParent()即可,如下代码所示:
@Override public void onTabChanged(String tagString) { if (tagString.equals("One")) { myMenuSettingTag = 1; ProgressDialog dialog = ProgressDialog.show( getParent() "提示 ", "正在获取数据,请稍等 _1", true, true); } if (tagString.equals("Two")) { myMenuSettingTag = 2; ProgressDialog dialog = ProgressDialog.show( S8CrashFixActivity.this, "提示 ", "正在获取数据,请稍等 _2", true, true); } if (tagString.equals("Three")) { myMenuSettingTag = 3; ProgressDialog dialog = ProgressDialog.show( S8CrashFixActivity.this, "提示 ", "正在获取数据,请稍等 _3", true, true); } if (myMenu != null) { onCreateOptionsMenu(myMenu); } }
6.5.9 The specified child already has a parent
异常中的关键字:
The specified child already has a parent.You must call removeView()on the child's parent first.
发生频率:★★★
这个异常,我们从字面上就能理解。在使用儿子的时候,要先调用其父亲的remove-View方法,解除父子关系。 [5]
我们在一个Activity中加载layout,一般这样写:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_s9);
但殊不知,换个写法也能达到同样的效果:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); LayoutInfiater infiater = (LayoutInfiater) getSystemService(LAYOUT_INFLATER_SERVICE); LinearLayout parent = (LinearLayout) infiater.infiate( R.layout.activity_s9_crash, null); setContentView(parent); }
我们尝试着改写setContent方法的内容,从layout布局中抓取它的子控件ImageView,当我们把ImageView放到setContent方法中时,就会报上述的错误了:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); LayoutInfiater infiater = (LayoutInfiater) getSystemService(LAYOUT_INFLATER_SERVICE); LinearLayout parent = (LinearLayout) infiater.infiate( R.layout.activity_s9_crash, null); ImageView child = (ImageView)parent.findViewById( R.id.imageView1); setContentView(child); }
这是因为ImageView是其所在layout的儿子,它必须跟它的父亲(parent)共存亡,除非我们使用removeView先把它从其父亲中移除,如下所示:
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); LayoutInfiater infiater = (LayoutInfiater) getSystemService(LAYOUT_INFLATER_SERVICE); LinearLayout parent = (LinearLayout) infiater.infiate( R.layout.activity_s9_crash, null); ImageView child = (ImageView) parent.findViewById( R.id.imageView1); parent.removeView(child); setContentView(child); }
6.5.10 子线程不能修改UI
异常中的关键字:
android.view.ViewRootImpl$CalledFromWrongThreadException:Only the original thread that created a view hierarchy can touch its views.……
发生频率:★★★★★
从字面上翻译是,只有原始创建这个视图层次(view hierarchy)的线程才能修改它的视图(view)。也就是说必须在程序的主线程(UI)线程中更新界面显示的工作。
话虽如此,但是我写了一个Demo,试图在子线程中更新TextView中的值,如下所示:
public class Scenario1Activity extends Activity { TextView mLoadingText; Button btnStartThread; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_s11_scenario1); mLoadingText = (TextView) findViewById(R.id.textView1); btnStartThread = (Button) findViewById(R.id.btnStartThread); new Thread(new Runnable() { @Override public void run() { mLoadingText.setText("hello world"); } }).start(); } }
但是奇迹出现了,居然能运行良好,不会有崩溃。这不由得使我对之前从书本上看到的概念产生了怀疑,不是说在子线程操作UI就会崩溃吗?
后来我加了1秒的等待时间,然后再修改TextView上的值,这个Crash就能稳定复现了(如果不能复现就把时间拉长到2~5秒),代码如下所示:
public class Scenario2Activity extends Activity { TextView mLoadingText; Button btnStartThread; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_s11_scenario2); mLoadingText = (TextView) findViewById(R.id.textView1); btnStartThread = (Button) findViewById(R.id.btnStartThread); // 在 onCreate方法中执行不会 Crash new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } // 刷新页面的文字 mLoadingText.setText("hello world"); } }).start(); } }
继续探索,在按钮点击事件中重复刚才的试验,Crash稳定重现:
btnStartThread.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { new Thread(new Runnable() { @Override public void run() { // 刷新页面的文字 mLoadingText.setText("hello"); } }).start(); } });
于是我重新检查了Android的定义,发现是自己对这句话有理解上的误区:“不建议在子线程中更新UI,会因此而产生不可预知的错误。”
这就是多线程编程,有时候你运行若干次,结果正确,并不表明你的逻辑就是对的。我们一定要遵循代码的规范,保持清晰的思维。
接下来解释一下在onCreate方法中操作UI为什么有时候不崩溃?就像前面所说,一定要等一会儿才会出现崩溃,肯定是这段时间内某种检查机制还没起作用,晚于后续对UI的操作。检查Android源码,这个方法是viewRoot的requestLayout()。只有在requestLayout方法的子方法checkThread中,才会抛出这个异常。
public void requestLayout() { checkThread(); mLayoutRequested = true; scheduleTraversals(); } void checkThread() { if(mThread != Thread.currentThread()) { throw new CalledFromWrongThreadException( "Only the original thread that created a view hierarchy can touch its views."); } }
由此而推测,在onCreate的时候,是requestLayout方法没有执行——layout布局文件还没有创建完成,导致我们可以在onCreate方法内在其他子线程中操作UI。
问题查出来了,接下来是如何正确解决问题,因为有时会碰到在非主UI线程更新视图的需要。这个时候我们有两种处理的方式。一种是Handler,另一种是Activity中的runOnUiThread(Runnable)方法。
方法1: 使用Handler。
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_s11_scenario4); mLoadhandler = new LoadHandler(); mLoadingText = (TextView) findViewById(R.id.textView1); btnStartThread = (Button) findViewById(R.id.btnStartThread); btnStartThread.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { new Thread(new Runnable() { @Override public void run() { mLoadhandler.sendEmptyMessage(101); } }).start(); } }); } // 主线程中的 handler class LoadHandler extends Handler { // 接受子线程传递的消息机制 @Override public void handleMessage(Message msg) { super.handleMessage(msg); int what = msg.what; switch (what) { case 101: { // 刷新页面的文字 mLoadingText.setText("test"); break; } } } }
方法2: 利用Activity的runOnUiThread方法把更新UI的代码创建在Runnable中,这样Runnable对像就能在UI程序中被调用。如果当前线程是UI线程,那么行动是立即执行。如果当前线程不是UI线程,操作是发布到事件队列的线程中。
public void onClick(View v) { runOnUiThread(new Runnable() { public void run() { // 刷新页面的文字 mLoadingText.setText("test"); } }); }
方法3: 使用AsyncTask。
private class MyTask extends AsyncTask<Void, Void, Void> { @Override protected Void doInBackground(Void... params) { publishProgress(); return null; } @Override protected void onProgressUpdate(Void... values) { super.onProgressUpdate(values); // 刷新页面的文字 mLoadingText.setText("test"); } @Override protected void onPostExecute(Void result) { // 刷新页面的文字 mLoadingText.setText("test2"); super.onPostExecute(result); } }
简单介绍一下这三个方法:
·onProgressUpdate方法的执行在收到publishProgress方法调用后,运行于UI线程中,对UI控件进行处理。
·onPostExecute()方法,则在doInBackground()方法结束后运行在UI线程,对result进行处理。
·doInBackground()方法中,就是在后台线程中处理我们的异步任务,不能做类似Toast的操作,同样会抛出Can't create handler inside thread that has not called Looper.prepare()异常。
接下来,在使用的时候就很简单了:
btnStartThread.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { MyTask myTask = new MyTask(); myTask.execute(); } });
6.5.11 不能在子线程操作AlertDialog和Toast
异常中的关键字:
Can't create handler inside thread that has not called Looper.prepare()
发生频率:★★★★★
我们继续讨论在子线程操作UI的事情。这次是要显示弹出框AlertDialog和吐司Toast。
AlertDialog,只要是在子线程中操作它,就会报上述的错误信息。我测试过,无论是在onCreate()还是按钮的点击方法中,都是一样:
btnStartThread1.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { new Thread(new Runnable() { @Override public void run() { new AlertDialog.Builder(S12Activity.this) .setTitle("标题 ") .setMessage("简单消息框 ") .setPositiveButton("确定 ", null).show(); } }).start(); } });
相应的解决方案有多种:
方案1: 在外面包一层Looper.prepare()和Looper.loop(),如下所示:
btnStartThread2.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { new Thread(new Runnable() { @Override public void run() { Looper.prepare(); new AlertDialog.Builder(S12Activity.this) .setTitle("标题 ") .setMessage("简单消息框 ") .setPositiveButton("确定 ", null).show(); Looper.loop(); } }).start(); } });
方案2: Looper的变形
btnStartThread3.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { new Thread(new Runnable() { @Override public void run() { showAlertByRunnable(S12Activity.this, "", 101); } }).start(); } }); private void showAlertByRunnable(final Context context, final CharSequence text, final int duration) { Handler handler = new Handler(Looper.getMainLooper()); handler.post(new Runnable() { @Override public void run() { new AlertDialog.Builder(S12Activity.this) .setTitle("标题 ") .setMessage("简单消息框 ") .setPositiveButton("确定 ", null) .show(); } }); }
我试过其他三个方案:Handler、runOnUiThread或Async,也能解决AlertDialog的问题,详细内容请参考6.5.10节,但是Looper的解决方案,针对操作UI控件却是无效的。
吐司Toast,这个控件和弹出框AlertDialog是一样的问题和解决方案,这里不再赘述。代码参见我博客上的源码。 [6]
[1] 有关这个Crash的更详细描述,请参见http://blog.csdn.net/yihongyuelan/article/details/9829313。
[2] 该解决方案摘自码农场的这篇文章:http://www.hankcs.com/program/mobiledev/solution-java-lang-illegalar-gumentexception-view-not-attached-to-window-manager.html。
[3] 参考http://stackoverf iow.com/questions/7768728/popupwindow-crash-on-dispatch-event和http://www.eoeandroid.com/ thread-109193-1-1.html这两篇文章。
[4] 可参考http://www.cnblogs.com/loulijun/p/3267958.html。
[5] 关于这个异常的分析,还有一篇文章,http://blog.csdn.net/lissdy/article/details/8453433,我不能复现,仅供参考。
[6] 代码下载地址:http://www.cnblogs.com/Jax/p/4656789.html。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论