Android动画内幕揭秘

2023-10-29

原文链接 Android Animation Internal Secrets

前面的文章重点讲了如何使用安卓平台提供的能力来做好一个动画。为了更深入的理解,需要去了解一下动画框架的内部机理,这样能够帮助我们做出更优雅的动画实现。

View Animation的原理

View Animation源码解析

View animation的代码都是在android.view.animation包下面。

这里面主要有三个东西,下面来分别仔细说说

Animation

主要是抽象类Animation以及它的四大子类,也是View animation中的四大变幻对象–位移变幻TranslateAnimation,缩放变幻ScaleAnimation,旋转变幻RotateAnimation和渐变变幻AlphaAnimation

以及一些工具对象,如AnimationSetAnimationUtils

仔细看这些类的源码可以发现,其实它们不复杂,里面也没啥东西,主要是用于各种参数管理,相当于封装出来的工具和原料,具体内部的原理并不在这里。仔细看四大变幻的applyTransformation方法,可以发现这一坨把最接近『原理』的东西都放在了一个叫做Transformation的对象中去了。

Transformation

直译变幻,但文档中的定义是动画过程中某一时刻应该做的变幻,此为Transformation

这货的实现也不复杂,它也就是个中间商,只是一个存储从Animation传过来的参数 的中间变量,它里面有一个Alpha成员参数用以保存当前的渐变参数值,以及一个Matrix,Matrix可以保存当前的位移,旋转和缩放。Matrix应该不算太陌生,处理过Bitmap变幻的同学,对它应该会有了解,都是通过Matrix来设置参数的。

Interpolator

动画是随时间变化的一系列视觉变幻,因人眼视觉残留,连在一起就是动画,跟电影是一个道理。这里就有一个非常关键的参数就是时间。时间对于动画来说体现在两方面一是时长,就是整个动画持续 的时间,另外一个就是变幻变化的速率,也就是说动画播放速度的变化率。其实,这里变化的并不是时间,时间是永恒的以固定速度在流逝,对于动画来说,帧率是固定的,后面会谈到,动画的帧率是由时间驱动器驱动的,它是以固定的时间脉冲来回调渲染动画的每一帧。这里的时间变化其实是做动画的每一帧时用到的参数 的变化,它并不是线性的,假设动画一共有10帧,要把View向右移动100px,默认是线性的,匀速的,也即每一帧都向前移动10px,但如果使用加速插值器,那么可能就是一个变加速运动,第1帧可能在0px,第2帧在5px,第3帧25px,第4帧到36px,以此类推。

时间插值器,就是用来调整播放速度的,用以实现时间变化。

View Animation的渲染原理

从前面的讨论来看,动画的渲染跟那几个对象都没有关系,使用View animation的时候,只有两种方法可以让动画生效,一是调用View#startAnimation,另外一个是View#setAnimation,然后再Animation#start

假如没有把Animation塞给某一个具体的View对象,光是调用Animation#start,是不会有任何影响和效果的。这说明动画的渲染是在View对象draw时做的,没有与具体View对象建立关联的动画是没有任何效果的。所以动画的渲染主要还要看View本身的逻辑。

可以从View#setAnimation和View#startAnimation入手来看,这两个方法只是把外部传进来的Animation对象保存在了一个叫做mCurrentAnimation成员里面,其他的什么也没做。查询索引,关键的地方有两个,一个是View#applyLegacyAnimation方法,另外一个就是View#draw方法。

先来看View#applyLegacyAnimation方法:

   /**
     * Utility function, called by draw(canvas, parent, drawingTime) to handle the less common
     * case of an active Animation being run on the view.
     */
    private boolean applyLegacyAnimation(ViewGroup parent, long drawingTime,
            Animation a, boolean scalingRequired) {
        Transformation invalidationTransform;
        final int flags = parent.mGroupFlags;
        final boolean initialized = a.isInitialized();
        if (!initialized) {
            a.initialize(mRight - mLeft, mBottom - mTop, parent.getWidth(), parent.getHeight());
            a.initializeInvalidateRegion(0, 0, mRight - mLeft, mBottom - mTop);
            if (mAttachInfo != null) a.setListenerHandler(mAttachInfo.mHandler);
            onAnimationStart();
        }

        final Transformation t = parent.getChildTransformation();
        boolean more = a.getTransformation(drawingTime, t, 1f);
        if (scalingRequired && mAttachInfo.mApplicationScale != 1f) {
            if (parent.mInvalidationTransformation == null) {
                parent.mInvalidationTransformation = new Transformation();
            }
            invalidationTransform = parent.mInvalidationTransformation;
            a.getTransformation(drawingTime, invalidationTransform, 1f);
        } else {
            invalidationTransform = t;
        }

        // more codes
        return more;
    }

这个方法看着比较长,但它就做了三件事情:1)初始化动画;2)获取当前时刻的Transformation;3)如果动画还没有完(还有下一帧),那就得调用View的invalidate,得重绘。

再看使用此方法的地方,是在draw,需要注意是带有三个参数的那个draw,在前面的文章里面介绍过,这个draw方法是由ViewGroup#dispatchDraw中drawChild时调用的:

   /**
     * This method is called by ViewGroup.drawChild() to have each child view draw itself.
     *
     * This is where the View specializes rendering behavior based on layer type,
     * and hardware acceleration.
     */
    boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
        // More codes

        Transformation transformToApply = null;
        boolean concatMatrix = false;
        final boolean scalingRequired = mAttachInfo != null && mAttachInfo.mScalingRequired;
        final Animation a = getAnimation();
        if (a != null) {
            more = applyLegacyAnimation(parent, drawingTime, a, scalingRequired);
            concatMatrix = a.willChangeTransformationMatrix();
            if (concatMatrix) {
                mPrivateFlags3 |= PFLAG3_VIEW_IS_ANIMATING_TRANSFORM;
            }
            transformToApply = parent.getChildTransformation();
        } else {
            if ((mPrivateFlags3 & PFLAG3_VIEW_IS_ANIMATING_TRANSFORM) != 0) {
                // No longer animating: clear out old animation matrix
                mRenderNode.setAnimationMatrix(null);
                mPrivateFlags3 &= ~PFLAG3_VIEW_IS_ANIMATING_TRANSFORM;
            }
            if (!drawingWithRenderNode
                    && (parentFlags & ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS) != 0) {
                final Transformation t = parent.getChildTransformation();
                final boolean hasTransform = parent.getChildStaticTransformation(this, t);
                if (hasTransform) {
                    final int transformType = t.getTransformationType();
                    transformToApply = transformType != Transformation.TYPE_IDENTITY ? t : null;
                    concatMatrix = (transformType & Transformation.TYPE_MATRIX) != 0;
                }
            }
        }

        // more codes

        int restoreTo = -1;
        if (!drawingWithRenderNode || transformToApply != null) {
            restoreTo = canvas.save();
        }
        if (offsetForScroll) {
            canvas.translate(mLeft - sx, mTop - sy);
        } else {
            if (!drawingWithRenderNode) {
                canvas.translate(mLeft, mTop);
            }
            if (scalingRequired) {
                if (drawingWithRenderNode) {
                    // TODO: Might not need this if we put everything inside the DL
                    restoreTo = canvas.save();
                }
                // mAttachInfo cannot be null, otherwise scalingRequired == false
                final float scale = 1.0f / mAttachInfo.mApplicationScale;
                canvas.scale(scale, scale);
            }
        }

        float alpha = drawingWithRenderNode ? 1 : (getAlpha() * getTransitionAlpha());
        if (transformToApply != null
                || alpha < 1
                || !hasIdentityMatrix()
                || (mPrivateFlags3 & PFLAG3_VIEW_IS_ANIMATING_ALPHA) != 0) {
            if (transformToApply != null || !childHasIdentityMatrix) {
                int transX = 0;
                int transY = 0;

                if (offsetForScroll) {
                    transX = -sx;
                    transY = -sy;
                }

                if (transformToApply != null) {
                    if (concatMatrix) {
                        if (drawingWithRenderNode) {
                            renderNode.setAnimationMatrix(transformToApply.getMatrix());
                        } else {
                            // Undo the scroll translation, apply the transformation matrix,
                            // then redo the scroll translate to get the correct result.
                            canvas.translate(-transX, -transY);
                            canvas.concat(transformToApply.getMatrix());
                            canvas.translate(transX, transY);
                        }
                        parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
                    }

                    float transformAlpha = transformToApply.getAlpha();
                    if (transformAlpha < 1) {
                        alpha *= transformAlpha;
                        parent.mGroupFlags |= ViewGroup.FLAG_CLEAR_TRANSFORMATION;
                    }
                }

                if (!childHasIdentityMatrix && !drawingWithRenderNode) {
                    canvas.translate(-transX, -transY);
                    canvas.concat(getMatrix());
                    canvas.translate(transX, transY);
                }
            }
		 }
        // more codes
        return more;
    }

这个方法更长,主要就看transformToApply这个变量就好了,这个变量是在调用了applyLegacyAnimation后被赋值的。之后,可以看到它其中的Matrix被作用于Canvas,而alpha值被用于setAlpha了。好了,这里就是动画的最核心的逻辑。前面说了Transformation对象就是包了一个Matrix和alpha,然后被用在了这里,Matrix作用于Canvas对象,以产生视觉变幻(位移,缩放和旋转),而渐变则是通过setAlpha实现的。

所以View Animation是View tree每次draw的时候去做的,用当前的Animation对象获取到Transformation,然后把Matrix和alpha应用到draw时的Canvas,这就产生了视觉变幻效果。因此,View animation只是放一遍电影,因为这一过程中变化 的只有Transformation对象,也即只有Matrix和alpha在变化,在View draw的时候应用一下就完了,它并没有对View的真实属性产生影响,仅是对渲染的结果Canvas产生影响。而每次View draw的时候,都是会重新生成一个Canvas对象,并且View的属性本身并没有变,所以新生成的Canvas对象并不会体现之前一次draw(也即上一帧)的变幻结果,它只是继续应用Transformation对象,假如动画结束了就没有了Transformation对象,那就没有Matrix和alpha可作用于Canvas,也就没有了动画效果,一切又恢复到了最初原始的样子。

Property Animation的原理

属性动画的实现主要是在android.animation里面,它有独立的一级包名,可以看出它在平台中的位置,是要高于View animation的。

Animator的源码解析

先从Animator对象看起,它是一个抽象类,只定义了关于动画的基本接口,如duration/start/end/cancel等,以及设置AnimatorListener以外,再无其他东西。

最为核心的对象是ValueAnimator,它是属性动画的核心,它主要有两部分,一是管理各种数值,前面的文章说过属性动画的核心原理就是在一定时间内,用一定速率把某个值变成另外一个值;另外一部分就涉及渲染原理,后面再详细说。

再有就是ObjectAnimator,它是ValueAnimator的子类,连同PropertyValuesHolder一起,针对某个对象的属性进行管理,主要涉及两方面,一个是属性值的管理,也即把对象的属性名字和其要设置的值都暂存起来,另外一部分就是通过反射来把要修改的值作用于目标对象。

Animator的时间驱动器

动画要让数值随时间而变化,当start了以后,最重要的事情 就是以一定的时间速率来刷新数值,也即是用一个时间驱动器来刷新每一帧。前面讨论了View animation,是在View tree渲染时去刷新动画的每一帧。

属性动画的核心在ValueAnimator里面,连同一个AnimatorHandler对象,一起实现了时间驱动。AnimatorHanndler是属性动画的时间驱动器,它从Choreographer中接收脉冲信号,然后再回调给所有的ValueAnimator,令其doAnimationFrame。它是一个单例,也就是说同一个进程里所有的属性动画用的是内一个时间驱动器,同一个AnimatorHandler。

注意:关于Choreographer的解释可以看另外的文章

当调用ValueAnimator#start时便会往AnimatorHandler对象添加一个回调,用以接收do frame的脉冲事件,然后从时间插值器mInterpolator中获取当前的时间速率,再调用animateValue进行数值的改变,其子类可以override此方法以实现属性的具体变化。这里还有一个变量mSelfPulsing用以控制是否使用AnimatorHandler,默认是true,也就是让ValueAnimator使用AnimatorHandler接收来自Choreographer的脉冲信号做动画。此外,也可以自己实现一个时间驱动器。

由此,便可以让在duration之内,渲染动画的每一帧。

Animator的渲染原理

ValueAnimator仅是让一个数值在一定时间内发生特定的变化,它没有实际的视觉效果。常常使用的是ObjectAnimator,并作用于View的属性以产生视觉效果,如前面文章中的例子。那么这个又是如何实现的呢?

ObjectAnimator是可能改变某个对象(内部称之为Target对象)的某个属性值,让其随时间变化,当应用到View对象时,比如translationY属性,ObjectAniamtor所做的也仅仅是让translationY的值随时间变化 而已,仅在animateValue时去调用View#setTranslationY把变化的数值传进去。是View自己在做重绘,View的setTranslationY方法中,有做invalidate以进行重绘。由此,便产生了视觉效果。

ViewPropertyAnimator是另一个常用的对象,但发现它并不是Animator的子类,是封装出来的专门针对View对象做属性动画的一个工具类,它本质上与ObjectAnimator一样,只不过做了一些集成与封装,可以同时方便的操作多个属性,另外它会把所有属性的值变更 过后统一调一次invalidate,效率上会略高一筹。ObjectAnimator一次只能操作一个属性,并且每个属性变化 时都会调一次invalidate。

它是把支持的属性都先放进一个map里面暂存起来,当调用startAnimation时,创建一个ValueAnimator,并设置一个AnimatorListener,在onAnimationUpdate时,把前面暂存的属性都设置到mView对象中去,然后调用一次invalidate让mView重绘。这里还需要注意,在设置属性这一块与ObjectAnimator也不一样,前面说了ObjectAniamtor是通过属性的settter来实现的,但View的属性的settter都会触发invalidate。所以,ViewPropertyAnimator为了避免每次设置属性时都触发invalidate,它是直接把属性塞给View的mRenderNode对象,然后在所有变化 的属性都设置完以后,再统一做一次重绘(invalidate)。

另外的区别就是,ViewPropertyAnimator仅支持一些特定的属性,而ObjectAnimator可以支持任意属性(只要有setter/getter,就可以)。

关于动画的常见问题

通过上面的论述,就搞清楚了动画原理了,下面来看一些比较有意思的问题。

动画是在主线程里做的么

动画主要是通过View对象来呈现视觉效果,View是在主线程中渲染的,所以动画也是在主线程里面完成的。这话呢,只对了一半,或者这么说是不够严谨的。

通过上面的讨论,View animation,都是在主线程中实现的,因为它的时间驱动器是View tree的渲染,也即在draw的时候,去计算当前的Transformation,然后应用到View的Canvas上面。这一切都是在主线程中完成的。

但对于属性动画,就不是这个样子,属性动画分两部分,一部分是让数值随时间变化 ,这个其实可以在任意线程中去做。通过上面的讨论,默认的情况下,确实也是在主线程中做的(从Choreographer得到时间脉冲,这是在主线程里面),但是留 有接口,可以改变的,虽然很少这样做,但确实是可行的,并且数值随时间变化,这个事情也是可以在任意线程中完成的。另外一部分,就是让变化 的数值对目标对象生效,这个要看具体的对象了,如果View,肯定 还是要在主线程里搞。

动画的帧率(FPS)是多少

从上面的讨论来看,无论是View animation还是属性动画,时间脉冲都是Choreographer,并且对View来说视觉要生效是通过重绘来做的,所以最高帧率都会是60FPS。

所以,其实动画的帧率是固定的,也就是说其doAnimationFrame是固定频率在回调。

这里要与动画的时间插值器区别开来,动画的真实帧率是固定的,时间插值器的作用是让动画的变化变成非线性的。比如说某个属性x从0变到100,ValueAnimator的doAnimationFrame以及animateValue会是以固定的频率,从Choreographer每隔16ms接收一次脉冲,就会调用一次animateValue,时间插值器的作用,能让x值的变化是非线性的:

时间脉冲:0 1 2 3 4 5 6 7 8 9 10
线性变化:0 10 20 30 40 50 60 70 80 90 100
加速减速:0 13 25 37 57 71 79 85 89 95 100

时间插值器并没有让动画的帧率发生变化 ,而是让动画的结果非线性变化。

动画过程中如何处理MotionEvent事件

没有任何影响,view animation是发生在draw的时候,而属性动画是设置属性后再re-draw。从逻辑 上来讲动画与事件不冲突,两者之间没有任何影响。

不过呢,View animation是对Canvas做变幻,View对象仍在原来的位置,原来的状态,所以点击动画过程中的View可能会没有效果,特别是对于有位移的时候。但属性动画就没有问题,View就是真实的在移动。

但对于业务逻辑来说,通常动画都用于某个View的入场和出场,所以入场动画做完之前,以及出场动画开始之后,不响应点击事件要好一些,当然,这个就要靠开发者自己去实现了。

动画可以取消么

当然可以,都有cancel接口可以调用,但具体影响不太一样。

对于View animation,Animation#cancel是会调用onAnimationEnd的,因为它的回调接口没有专门用于cancel的。

但属性动画的回调接口要丰富一些,它有cancel,所以是会回调onAimationCancel的,但不会回调onAnimationEnd。

动画需要注意的事项

一定要实现onAnimationCancel,以及onAnimationEnd,如果有涉及状态变更,或者关联其他动画时。要知道动画除了常规结束还会有被cancel掉的可能。

另外,就是对于属性动画,取消有两种方式,一是直接调用Animator#cancel另外一种是调用Animator#end,两个方法在处理最后的状态时略有差异。end方法会把属性的最终状态设置给属性,然后回调onAnimationEnd,但cancel就直接终止动画了,属性当前啥状态那就啥状态,然后回调onAnimationCancel。其实,大多数情况下,end更为合理,但end可能会造成视觉上的跳跃,属性的状态会突然变化。

再有就是,如果对于View,有多个属性同时做动画时,用ViewPropertyAnimator更好一些。语法上面也更简洁,性能上也略优一些。

原创不易,打赏点赞在看收藏分享 总要有一个吧

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Android动画内幕揭秘 的相关文章

  • 这个方法比 Math.random() 更快吗?

    我是一名初学者 目前已经开始开发一款使用粒子群优化算法的 Android 游戏 我现在正在尝试稍微优化我的代码 并且 for 循环中有相当多的 Math random 几乎一直在运行 所以我正在考虑一种方法来绕过并跳过所有 Math ran
  • 我在哪里可以获得可靠的熵来源(真正的随机性字节[])?

    目前 我正在寻找一种方法来增加随机性的质量 in my Android应用程序 纸牌游戏 之前 估计对于我的情况 52 排列 至少需要 226 位熵 226 个随机位 我打算用这个byte 作为种子SecureRandom SecureRa
  • 如何在 ADB 连接期间禁用电池充电?

    问题描述 每次我在电脑和手机之间连接 USB 线时 电池都会自动充电 我想使用 ADB 协议 但我不想在 ADB 连接期间为电池充电 是否可以关闭此充电功能 当然 我该怎么做呢 环境 Android 操作系统 4 及更高版本的手机 我只需要
  • 使用一个 apk 安装两个应用程序

    我有 2 个应用程序 1 内容提供者 2 使用此 ContentProvider 的应用程序 我需要使用单个 apk 文件安装这 2 个应用程序 我想在 Eclipse 中同时推送这两个应用程序 如果我将另一个项目添加到一个应用程序的构建路
  • 从 arraylist 和 hashmap 中删除重复项

    我有一个数组列表 其中包含付款人的姓名 另一个数组列表包含每次付款的费用 例如 nameArray 尼古拉 劳尔 洛伦佐 劳尔 劳尔 洛伦佐 尼古拉 价格数组 24 12 22 18 5 8 1 我需要将每个人的费用相加 所以数组必须变成
  • 需要使用手机后退按钮返回 Web 视图的帮助

    这是我的代码 package com testappmobile import android app Activity import android os Bundle import android view KeyEvent impor
  • onScale 事件后触发奇怪的 onScroll 事件

    我有一个同时使用 SimpleOnScaleGestureListener 和 SimpleOnGestureListener 的应用程序 每当我进行捏缩放时 我都会得到预期的 onScale 但是当我抬起时 我会看到一个奇怪的 onScr
  • 位图内存不足错误

    我对这个错误有疑问 我从 URL 制作网站图标解析器 我这样做是这样的 public class GrabIconsFromWebPage public static String replaceUrl String url StringB
  • Android:如何使用后台线程?

    我开发了一个应用程序 它从互联网获取内容并相应地在设备的屏幕上显示它 该程序运行得很好 就是有点慢 加载并显示内容大约需要 3 4 秒 我想将获取内容并将其显示在后台线程中的所有代码放在一起 当程序执行这些功能时 我想显示一个进度对话框 你
  • 具有自定义源集的 Android Gradle 风格 - gradle 文件应该是什么样子?

    我有一个旧的 eclipse 项目 我已经转移到 android studio 并设置为使用flavor 它似乎工作得很好 直到我开始尝试在我的风格之间使用不同的 java 文件 我的项目设置是这样的 ProjectRoot acitonb
  • Android 版 jTwitter 授权错误

    我在我的 Android 应用程序中使用 jTwitter 库 直到前天一切都运转良好 但今天遇到异常 服务提供商响应错误 301 请帮助我 这是堆栈跟踪 02 21 21 07 27 258 E AndroidRuntime 4013 F
  • 在 Android Lollipop 中从 Uri 中裁剪照片后总是返回 Null?

    我尝试在拍照或挑选照片后从 Uri 中裁剪图像 我的代码是这样的 public static void cropImage Uri uri Activity activity int action code Intent intent ne
  • Android:使 Dialog 周围的所有内容都比默认值更暗

    我有一个具有以下样式的自定义对话框 它显示了一个无边框对话框 后面的任何内容都会 稍微 变暗 我的设计师希望背后的一切都比 Android 的默认设置更暗 但不是完全黑色 有这样的设置吗 我能想到的唯一解决方法是使用全屏活动而不是对话框 只
  • 来自相机的 MediaCodec 视频流方向和颜色错误

    我正在尝试流式传输视频捕获直接从相机适用于 Android 设备 到目前为止 我已经能够从 Android 相机捕获每一帧预览帧 byte data Camera camera 函数 对数据进行编码 然后成功解码数据并显示到表面 我用的是安
  • Jetpack 导航:如何从一个嵌套图的子级导航到另一个嵌套图的子级?

    导航结构 MainActivity nav root HomeFragment AuthNestedGraph nav auth BeforeOtpFragment home OtpFragment ProfileNestedGraph n
  • SDK >=26 仍需要 mipmap/ic_launcher.png?

    在 Android 中 有两种指定启动器图标 可以说是应用程序图标 的方法 老 方式 在 mipmap 文件夹中指定不同的 png 文件 通常命名为 ic launcher png 但可以通过以下方式设置名称android icon mip
  • Glass 语音命令给定列表中最接近的匹配项

    使用 Glass 您可以通过 确定 Glass 菜单启动应用程序 它似乎会选择最接近的匹配项 除非命令相距数英里 并且您可以明显看到命令列表 无论如何 是否可以从应用程序内或从语音提示 在初始应用程序触发后 给出类似的列表并返回最接近的匹配
  • 我在 PopupMenu 中使用 ShareActionProvider,但显示两个 PopupMenu?

    我在 PopupMenu 中使用 ShareActionProvider 但是当我单击共享菜单项时 它会在屏幕上显示两个 PopupMenus 一个被另一个覆盖 一个显示应用程序图标和名称 另一个仅显示应用程序名称 除了这个问题之外 它工作
  • 在android中创建SQLite数据库

    我想在我的应用程序中创建一个 SQLite 数据库 其中包含三个表 我将向表中添加数据并稍后使用它们 但我喜欢保留数据库 就好像第一次安装应用程序时它会检查数据库是否存在 如果存在则更新它 否则如果不存在则创建一个新数据库 此外 我正在制作
  • 将焦距(以毫米为单位)转换为像素 - Android

    在 Android 中 我当前正在访问camera s焦距通过使用getFocalLength in Camera1 Camera2不是一个选择 我正在尝试完全填充当前的计算 focal length pix focal length m

随机推荐

  • C++:给定一个字符串,验证是否为回文,只考虑字母和数字字符,忽略字母大小写。

    include
  • 【微信小程序】微信小程序实现点击分享链接进入的分享页面左上角是返回按钮

    首先先和你们说这是可以实现而且非常简单 接下来我们就来看看如何实现这种需求的 首先我们需要配置分享链接 例如 detail js页面 Page onShareAppMessage function res var url 页面参数 if r
  • Spring中的18个注解,你会几个?

    点击上方 Java之间 选择 置顶或者星标 你关注的就是我关心的 作者 Java的小本家 Controller 标识一个该类是Spring MVC controller处理器 用来创建处理http请求的对象 RestController S
  • 结合AG-Grid二次封装element-plus的el-table表格

    MyTable组件封装 路径 conponents MyTable index vue template
  • 第三周课程总结&实验报告一

    一 实验报告 1 打印输出所有的 水仙花数 所谓 水仙花数 是指一个3位数 其中各位数字立方和等于该数本身 例如 153是一个 水仙花数 I 实验代码 public class text public static void main St
  • 操作系统复习题

    一 填空题 1 通常所说操作系统的四大模块是指处理机管理 存储管理 设备管理 文件 管理 2 进程实体是由 进程控制块 PCB 程序段和数据段这三部分组成 3 文件系统中 空闲存储空间的管理方法有空闲表法 空闲链表法 位示图法和 成组链接法
  • 7_2,24位真彩模式(2013-2-27)

    同理 24位为3通道 3字节 但是经过测试 有问题 不支持24位 7 3 32位真彩模式 32位与16位不同之处 32位分为ARGB和XRGB 各8位 ARGB中前8位为透明色 XRGB前8位为了对齐 一般清为0 define RGB32B
  • JDBC流程

    JDBC JAVA 访问数据库的技术 Jdbc是一种Java连接数据库技术 Java database connectity 它是 Java 提供的一些接口 这些接口大部分是数据库厂商提供的 jar包 我们要做的 是连接数据库以后 如何使用
  • php和nginx镜像合并 && 代码打包到镜像 —— k8s从入门到高并发系列教程 (二)

    上文使用了nginx和php fpm两个镜像组装了nginx php环境 然而实际企业的微服务架构 nginx和php fpm是被统一看作一个微服务供其他服务调用的 另外 配置文件和源代码也不会通过映射到容器中的方式进行 而是打包到了企业的
  • pytorch中dataloader的num_workers参数

    结论速递 在Windows系统中 num workers参数建议设为0 在Linux系统则不需担心 1 问题描述 在之前的任务超大图上的节点表征学习中 使用PyG库用于数据加载的DataLoader ClusterLoader等类时 会涉及
  • [分享]我发现了一个快速完成物联网毕业设计的好方法!

    对于计算机相关专业的毕业生来说 毕业论文真的是一件特别令人头疼的事情 当然学霸除外 毕设 编程 每每想到这里 是不是很想原地爆炸 莫着急往下看 确认过眼神 你就是我要帮助的人 下面就给大家介绍一个快速完成毕业设计的方法 绝对的亲身实践哦 作
  • [极客大挑战 2019]Secret File

    进入靶场查看源码 进入这个网页 5f5574c7 75b8 4251 befa d89a391dafc4 node4 buuoj cn 81 Archive room php 点击这个就会跳入下一个网页 发现没有东西 抓包试试 先返回到这个
  • vite项目集成eslint和prettier

    一 eslint介绍 eslint中文官网 https zh hans eslint org docs latest use getting started 1 什么是eslint ESLint是一个开源的JavaScript代码静态分析工
  • vscode连接远程Linux服务器

    文章目录 一 环境安装 1 1 下载vscode 1 2 下载vscode sever 二 ssh链接 2 1 安装Remote SSH 2 2 设置vscode ssh 2 3 设置免密登录 2 3 1 本地生成公私钥 2 3 2 服务器
  • sharding-jdbc事务解读

    序言 sharding jdbc在分库分表方面提供了很大的便利性 在使用DB的时候 通常都会涉及到事务这个概念 而在分库分表的环境上再加上事务 就会使事情变得复杂起来 本章试图剖析sharding jdbc在事务方面的解决思路 传统事务回顾
  • 二叉查找树,红黑树,AVL树,B~/B+树(B-tree),伸展树——优缺点及比较

    二叉查找树 Binary Search Tree 很显然 二叉查找树的发现完全是因为静态查找结构在动态插入 删除结点所表现出来的无能为力 需要付出极大的代价 BST 的操作代价分析 1 查找代价 任何一个数据的查找过程都需要从根结点出发 沿
  • cpsr寄存器

    设置cpsr寄存器为svc模式 特权模式 CPSR 程序状态寄存器 current program status register cpsr在用户级编程时用于存储条件码 CPSR包含条件码标志 中断禁止位 当前处理器模式以及其他状态和控制信
  • Nvidia container无法修改显示模式问题

    电脑类型 联想拯救者y7000p 2020 问题 当启动游戏时会跳出 Nvidia container 提示的无法 更改显示模式问题 解决办法 退出联想电脑管家
  • 面试题:内存泄漏以及避免和减少这类错误的方法?

    面试题 内存泄漏以及避免和减少这类错误的方法 在C 程序中 内存泄漏是一种常见的错误 它指的是在程序中使用new操作符为对象分配内存后 未对其进行及时释放导致的内存浪费 如果内存泄漏问题得不到解决 会导致程序运行速度变慢 稳定性降低甚至崩溃
  • Android动画内幕揭秘

    原文链接 Android Animation Internal Secrets 前面的文章重点讲了如何使用安卓平台提供的能力来做好一个动画 为了更深入的理解 需要去了解一下动画框架的内部机理 这样能够帮助我们做出更优雅的动画实现 View