源码看CoordinatorLayout.Behavior原理
在上一篇博客CoordinatorLayout高级用法-自定义Behavior中,我们介绍了如何去自定义一个CoordinatorLayout的Behavior,通过文章也可以看出Behavior在CoordinatorLayout中地位是相当高的,那么今天我们就来接着上篇博客来从源码分析一下Behavior的实现思路,如果你对CoordinatorLayout和Behavior还不熟悉的话,建议先去看看上篇博客《CoordinatorLayout高级用法-自定义Behavior》。
这篇文章我们要分析的内容有:
- Behavior的实例化
- layoutDependsOn和onDependentViewChanged调用过程
- onStartNestedScroll和onNestedPreScroll实现原理
- Behavior的事件分发过程
Behavior的实例化
大家都知道,我们在view中可以通过app:layout_behavior
然后指定一个字符串来表示使用哪个behavior,稍微去想一下,在CoordinatorLayout中肯定是利用反射机制来完成的behavior的实例化,现在就让我们从CoordinatorLayout的源码中找寻答案,来验证我们的猜想。首先,我们来看看CoordinatorLayout的一个内部类,也是大家熟悉的LayoutParams
,
public static class LayoutParams extends ViewGroup.MarginLayoutParams {/*** A {@link Behavior} that the child view should obey.*/Behavior mBehavior;...
}
在这里我们确实看到了behavior的影子,那它是在什么时候被初始化的呢?继续看代码,
LayoutParams(Context context, AttributeSet attrs) {super(context, attrs);final TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.CoordinatorLayout_LayoutParams);...mBehaviorResolved = a.hasValue(R.styleable.CoordinatorLayout_LayoutParams_layout_behavior);if (mBehaviorResolved) {mBehavior = parseBehavior(context, attrs, a.getString(R.styleable.CoordinatorLayout_LayoutParams_layout_behavior));}a.recycle();
}
在LayoutParams的构造方法中,首先是去检查了是不是有layout_behavior
,这里很容易理解,接下来调用了parseBehavior
方法,返回了Behavior的实例,我们非常有理由去看看parseBehavior
到底干了嘛,或许我们要的答案就在里面!
// 这里是指定的Behavior的参数类型
static final Class<?>[] CONSTRUCTOR_PARAMS = new Class<?>[] {Context.class,AttributeSet.class
};...static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {if (TextUtils.isEmpty(name)) {return null;}// 代表了我们指定的那个behavior的完整路径final String fullName;// 如果是".MyBehavior"// 则在前面加上程序的包名if (name.startsWith(".")) {// Relative to the app package. Prepend the app package name.fullName = context.getPackageName() + name;} else if (name.indexOf('.') >= 0) {// 这里我们指定了全名// Fully qualified package name.fullName = name;} else {// Assume stock behavior in this package (if we have one)fullName = !TextUtils.isEmpty(WIDGET_PACKAGE_NAME)? (WIDGET_PACKAGE_NAME + '.' + name): name;}try {Map<String, Constructor<Behavior>> constructors = sConstructors.get();if (constructors == null) {constructors = new HashMap<>();sConstructors.set(constructors);}Constructor<Behavior> c = constructors.get(fullName);// 这里利用反射去实例化了指定的Behavior// 并且值得注意到是,这里指定了构造的参数类型// 也就是说我们在自定义Behavior的时候,必须要有这种类型的构造方法if (c == null) {final Class<Behavior> clazz = (Class<Behavior>) Class.forName(fullName, true,context.getClassLoader());c = clazz.getConstructor(CONSTRUCTOR_PARAMS);c.setAccessible(true);constructors.put(fullName, c);}return c.newInstance(context, attrs);} catch (Exception e) {throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);}
}
上面的代码很容易理解,就是利用反射机制去实例化了Behavior,调用的是两个参数的那个构造方法,这也就是我们在自定义Behavior的时候为什么一定要去重写,
public Behavior(Context context, AttributeSet attrs) {super(context, attrs);}
这个构造的原因。看来获取一个Behavior的实例还是很简单的,那么,下面就让我们开始分析Behavior中常用方法调用的机制吧。
layoutDependsOn和onDependentViewChanged调用过程
在上一篇博客中我们学会了自定义两种形式的Behavior,其中第一种就是去观察一个view的状态变化,也就是涉及到layoutDependsOn
和onDependentViewChanged
两个方法的调用,现在我们从源码的角度来分析一下这两个方法调用的时机和调用的过程,在前一篇博客中我们提到过onDependentViewChanged
这个方法会在view的状态发生变化后去调用,那在状态发生变化时必定会执行什么操作呢?重绘,是的,状态变化了,那肯定重绘是避免不了的,在CoordinatorLayout
中注册了一个ViewTreeObserver
,我们可以从这里入手,因为它可以监听到view的各种状态变化,
@Override
public void onAttachedToWindow() {super.onAttachedToWindow();resetTouchBehaviors();if (mNeedsPreDrawListener) {if (mOnPreDrawListener == null) {// 实例化了OnPreDrawListener// 并在下面注册到了ViewTreeObserver中mOnPreDrawListener = new OnPreDrawListener();}final ViewTreeObserver vto = getViewTreeObserver();vto.addOnPreDrawListener(mOnPreDrawListener);}if (mLastInsets == null && ViewCompat.getFitsSystemWindows(this)) {// We're set to fitSystemWindows but we haven't had any insets yet...// We should request a new dispatch of window insetsViewCompat.requestApplyInsets(this);}mIsAttachedToWindow = true;
}
在onAttachedToWindow
向ViewTreeObserver注册了一个监听draw变化的Observer,那在这里Observer中到底干了嘛呢?
class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {@Overridepublic boolean onPreDraw() {dispatchOnDependentViewChanged(false);return true;}
}
就两行代码,调用了dispatchOnDependentViewChanged
方法,看方法名我们就知道这次找对对象了,怀着激动的心情来看看dispatchOnDependentViewChanged
void dispatchOnDependentViewChanged(final boolean fromNestedScroll) {final int layoutDirection = ViewCompat.getLayoutDirection(this);final int childCount = mDependencySortedChildren.size();// 遍历所有的子viewfor (int i = 0; i < childCount; i++) {final View child = mDependencySortedChildren.get(i);final LayoutParams lp = (LayoutParams) child.getLayoutParams();...// Did it change? if not continue// 检查是否变化了,没有变化直接下一次循环final Rect oldRect = mTempRect1;final Rect newRect = mTempRect2;getLastChildRect(child, oldRect);getChildRect(child, true, newRect);if (oldRect.equals(newRect)) {continue;}// Update any behavior-dependent views for the change// 这里从下一个子view开始//mDependencySortedChildren有一个排序规则// selectionSort// 感兴趣的可以看一下mDependencySortedChildren部分。for (int j = i + 1; j < childCount; j++) {final View checkChild = mDependencySortedChildren.get(j);final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();// 获取到Behaviorfinal Behavior b = checkLp.getBehavior();// 这里调用Behavior的layoutDependsOn来判断我们的带有behavior的view是不是依赖这个viewif (b != null && b.layoutDependsOn(this, checkChild, child)) {if (!fromNestedScroll && checkLp.getChangedAfterNestedScroll()) {// If this is not from a nested scroll and we have already been changed// from a nested scroll, skip the dispatch and reset the flagcheckLp.resetChangedAfterNestedScroll();continue;}// 这里调用了Behavior的onDependentViewChangedfinal boolean handled = b.onDependentViewChanged(this, checkChild, child);...}}}
}
dispatchOnDependentViewChanged
方法有一个布尔类型的参数,上面我们传递的是false, 这里主要是区分是view引起的状态变化还是布局引起的,在一些的scroll中也会调用dispatchOnDependentViewChanged
这个方法。
好了,现在我们终于搞懂了onDependentViewChanged
调用机制了,下面我们来看看关于滑动监听的部分。
onStartNestedScroll和onNestedPreScroll实现原理
在开始源码之前,我们先来思考个问题,现在有一个view是可以上下滑动的,那这个view的滑动对于父view来说是不是可见的?或者说是可预知的?显然不是,一个view的滑动对于父布局来说是透明的?所以现在我们不能简简单单的从CoordinatorLayout
入手了,而是要从那个可以滑动的view入手,我们选择NestedScrollView
来进行分析。NestedScrollView
有一个NestedScrollingChildHelper
类型的变量mChildHelper
引起了我们的注意,因为很多看名字很像关于滑动部分的代码都调用了这个类的一些方法,来看看有哪些吧?
mChildHelper = new NestedScrollingChildHelper(this);...@Override
public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) {mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes);startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);
}@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed,int dyUnconsumed) {final int oldScrollY = getScrollY();scrollBy(0, dyUnconsumed);final int myConsumed = getScrollY() - oldScrollY;final int myUnconsumed = dyUnconsumed - myConsumed;dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null);
}@Override
public boolean startNestedScroll(int axes) {return mChildHelper.startNestedScroll(axes);
}@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,int dyUnconsumed, int[] offsetInWindow) {return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed,offsetInWindow);
}@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
很简单,不过我们好像发现了一点眉目,这些方法何时调用我们还是不是很清楚,滑动必然和事件有关,我们就来从事件的部分入手吧,毕竟是我们熟悉的地方。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {...switch (action & MotionEventCompat.ACTION_MASK) {...case MotionEvent.ACTION_DOWN: {...startNestedScroll(ViewCompat.SCROLL_AXIS_VERTICAL);}...}...
}
在down的时候我们调用了startNestedScroll
方法,那我们就顺着这条线往下看mChildHelper.startNestedScroll(axes)
。
public boolean startNestedScroll(int axes) {if (hasNestedScrollingParent()) {// Already in progressreturn true;}if (isNestedScrollingEnabled()) {// 获取当前view的parentViewParent p = mView.getParent();View child = mView;// 一个循环,不断的往上层去获取parent// 直到条件成立,或者没有parent了 退出while (p != null) {// 这里是关键代码,猜测这里肯定肯定去调用了CoordinatorLayout的对应方法。if (ViewParentCompat.onStartNestedScroll(p, child, mView, axes)) {mNestedScrollingParent = p;ViewParentCompat.onNestedScrollAccepted(p, child, mView, axes);return true;}if (p instanceof View) {child = (View) p;}// 替换,继续循环p = p.getParent();}}return false;
}
在这个方法中一个while循环,不断的去获取view的的parent,然后一个ViewParentCompat.onStartNestedScroll
作为条件成立了就return true了,我们有理由猜测ViewParentCompat.onStartNestedScroll
里去调用了CoordinatorLayout
的相应方法。注意参数,p是我们遍历到父view,我们先认为是CoordinatorLayout
吧,child是CoordinatorLayout
的直接嵌套着目标view的子view,mView在这里就是NestedScrollView
了。
public class ViewParentCompat {static class ViewParentCompatStubImpl implements ViewParentCompatImpl {@Overridepublic boolean onStartNestedScroll(ViewParent parent, View child, View target,int nestedScrollAxes) {if (parent instanceof NestedScrollingParent) {return ((NestedScrollingParent) parent).onStartNestedScroll(child, target,nestedScrollAxes);}return false;}}
}
这里面很简单,看看parent是不是NestedScrollingParent
类型的,如果是,则调用了onStartNestedScroll
这个方法,而我们的CoordinatorLayout
肯定是实现了NestedScrollingParent
接口的,
public class CoordinatorLayout extends ViewGroup implements NestedScrollingParent { }
好了,现在我们终于回到CoordinatorLayout
了,来看看他的onStartNestedScroll
方法,
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {boolean handled = false;final int childCount = getChildCount();for (int i = 0; i < childCount; i++) {final View view = getChildAt(i);final LayoutParams lp = (LayoutParams) view.getLayoutParams();final Behavior viewBehavior = lp.getBehavior();if (viewBehavior != null) {// 调用遍历出来的这个子view的onStartNestedScroll方法final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child, target,nestedScrollAxes);handled |= accepted;lp.acceptNestedScroll(accepted);} else {lp.acceptNestedScroll(false);}}return handled;
}
这里还是去遍历了所有子view,然后去调用它的onStartNestedScroll
方法,它的返回值,决定了NestedScrollingChildHelper.onStartNestedScroll
是不是要继续遍历,如果我们的子view对这个view的滑动感兴趣,就返回true,它的遍历就会结束掉。
好了,现在start的过程我们分析完了,大体的流程就是:
NestedScrollView.onInterceptTouchEvent->NestedScrollingChildHelper.onStartNestedScroll->CoordinatorLayout.onStartNestedScroll
下面的各种滑动调用流程也是一样的,这里我们就不再重复分析了,感兴趣的可以自己去看一下源码。
Behavior的事件分发过程
上面的分析其实已经将我们自定义Behavior中使用到的方法的调用流程分析完了,不过我们还是要拓展一下,其实Behavior也是支持事件的传递的,在这方面,Behavior好像是一个代理一样,在CoordinatorLayout的各种事件处理的方法中去调用Behavior的事件处理方法,返回值决定了CoordinatorLayout对事件的消费情况。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {MotionEvent cancelEvent = null;final int action = MotionEventCompat.getActionMasked(ev);// Make sure we reset in case we had missed a previous important event.if (action == MotionEvent.ACTION_DOWN) {resetTouchBehaviors();}// 去看看子view中behavior是有要拦截// 如果要拦截,则我们要拦截// 在这里Behavior类似一个代理final boolean intercepted = performIntercept(ev, TYPE_ON_INTERCEPT);if (cancelEvent != null) {cancelEvent.recycle();}if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {resetTouchBehaviors();}return intercepted;
}
这里面调用了performIntercept
方法,而且指定了个常量TYPE_ON_INTERCEPT
代表了我们在拦截阶段调用的,既然有区分,肯定在别的地方也有调用,答案是肯定的,在onTouch
里也有对performIntercept
的调用,
@Override
public boolean onTouchEvent(MotionEvent ev) {boolean handled = false;boolean cancelSuper = false;MotionEvent cancelEvent = null;final int action = MotionEventCompat.getActionMasked(ev);// 这里要说道说道// 两个条件:1 如果behavior想要拦截// 2 behavior的onTouchEvent返回true// 为什么会有两个条件呢?// 解答:第一个条件是正常的分发流程, 很容易理解//// 第二个条件是在没有子view消费事件,所以事件会冒泡到此// 这时,我们还要继续询问behavior是否要消费该事件// 这里在performIntercept中执行的是:// case TYPE_ON_TOUCH: // 从onTouchEvent调用的// intercepted = b.onTouchEvent(this, child, ev);// break;// 当intercepted为true时,表示我们对该down事件感兴趣// 此时 mBehaviorTouchView也有了赋值if (mBehaviorTouchView != null || (cancelSuper = performIntercept(ev, TYPE_ON_TOUCH))) {// Safe since performIntercept guarantees that// mBehaviorTouchView != null if it returns truefinal LayoutParams lp = (LayoutParams) mBehaviorTouchView.getLayoutParams();final Behavior b = lp.getBehavior();if (b != null) {// 这里同样的事件会继续执行一遍onTouchEvent?handled = b.onTouchEvent(this, mBehaviorTouchView, ev);}}// 如果behavior不感兴趣// 轮到自己了,问问自己干不感兴趣// Keep the super implementation correctif (mBehaviorTouchView == null) {handled |= super.onTouchEvent(ev);} else if (cancelSuper) {// 如果behavior执行了事件(并不是拦截了事件,上面的第一个if的第一个条件不成立,第二个条件成立)// 能执行到这,说明behavior没有拦截事件,但在事件冒泡的过程中消费了事件// mBehaviorTouchView是在performIntercept(ev, TYPE_ON_TOUCH)赋值的// 则给自己执行一个cancel事件if (cancelEvent == null) {final long now = SystemClock.uptimeMillis();cancelEvent = MotionEvent.obtain(now, now,MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);}super.onTouchEvent(cancelEvent);}if (!handled && action == MotionEvent.ACTION_DOWN) {}if (cancelEvent != null) {cancelEvent.recycle();}if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {resetTouchBehaviors();}return handled;
}
恩,这里面的代码注释已经写的很明白了,但是需要注意的一点,这一点我很长时间没有相通,就是为什么还要在onTouch
里还要调用一遍performIntercept
,是这样的,假如现在事件没有任何子view去消费,那么事件会冒泡到此,本着把Behavior看作是一个代理的原则,这里肯定还是要去询问一下Behavior是不是要执行这个事件,注意这里说的是执行而不是拦截,这是因为performIntercept
不仅仅会调用Behavior的拦截部分的代码,也会调用执行的代码,就是通过第二个参数区分的。可以看到,这里我们使用了TYPE_ON_TOUCH
。
好了,说了这么多performIntercept
,是时候来看看performIntercept
的代码了。
private boolean performIntercept(MotionEvent ev, final int type) {boolean intercepted = false;boolean newBlock = false;MotionEvent cancelEvent = null;final int action = MotionEventCompat.getActionMasked(ev);final List<View> topmostChildList = mTempList1;getTopSortedChildren(topmostChildList);// Let topmost child views inspect firstfinal int childCount = topmostChildList.size();for (int i = 0; i < childCount; i++) {final View child = topmostChildList.get(i);final LayoutParams lp = (LayoutParams) child.getLayoutParams();final Behavior b = lp.getBehavior();// 如果现在已经有拦截了的// 并且现在是down// 则 所有的behavior会受到一个cancel事件if ((intercepted || newBlock) && action != MotionEvent.ACTION_DOWN) {// Cancel all behaviors beneath the one that intercepted.// If the event is "down" then we don't have anything to cancel yet.if (b != null) {if (cancelEvent == null) {final long now = SystemClock.uptimeMillis();cancelEvent = MotionEvent.obtain(now, now,MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);}switch (type) {case TYPE_ON_INTERCEPT: // 从onInterceptTouchEvent调用的b.onInterceptTouchEvent(this, child, cancelEvent);break;case TYPE_ON_TOUCH: // 从onTouch调用的b.onTouchEvent(this, child, cancelEvent);break;}}continue;}// 如果现在还没有拦截 并且具有behaviorif (!intercepted && b != null) {switch (type) {case TYPE_ON_INTERCEPT: // 从onInterceptTouchEvent调用的intercepted = b.onInterceptTouchEvent(this, child, ev);break;case TYPE_ON_TOUCH: // 从onTouchEvent调用的intercepted = b.onTouchEvent(this, child, ev);break;}if (intercepted) {mBehaviorTouchView = child;}}// Don't keep going if we're not allowing interaction below this.// Setting newBlock will make sure we cancel the rest of the behaviors.final boolean wasBlocking = lp.didBlockInteraction();final boolean isBlocking = lp.isBlockingInteractionBelow(this, child);newBlock = isBlocking && !wasBlocking;// 如果不允许继续分发,则直接退出if (isBlocking && !newBlock) {// Stop here since we don't have anything more to cancel - we already did// when the behavior first started blocking things below this point.break;}}topmostChildList.clear();return intercepted;
}
这里面的代码也很容易理解,就是去遍历所有的view,在不同的情景下调用Behavior的onInterceptTouchEvent或onTouch方法。
好了关于Behavior的源码我们就分析到这里,相信大家在看完之后会对Behavior有一个全新的认识,而且google已经建议我们使用support design的东西了(没发现现在的项目默认模板文件就是一个标准的support design布局吗),所以我们还是有必要对新东西有个更加深入的认识,而且这样也会有助于我们理解google工程师的思路,在解决一些问题的时候我们完全可以参考一下这些思路。
ok,不扯了,今天就到这里吧,拜拜。
源码看CoordinatorLayout.Behavior原理相关推荐
- 开发直播源码需要了解哪些原理?小白必看内容
在直播源码开发的过程中会涉及到很多技术细节和原理,只有对这些技术细节和原理有了了解,才能实现更高效率的开发.接下来我们就一起来看一下开发直播源码需要了解哪些原理吧. [一个完整直播源码架构] [一个完 ...
- 老李推荐:第5章5节《MonkeyRunner源码剖析》Monkey原理分析-启动运行: 获取系统服务引用 1...
老李推荐:第5章5节<MonkeyRunner源码剖析>Monkey原理分析-启动运行: 获取系统服务引用 上一节我们描述了monkey的命令处理入口函数run是如何调用optionPro ...
- linux内核第一个函数,通过内核源码看函数调用之前世今生 - 极光 - CSDN博客
通过内核源码看函数调用之前世今生 作者:杨小华 栈(Stack):一个有序的积累或堆积 韦氏词典 对每一位孜孜不倦的程序员来说,栈已深深的烙在其脑海中,甚至已经发生变异.栈可以用来传递函数参数.存储局 ...
- linux内核线程socket,从Linux源码看Socket(TCP)的accept
从Linux源码看Socket(TCP)的accept 前言 笔者一直以为若是能知道从应用到框架再到操做系统的每一处代码,是一件Exciting的事情. 今天笔者就从Linux源码的角度看下Serve ...
- 老李推荐:第6章6节《MonkeyRunner源码剖析》Monkey原理分析-事件源-事件源概览-命令队列...
老李推荐:第6章6节<MonkeyRunner源码剖析>Monkey原理分析-事件源-事件源概览-命令队列 事件源在获得字串命令并把它翻译成对应的MonkeyEvent事件后,会把这些事件 ...
- 深入理解redux之从redux源码到react-redux的原理
在使用react的过程中,用redux来管理应用中的状态,使应用流更清晰的同时也会有小小的疑惑,比如reducer在redux中时怎么发挥作用的,为什么只要写好reducer,和dispatch特定a ...
- 顺序线性表 ---- ArrayList 源码解析及实现原理分析
原创播客,如需转载请注明出处.原文地址:http://www.cnblogs.com/crawl/p/7738888.html ------------------------------------ ...
- 手撸spring源码分析IOC实现原理
手撸spring源码分析IOC实现原理 文章出处:https://github.com/fuzhengwei/small-spring 根据小付哥的手撸spring核心源码一步步学习出来的结果收货总结 ...
- 小白前端之路:手写一个简单的vue-router这几年,好像过的好快,怀念我的大学生活。 - 连某人 大三实习生,之前写过简单MVVM框架、简单的vuex、但是看了vue-router的源码(看了
这几年,好像过的好快,怀念我的大学生活. 连某人 大三实习生,之前写过简单MVVM框架.简单的vuex.但是看了vue-router的源码(看了大概)之后就没有写,趁着周末不用工作(大三趁着不开学出来 ...
最新文章
- python读取csv文件第一行_尝试读取CSV文件的第一行返回['/']
- html meta 跳转 白屏,vue使用keepAlive之后页面空白白屏?route和router什么区别呢?...
- python 类-如何理解python的类与对象?
- 【网上的都不靠谱?还是得改源码】用Javasisst的字节码插桩技术,彻底解决Gson转Map时,Int变成double问题...
- JAVA客户端数据传输_java模拟TCP通信实现客户端上传文件到服务器端
- 前端学习(3080):vue+element今日头条管理-发布文章功能介绍
- 计算器百分号如何用代码实现_如何用 100 行 Python 代码实现新闻爬虫?这样可算成功?...
- Archlinux 试用
- python学习之路——day1(18/9/11)
- 在IEEE会议论文LaTeX模板中添加页码
- 夜神模拟器怎么设置android版本号,adb连接夜神模拟器(包括安装adb,夜神模拟器)需要将夜神模拟器的版本号与adb版本一致...
- win10开机黑屏不显示桌面,怎么办
- 选择并遮住工具(边缘调整工具)+剪切蒙版法,扣除头发丝
- 玩游戏使用云服务器,云服务器可以玩大型游戏吗?
- CCF CSP 行车路线 java 201712_4
- 废纸箱机器人图纸_手工制作大全:手绘纸箱机器人
- 金色css颜色代码大全,CSS颜色代码大全
- PowerBI-逻辑函数-IF
- 2023-ISCC信息安全与对抗竞赛wp-misc(详解,有脚本( •̀ ω •́ ),脚本解析,有附件)
- Golang实现发送微信公众号模板消息(每日一句和天气预报)
热门文章
- 【go语言学习笔记】if语句以及带赋值语句的if语句
- 逍遥安卓微信连不上服务器,逍遥安卓模拟器使用常见问题
- 运筹优化在物流行业应用demo案例,Gurobi求解
- FFT/DFT/DCT
- FreeRTOS基础(二):STM32G4系列运行FreeRTOS驱动8路PWM波控制多个路舵机
- 通俗讲解多层全连接前向网络的基本结构
- php按回车执行搜索,jquery绑定回车动作扑捉回车键触发的事件_jquery
- python五种常见的排序方法是_python中序列的排序,sorted方法、reversed方法的使用...
- 人工鱼群算法python_鱼群算法 - Brillou的个人空间 - OSCHINA - 中文开源技术交流社区...
- css属性和属性值,溢出属性,css盒子