Snackbar源码分析
目录介绍
- 1.最简单创造方法
- 1.1 Snackbar作用
- 1.2 最简单的创建
- 1.3 Snackbar消失的几种方式
- 2.源码分析
- 2.1 Snackbar的make方法源码分析
- 2.2 对Snackbar属性进行设置
- 2.3 Snackbar的show显示与点击消失
- 2.4 显示和隐藏中动画源码分析
- 3.经典总结
- 3.1 Snackbar和SnackbarManager类的设计
- 4.思考问题分析
- 4.1 Snackbar的设计思路
- 4.2 什么时候Snackbar显示会导致FloatingActionButton上移
- 4.3 Snackbar控件show时为何从下往上移出来
- 4.4 为什么Snackbar总是显示在最下面
- 4.5 Snackbar与吐司有何区别
- 5.Snackbar封装库
好消息
- 博客笔记大汇总【16年3月到至今】,包括Java基础及深入知识点,Android技术博客,Python学习笔记等等,还包括平时开发中遇到的bug汇总,当然也在工作之余收集了大量的面试题,长期更新维护并且修正,持续完善……开源的文件是markdown格式的!同时也开源了生活博客,从12年起,积累共计47篇[近20万字],转载请注明出处,谢谢!
- 链接地址:github.com/yangchong21…
- 如果觉得好,可以star一下,谢谢!当然也欢迎提出建议,万事起于忽微,量变引起质变!
- Snackbar封装库项目地址:github.com/yangchong21…
- 02.Toast源码深度分析
- 最简单的创建,简单改造避免重复创建,show()方法源码分析,scheduleTimeoutLocked吐司如何自动销毁的,TN类中的消息机制是如何执行的,普通应用的Toast显示数量是有限制的,用代码解释为何Activity销毁后Toast仍会显示,Toast偶尔报错Unable to add window是如何产生的,Toast运行在子线程问题,Toast如何添加系统窗口的权限等等
- 03.DialogFragment源码分析
- 最简单的使用方法,onCreate(@Nullable Bundle savedInstanceState)源码分析,重点分析弹窗展示和销毁源码,使用中show()方法遇到的IllegalStateException分析
- 05.PopupWindow源码分析
- 显示PopupWindow,注意问题宽和高属性,showAsDropDown()源码,dismiss()源码分析,PopupWindow和Dialog有什么区别?为何弹窗点击一下就dismiss呢?
- 06.Snackbar源码分析
- 最简单的创建,Snackbar的make方法源码分析,Snackbar的show显示与点击消失源码分析,显示和隐藏中动画源码分析,Snackbar的设计思路,为什么Snackbar总是显示在最下面
- 07.弹窗常见问题
- DialogFragment使用中show()方法遇到的IllegalStateException,什么常见产生的?Toast偶尔报错Unable to add window,Toast运行在子线程导致崩溃如何解决?
1.最简单创造方法
1.1 Snackbar作用
- Snackbar是Android支持库中用于显示简单消息并且提供和用户的一个简单操作的一种弹出式提醒。当使用Snackbar时,提示会出现在消息最底部,通常含有一段信息和一个可点击的按钮。
- 同样作为消息提示,Snackbar相比于Toast而言,增加了一个用户操作,并且在同时弹出多个消息时,Snackbar会停止前一个,直接显示后一个,也就是说同一时刻只会有一个Snackbar在显示;而Toast则不然,如果不做特殊处理,那么同时可以有多个Toast出现;Snackbar相比于Dialog,操作更少,因为只有一个用户操作的接口,而Dialog最多可以设置三个,另外Snackbar的出现并不影响用户的继续操作,而Dialog则必须需要用户做出响应,所以相比Dialog,Snackbar更轻量。
1.2 最简单的创建
- 如下所示
Snackbar sb = Snackbar.make(v,"潇湘剑雨",Snackbar.LENGTH_LONG).setAction("删除吗?", new View.OnClickListener() {@Overridepublic void onClick(View v) {//点击了"是吗?"字符串操作ToastUtils.showRoundRectToast("逗比");}}).setActionTextColor(Color.RED).setText("杨充是个逗比").addCallback(new BaseTransientBottomBar.BaseCallback<Snackbar>() {@Overridepublic void onDismissed(Snackbar transientBottomBar, int event) {super.onDismissed(transientBottomBar, event);switch (event) {case Snackbar.Callback.DISMISS_EVENT_CONSECUTIVE:case Snackbar.Callback.DISMISS_EVENT_MANUAL:case Snackbar.Callback.DISMISS_EVENT_SWIPE:case Snackbar.Callback.DISMISS_EVENT_TIMEOUT:ToastUtils.showRoundRectToast("删除成功");break;case Snackbar.Callback.DISMISS_EVENT_ACTION:ToastUtils.showRoundRectToast("撤销了删除操作");break;}Log.d("MainActivity","onDismissed");}@Overridepublic void onShown(Snackbar transientBottomBar) {super.onShown(transientBottomBar);Log.d("MainActivity","onShown");}});
sb.show();
复制代码
1.3 Snackbar消失的几种方式
- Snackbar显示只有一种方式,那就是调用show()方法,但是消失有几种方式:时间到了自动消失、点击了右侧按钮消失、新的Snackbar出现导致旧的Snackbar消失、滑动消失或者通过调用dismiss()消失。
- 分别对应于Snackbar.Callback中的几个常量值。
- DISMISS_EVENT_ACTION:点击了右侧按钮导致消失
- DISMISS_EVENT_CONSECUTIVE:新的Snackbar出现导致旧的消失
- DISMISS_EVENT_MANUAL:调用了dismiss方法导致消失
- DISMISS_EVENT_SWIPE:滑动导致消失
- DISMISS_EVENT_TIMEOUT:设置的显示时间到了导致消失
- Callback有两个方法
- void onDismissed(B transientBottomBar, @DismissEvent int event)
- void onShown(B transientBottomBar)
- 其中onShown在Snackbar可见时调用,onDismissed在Snackbar准备消失时调用。
- 分别对应于Snackbar.Callback中的几个常量值。
2.源码分析
2.1 Snackbar的make方法源码分析
- 创建Snackbar需要使用静态的make方法,并且其中的view参数是一个查找父布局的起点
- 这里可以看到,snackBar的布局是design_layout_snackbar_include,假如我们需要自定义SnackBar并且设置字体颜色,大小等属性。则需要拿到这个布局的控件id等。关于封装库,可以查看:github.com/yangchong21…
- 其中findSuitableParent()方法为以view为起点寻找合适的父布局,下面看看findSuitableParent()如何做的?
- 看了下面源码可知:可以看到如果view是CoordinatorLayout,那么就直接作为父布局了;如果是FrameLayout,并且如果是android.R.id.content,也就是查找到了DecorView,即最顶部,那么就只用这个view;如果不是的话,先保存下来;接下来就是获取view的父布局,然后循环再次判断。这样导致的结果最终会有两个选择,要么是CoordinatorLayout,要么就是FrameLayout,并且是最顶层的那个布局。
- 如果从View往上搜寻,如果有CoordinatorLayout,那么就使用该CoordinatorLayout ;如果从View往上搜寻,没有CoordinatorLayout,那么就使用android.R.id.content的FrameLayout
2.2 对Snackbar属性进行设置
2.2.1 setActionTextColor设置action颜色
- 可以看到先是获取父布局contentLayout,然后在获取snackbar_action的mActionView
@NonNull public Snackbar setActionTextColor(@ColorInt int color) {final SnackbarContentLayout contentLayout = (SnackbarContentLayout) mView.getChildAt(0);final TextView tv = contentLayout.getActionView();tv.setTextColor(color);return this; }//然后看SnackbarContentLayout类中getActionView方法 @Override protected void onFinishInflate() {super.onFinishInflate();mMessageView = (TextView) findViewById(R.id.snackbar_text);mActionView = (Button) findViewById(R.id.snackbar_action); } public Button getActionView() {return mActionView; } 复制代码
2.2.2 看setAction()方法的实现
- 首先是获取父布局contentLayout,然后通过contentLayout调用getActionView()方法,返回的tv其实就是右边的Button,然后判断文本和监听器,设置可见性、文本、监听器。
@NonNull public Snackbar setAction(CharSequence text, final View.OnClickListener listener) {final SnackbarContentLayout contentLayout = (SnackbarContentLayout) mView.getChildAt(0);final TextView tv = contentLayout.getActionView();if (TextUtils.isEmpty(text) || listener == null) {tv.setVisibility(View.GONE);tv.setOnClickListener(null);} else {tv.setVisibility(View.VISIBLE);tv.setText(text);tv.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View view) {listener.onClick(view);// Now dismiss the SnackbardispatchDismiss(BaseCallback.DISMISS_EVENT_ACTION);}});}return this; } 复制代码
2.3 Snackbar的show显示与点击消失
- 2.3.1 show显示
- 可以看到,首先获取一个SnackbarManager对象,然后调用它的show方法。可以看到在这个方法中,先判断如果是当前正在显示的SnackBar对应的CallBack,则更新显示时长,然后从消息队列中移除,最后调用scheduleTimeoutLocked方法发送定时消息dismiss;如果是下一个要显示的,则更新显示时长;如果都不是,那么就创建一个SnackbarRecord对象。
- isCurrentSnackbarLocked:如果当前已经有一个Snackbar显示了,又再调用了该对象的show方法,但是只是设置了不同时间,那么isCurrentSnackbarLocked就会是true,执行里面的方法。
- isNextSnackbarLocked:如果当前已有一个Snackbar正在显示,又创建了一个新的Snackbar并调用show方法,则执行这个条件代码
- 如果两条件都不成立,则需要创建一个新记录并对其进行排队。
public void show() {SnackbarManager.getInstance().show(mDuration, mManagerCallback); }public void show(int duration, Callback callback) {synchronized (mLock) {if (isCurrentSnackbarLocked(callback)) {// 表示回调已在队列中。我们只需更新持续时间mCurrentSnackbar.duration = duration;// 如果这是当前正在显示的Snackbar,请调用重新调度它的// timeoutmHandler.removeCallbacksAndMessages(mCurrentSnackbar);// 这个方法很重要,当执行时间结束后,就会自动dismiss。下面再详细分析scheduleTimeoutLocked(mCurrentSnackbar);return;} else if (isNextSnackbarLocked(callback)) {//我们只需更新持续时间mNextSnackbar.duration = duration;} else {//否则,我们需要创建一个新记录并对其进行排队。mNextSnackbar = new SnackbarRecord(duration, callback);}if (mCurrentSnackbar != null && cancelSnackbarLocked(mCurrentSnackbar,Snackbar.Callback.DISMISS_EVENT_CONSECUTIVE)) {// 如果我们目前有一个Snackbar,请尝试取消它并排队等待。return;} else {// 清除当前的快捷键mCurrentSnackbar = null;//很重要showNextSnackbarLocked();}} }//注意这个callback方法 final SnackbarManager.Callback mManagerCallback = new SnackbarManager.Callback() {@Overridepublic void show() {sHandler.sendMessage(sHandler.obtainMessage(MSG_SHOW, BaseTransientBottomBar.this));}@Overridepublic void dismiss(int event) {sHandler.sendMessage(sHandler.obtainMessage(MSG_DISMISS, event, 0,BaseTransientBottomBar.this));} };//处理sHandler发送的消息 static {sHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {@Overridepublic boolean handleMessage(Message message) {switch (message.what) {case MSG_SHOW:((BaseTransientBottomBar) message.obj).showView();return true;case MSG_DISMISS:((BaseTransientBottomBar) message.obj).hideView(message.arg1);return true;}return false;}}); } 复制代码
- 然后看看showNextSnackbarLocked这个方法,注意:mCurrentSnackbar当前正在显示的,而mNextSnackbar是下一个要显示的。能看到会调用callback的show方法,而这个calllback对象就是我们在调用snackbar的show方法是传进去的那个。向Snackbar的Handler发送一个消息,最后显示Snackbar。
private void showNextSnackbarLocked() {if (mNextSnackbar != null) {mCurrentSnackbar = mNextSnackbar;mNextSnackbar = null;final Callback callback = mCurrentSnackbar.callback.get();if (callback != null) {callback.show();} else {// The callback doesn't exist any more, clear out the SnackbarmCurrentSnackbar = null;}} } 复制代码
- 2.3.2 看看scheduleTimeoutLocked源码如何销毁snackBar
- 可以发现,如果我们设置为无限期,则不会设置超时,直接return函数。然后发送了一个叫做MSG_TIMEOUT的消息,继续追终,最后会到达cancelSnackbarLocked方法。在cancelSnackbarLocked这个方法中,首先移除SnackbarRecord发出的所有消息,然后调用Callback的dismiss方法,从上面我们知道最终是向Snackbar的sHandler发送了一条消息,最终是调用Snackbar的hideView消失。
private void scheduleTimeoutLocked(SnackbarRecord r) {if (r.duration == Snackbar.LENGTH_INDEFINITE) {// If we're set to indefinite, we don't want to set a timeoutreturn;}int durationMs = LONG_DURATION_MS;if (r.duration > 0) {durationMs = r.duration;} else if (r.duration == Snackbar.LENGTH_SHORT) {durationMs = SHORT_DURATION_MS;}mHandler.removeCallbacksAndMessages(r);mHandler.sendMessageDelayed(Message.obtain(mHandler, MSG_TIMEOUT, r), durationMs); }//接受mHandler消息并且处理 mHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {@Overridepublic boolean handleMessage(Message message) {switch (message.what) {case MSG_TIMEOUT:handleTimeout((SnackbarRecord) message.obj);return true;}return false;} });// void handleTimeout(SnackbarRecord record) {synchronized (mLock) {if (mCurrentSnackbar == record || mNextSnackbar == record) {cancelSnackbarLocked(record, Snackbar.Callback.DISMISS_EVENT_TIMEOUT);}} }//最终可以追踪到这个方法 private boolean cancelSnackbarLocked(SnackbarRecord record, int event) {final Callback callback = record.callback.get();if (callback != null) {// Make sure we remove any timeouts for the SnackbarRecordmHandler.removeCallbacksAndMessages(record);callback.dismiss(event);return true;}return false; } 复制代码
2.4 显示和隐藏中动画源码分析
- 在显示的时候是这样设置动画的,具体如下所示
- 在隐藏的时候是这样设置动画的,具体如下所示
- 最后具体看一下animateViewOut部分源码
- 可以看到在动画结束的最后都调用了onViewHidden方法,所以最终都是要调用onViewHidden方法的。
private void animateViewOut(final int event) {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {ViewCompat.animate(mView).translationY(mView.getHeight()).setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR).setDuration(ANIMATION_DURATION).setListener(new ViewPropertyAnimatorListenerAdapter() {@Overridepublic void onAnimationStart(View view) {mContentViewCallback.animateContentOut(0, ANIMATION_FADE_DURATION);}@Overridepublic void onAnimationEnd(View view) {onViewHidden(event);}}).start();} else {Animation anim = AnimationUtils.loadAnimation(mView.getContext(),R.anim.design_snackbar_out);anim.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR);anim.setDuration(ANIMATION_DURATION);anim.setAnimationListener(new Animation.AnimationListener() {@Overridepublic void onAnimationEnd(Animation animation) {onViewHidden(event);}@Overridepublic void onAnimationStart(Animation animation) {}@Overridepublic void onAnimationRepeat(Animation animation) {}});mView.startAnimation(anim);} } 复制代码
- onViewHidden提供具体的业务处理,具体如下所示
- 首先调用SnackbarManager的onDismissed方法,然后判断Snackbar.Callback是不是null,调用Snackbar.Callback的onDismissed方法,就是我们上面介绍的处理Snackbar消失的方法。最后就是将Snackbar的mView移除。
3.经典总结
3.1 Snackbar和SnackbarManager类的设计
- Snackbar和SnackbarManager,SnackbarManager内部有两个SnackbarRecord,一个mCurrentSnackbar,一个mNextSnackbar,SnackbarManager通过这两个对象实现Snackbar的顺序显示,如果在一个Snackbar显示之前有Snackbar正在显示,那么使用mNextSnackbar保存第二个Snackbar,然后让第一个Snackbar消失,然后消失之后再调用SnackbarManager显示下一个Snackbar,如此循环,实现了Snackbar的顺序显示。
- Snackbar负责显示和消失,具体来说其实就是添加和移除View的过程。Snackbar和SnackbarManager的设计很巧妙,利用一个SnackbarRecord对象保存Snackbar的显示时间以及SnackbarManager.Callback对象,前面说到每一个Snackbar都有一个叫做mManagerCallback的SnackbarManager.Callback对象,下面看一下SnackRecord类的定义:
- Snackbar向SnackbarManager发送消息主要是调用SnackbarManager.getInstace()返回一个单例对象;而SnackManager向Snackbar发送消息就是通过show方法传入的Callback对象。SnackbarManager中的Handler只处理一个MSG_TIMEOUT事件,最后是调用Snackbar的hideView消失的;Snackbar的sHandler处理两个消息,showView和hideView,而消息的发送者是mManagerCallback,控制者是SnackbarManager。
4.思考问题分析
4.1 Snackbar的设计思路
- 具体可以看经典总结3.1
4.2 什么时候Snackbar显示会导致FloatingActionButton上移
- 为什么CoordinatorLayout + FloatingActionButton,当Snackbar显示的时候FloatingActionButton会上移呢,这个是怎么实现的?
- 把CoordinatorLayout替换成FrameLayout确不行。这个问题我们还没说。其实这个不是在Snackbar里面处理的,是通过CoordinatorLayout和Behavior来处理的。那具体的处理在哪里呢。FloatingActionButton类里面Behavior类。正是Behavior里面的两个函数layoutDependsOn()和onDependentViewChanged()函数作用的结果。直接进去看下FloatingActionButton内部类Behavior里面这两个函数的代码。
4.3 Snackbar控件show时为何从下往上移出来
- 至于说Snackbar控件show时为何从下往上移出来,看下面这段代码就知道呢,如下所示
4.4 为什么Snackbar总是显示在最下面
- 直接找到make方法中的填充布局,然后去看design_layout_snackbar_include的布局参数,结果如下:
4.5 Snackbar与吐司有何区别
- 与Toast进行比较,SnackBar有优势:
- 1.SnackBar可以自动消失,也可以手动取消(侧滑取消,但是需要在特殊的布局中,后面会仔细说)
- 2.SnackBar可以通过setAction()来与用户进行交互
- 3.通过CallBack我们可以获取SnackBar的状态
5.Snackbar封装库
- 可以一行代码调用,也可以自己使用链式编程调用。支持设置显示时长属性;可以设置背景色;可以设置文字大小,颜色;可以设置action内容,文字大小,颜色,还有点击事件;可以设置icon;代码如下所示,更多内容可以直接运行demo哦!
//1.只设置text SnackBarUtils.showSnackBar(this,"滚犊子");//2.设置text,action,和点击事件 SnackBarUtils.showSnackBar(this, "滚犊子", "ACTION", new View.OnClickListener() {@Overridepublic void onClick(View v) {ToastUtils.showRoundRectToast("滚犊子啦?");} });//3.设置text,action,和点击事件,和icon SnackBarUtils.showSnackBar(this, "滚犊子", "ACTION",R.drawable.icon_cancel, new View.OnClickListener() {@Overridepublic void onClick(View v) {ToastUtils.showRoundRectToast("滚犊子啦?");} });//4.链式调用 SnackBarUtils.builder().setBackgroundColor(this.getResources().getColor(R.color.color_7f000000)).setTextSize(14).setTextColor(this.getResources().getColor(R.color.white)).setTextTypefaceStyle(Typeface.BOLD).setText("滚犊子").setMaxLines(4).centerText().setActionText("收到").setActionTextColor(this.getResources().getColor(R.color.color_f25057)).setActionTextSize(16).setActionTextTypefaceStyle(Typeface.BOLD).setActionClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {ToastUtils.showRoundRectToast("滚犊子啦?");}}).setIcon(R.drawable.icon_cancel).setActivity(MainActivity.this).setDuration(SnackBarUtils.DurationType.LENGTH_INDEFINITE).build().show(); 复制代码
关于其他内容介绍
01.关于博客汇总链接
- 1.技术博客汇总
- 2.开源项目汇总
- 3.生活博客汇总
- 4.喜马拉雅音频汇总
- 5.其他汇总
02.关于我的博客
- 我的个人站点:www.yczbj.org,www.ycbjie.cn
- github:github.com/yangchong21…
- 知乎:www.zhihu.com/people/yang…
- 简书:www.jianshu.com/u/b7b2c6ed9…
- csdn:my.csdn.net/m0_37700275
- 喜马拉雅听书:www.ximalaya.com/zhubo/71989…
- 开源中国:my.oschina.net/zbj1618/blo…
- 泡在网上的日子:www.jcodecraeer.com/member/cont…
- 邮箱:yangchong211@163.com
- 阿里云博客:yq.aliyun.com/users/artic… 239.headeruserinfo.3.dT4bcV
- segmentfault头条:segmentfault.com/u/xiangjian…
Snackbar源码分析相关推荐
- android snackbar源码,Snackbar源码分析
目录介绍 1.最简单创造方法 1.1 Snackbar作用 1.2 最简单的创建 1.3 Snackbar消失的几种方式 2.源码分析 2.1 Snackbar的make方法源码分析 2.2 对Sna ...
- Dialog源码分析
目录介绍 1.简单用法 2.AlertDialog源码分析 2.1 AlertDialog.Builder的构造方法 2.2 通过AlertDialog.Builder对象设置属性 2.3 build ...
- DialogFragment源码分析
2019独角兽企业重金招聘Python工程师标准>>> 目录介绍 1.最简单的使用方法 1.1 官方建议 1.2 最简单的使用方法 1.3 DialogFragment做屏幕适配 2 ...
- flutter_bloc使用及部分源码分析
flutter_bloc 是一个bloc第三方库,这个库很方便的让你集成bloc模式,这个库结合了RXDart.目前我们项目中就有用到rxdart. bloc模式 BLoC是一种利用reactive ...
- 【Golang源码分析】Go Web常用程序包gorilla/mux的使用与源码简析
目录[阅读时间:约10分钟] 一.概述 二.对比: gorilla/mux与net/http DefaultServeMux 三.简单使用 四.源码简析 1.NewRouter函数 2.HandleF ...
- SpringBoot-web开发(四): SpringMVC的拓展、接管(源码分析)
[SpringBoot-web系列]前文: SpringBoot-web开发(一): 静态资源的导入(源码分析) SpringBoot-web开发(二): 页面和图标定制(源码分析) SpringBo ...
- SpringBoot-web开发(二): 页面和图标定制(源码分析)
[SpringBoot-web系列]前文: SpringBoot-web开发(一): 静态资源的导入(源码分析) 目录 一.首页 1. 源码分析 2. 访问首页测试 二.动态页面 1. 动态资源目录t ...
- SpringBoot-web开发(一): 静态资源的导入(源码分析)
目录 方式一:通过WebJars 1. 什么是webjars? 2. webjars的使用 3. webjars结构 4. 解析源码 5. 测试访问 方式二:放入静态资源目录 1. 源码分析 2. 测 ...
- Yolov3Yolov4网络结构与源码分析
Yolov3&Yolov4网络结构与源码分析 从2018年Yolov3年提出的两年后,在原作者声名放弃更新Yolo算法后,俄罗斯的Alexey大神扛起了Yolov4的大旗. 文章目录 论文汇总 ...
最新文章
- 从 Blast2GO 本地化聊一聊 Linux 下 MySQL 的源码安装
- 客户端网络pomelo学习笔记 (3) node.js 与 c 客户端 Diffie-Hellman 密钥交换算法的实现客户端网络...
- ToString格式化
- Docker入门与实战
- Airflow 中文文档:安装
- 鸿蒙眼镜怎么样,Babiators儿童太阳镜怎么样 Babiators儿童太阳镜测评
- 《精通软件性能测试与LoadRunner最佳实战》—第1章1.1节软件测试基础
- Tweet button with a callback – How to?
- 基于vue+node的校园交流平台
- 使用Mathcad解受迫振动微分方程并画图
- VB2010网络通信服务器
- 电影海报的字体如何设计?——黎乙丙
- 用友u8服务器修改ipv4,用友U8-OA11.1 用友U8加密狗更换服务器了-用友U8
- 一只小白,在学习delphi.感觉很吃力。。
- 一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
- mysql 主从服务-主从复制数据一致性校验出现的问题
- js判断Lodop驱动是否安装
- UPC——2020年春混合个人训练第二十五场(FG)
- 微信小程序image加载图片失败的处理方法
- 自制USB wifi信号放大天线