IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Android属性动画-ValueAnimator原理解析 -> 正文阅读

[移动开发]Android属性动画-ValueAnimator原理解析

Android属性动画-ValueAnimator原理解析

一、概述

android中的属性动画的实现是通过不断的改变View的属性然后刷新,这个改变过程是通过数据的连续补帧和渐变来实现的,那么这个就需要有个脉冲的类来实现这个功能,而且这个脉冲不是随便写的,必须要根据硬件的配置和设置已经硬件环境来触发,这个过程中有个重要的实现类就是ValueAnimator,今天就来说下这个类实现的来龙去脉

二、问题

1、ValueAnimator的继承关系是什么,有哪些兄弟类,都什么作用?

2、ValueAnimator的脉冲源是怎么产生的,过程是什么?

三、分析

一、1、ValueAnimator的继承关系是什么,有哪些兄弟类,都什么作用?

Animator
ValueAnimator
AnimatorSet
ObjectAnimator

上图是他们的继承关系

Animator:是属性动画的基类,这个是一个抽象类,是一个基础框架类,里面定义个很多的规范方法,需要子类实现,比如动画的开始、动画的暂停、动画的结束、动画的模式、动画持续时间等方法,这个不是我们讨论的重点,直到干啥就可以了

valueAnimator:这个是属性动画的核心类,这个类里面主要完成了:

1、脉冲回调机制

2、对属性动画的开始、暂停、设置参数等做了逻辑实现

这个类可以用来实现自定义的属性动画比较多,不限于View的,比如实现Paint的draw 不是基于View本身的,画一个圆,就可以用这个完成,扩展性非常高

ObjectAnimator:这个是继承于ValueAnimator的,在此基础上,主要针对View的动画做了方便处理,可以直接对View的set,get 属性直接做动画,更加方便直接

AnimatorSet:看这个名字,就直到这个是对多个动画Animator进行按照一定的顺序执行的类,通俗的说是一个组合动画,以此来实现更加复杂的动画的目的

1、ValueAnimator的脉冲源是怎么产生的,过程是什么?

ValueAnimator
AnimationHandler
Choreographer

AnimationHandler:这个是对Choregrapher的包装类,对ValueAnimator提供了简易接口,AnimationHandler通过Choreographer实现脉冲回调

ChoreograVspher:这个英文含义“编舞者”,通过想ServiceFlinger发送消息,然后经过处理后,ServiceFlinger向Choreographer发送Vsync信号,以此来统一系统的输入、视图绘制、动画的时机,所以这个实际是根据硬件的情况来输出的

有了整体的了解后我们通过源码分步看下过程:

ValueAnimator对AnimationHandler调用

在动画的开始时候就是start()调用之后:

private void start(boolean playBackwards) {
        if (Looper.myLooper() == null) {
            throw new AndroidRuntimeException("Animators may only be run on Looper threads");
        }
        mReversing = playBackwards;
        mSelfPulse = !mSuppressSelfPulseRequested;
        // Special case: reversing from seek-to-0 should act as if not seeked at all.
        if (playBackwards && mSeekFraction != -1 && mSeekFraction != 0) {
            if (mRepeatCount == INFINITE) {
                // Calculate the fraction of the current iteration.
                float fraction = (float) (mSeekFraction - Math.floor(mSeekFraction));
                mSeekFraction = 1 - fraction;
            } else {
                mSeekFraction = 1 + mRepeatCount - mSeekFraction;
            }
        }
        mStarted = true;
        mPaused = false;
        mRunning = false;
        mAnimationEndRequested = false;
        // Resets mLastFrameTime when start() is called, so that if the animation was running,
        // calling start() would put the animation in the
        // started-but-not-yet-reached-the-first-frame phase.
        mLastFrameTime = -1;
        mFirstFrameTime = -1;
        mStartTime = -1;
    //这里是核心代码,引起下一步的部分
        addAnimationCallback(0);

        if (mStartDelay == 0 || mSeekFraction >= 0 || mReversing) {
            // If there's no start delay, init the animation and notify start listeners right away
            // to be consistent with the previous behavior. Otherwise, postpone this until the first
            // frame after the start delay.
            startAnimation();
            if (mSeekFraction == -1) {
                // No seek, start at play time 0. Note that the reason we are not using fraction 0
                // is because for animations with 0 duration, we want to be consistent with pre-N
                // behavior: skip to the final value immediately.
                setCurrentPlayTime(0);
            } else {
                setCurrentFraction(mSeekFraction);
            }
        }
    }

addAnimationCallback:

private void addAnimationCallback(long delay) {
        if (!mSelfPulse) {
            return;
        }
    //这个地方就是调用了ValueAnimator中的animationHandler的addAnimationFrameCallback方法
    
        getAnimationHandler().addAnimationFrameCallback(this, delay);
    }

addAnimationFrameCallback:

public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) {
    if (mAnimationCallbacks.size() == 0) {
        //这里调用了Provider,这个provider其实是AnimationHandler的内部静态类,是对Choreograph的包装
        //他向Choreograph 发送了一个消息请求
        getProvider().postFrameCallback(mFrameCallback);
    }
    //向mAnimationCallbacks 添加了一个callback
    if (!mAnimationCallbacks.contains(callback)) {
        mAnimationCallbacks.add(callback);
    }

    if (delay > 0) {
        mDelayedCallbackStartTime.put(callback, (SystemClock.uptimeMillis() + delay));
    }
}
//下面这个是MyFrameCallbackProvider类,可以看到里面有Choreographer,对Choreographer 的包装类
private class MyFrameCallbackProvider implements AnimationFrameCallbackProvider {

        final Choreographer mChoreographer = Choreographer.getInstance();

        @Override
        public void postFrameCallback(Choreographer.FrameCallback callback) {
            mChoreographer.postFrameCallback(callback);
        }

        @Override
        public void postCommitCallback(Runnable runnable) {
            mChoreographer.postCallback(Choreographer.CALLBACK_COMMIT, runnable, null);
        }

        @Override
        public long getFrameTime() {
            return mChoreographer.getFrameTime();
        }

        @Override
        public long getFrameDelay() {
            return Choreographer.getFrameDelay();
        }

        @Override
        public void setFrameDelay(long delay) {
            Choreographer.setFrameDelay(delay);
        }
    }

上面向Choreographer 发了消息,我们看下在那里有回调的结果

其实回调结果就在getProvider().postFrameCallback(mFrameCallback); 的mFrameCallback中

private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
    @Override
    public void doFrame(long frameTimeNanos) {
        //这个是AnimationHandler自己本身的处理逻辑,最终会触发自己的回调接口
        doAnimationFrame(getProvider().getFrameTime());
        //因为mAnimationCallbacks 之前添加了,所以size>0 所以又向Choreographer发消息
        //然后形成不断的循环
        //这个循环速度是受到硬件的情况限制的,比如硬件是支持60帧的的,这个速度就按照这个来,
        
        if (mAnimationCallbacks.size() > 0) {
            getProvider().postFrameCallback(this);
        }
    }
};

这里可以总结下AnimationHandler:

AnimationHandler 内部的循环是不断的,在接受的位置又发送消息,达到永久循环,来做到不断的脉冲回调机制

除非通过removeFrameCallback移除调callback就停止

还有一点AnimationHandler是ThreadLocal的类型,是每个线程的本地变量,不同的线程不受到干扰

public final static ThreadLocal<AnimationHandler> sAnimatorHandler = new ThreadLocal<>();

对ThreadLocal不了解的同学可以看我的关于ThreadLocal的介绍

Choreographer

Choreographer 是线程的本地变量,通过ThreadLocal保存,每个线程都一个Choreographer的初始值,每个线程都在所在的那个Looper上工作,Looper不可以为null

private static volatile Choreographer mMainInstance;

// Thread local storage for the SF choreographer.
private static final ThreadLocal<Choreographer> sSfThreadInstance =
        new ThreadLocal<Choreographer>() {
            @Override
            protected Choreographer initialValue() {
                Looper looper = Looper.myLooper();
                if (looper == null) {
                    throw new IllegalStateException("The current thread must have a looper!");
                }
                return new Choreographer(looper, VSYNC_SOURCE_SURFACE_FLINGER);
            }
        };

1、向系统post信号,这个是请求脉冲的信号

public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {
        if (callback == null) {
            throw new IllegalArgumentException("callback must not be null");
        }

        postCallbackDelayedInternal(CALLBACK_ANIMATION,
                callback, FRAME_CALLBACK_TOKEN, delayMillis);
    }

继续看postCallbackDelayedInternal

private void postCallbackDelayedInternal(int callbackType,
        Object action, Object token, long delayMillis) {
    if (DEBUG_FRAMES) {
        Log.d(TAG, "PostCallback: type=" + callbackType
                + ", action=" + action + ", token=" + token
                + ", delayMillis=" + delayMillis);
    }

    synchronized (mLock) {
        final long now = SystemClock.uptimeMillis();
        final long dueTime = now + delayMillis;
        mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
		//立刻发射
        if (dueTime <= now) {
            scheduleFrameLocked(now);
        } else {
            //通过handler发射,需要优先级的队列
            Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
            msg.arg1 = callbackType;
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, dueTime);
        }
    }
}

上面这里主要两点

1、mCallbackQueues队列中添加了一个action,这个是回调,后面在结果那里要回调他

2、向系统的ServiceFliger发送请求信号

最后经过代码辗转会来到下面代码

private void scheduleVsyncLocked() {
    mDisplayEventReceiver.scheduleVsync();
}

我们看到里面有个mDisplayEventReceiver,我们看下这个是什么呢?

我们可以看到,这个是一个继承自DisplayEventReceiver的子类,实现了Runnable,线程任务

说明这个方法scheduleVsync 调用的是父类的方法

private final class FrameDisplayEventReceiver extends DisplayEventReceiver
        implements Runnable {
    private boolean mHavePendingVsync;
    private long mTimestampNanos;
    private int mFrame;

    public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
        super(looper, vsyncSource, CONFIG_CHANGED_EVENT_SUPPRESS);
    }

    // TODO(b/116025192): physicalDisplayId is ignored because SF only emits VSYNC events for
    // the internal display and DisplayEventReceiver#scheduleVsync only allows requesting VSYNC
    // for the internal display implicitly.
    @Override
    public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
        // Post the vsync event to the Handler.
        // The idea is to prevent incoming vsync events from completely starving
        // the message queue.  If there are no messages in the queue with timestamps
        // earlier than the frame time, then the vsync event will be processed immediately.
        // Otherwise, messages that predate the vsync event will be handled first.
        long now = System.nanoTime();
        if (timestampNanos > now) {
            Log.w(TAG, "Frame time is " + ((timestampNanos - now) * 0.000001f)
                    + " ms in the future!  Check that graphics HAL is generating vsync "
                    + "timestamps using the correct timebase.");
            timestampNanos = now;
        }

        if (mHavePendingVsync) {
            Log.w(TAG, "Already have a pending vsync event.  There should only be "
                    + "one at a time.");
        } else {
            mHavePendingVsync = true;
        }

        mTimestampNanos = timestampNanos;
        mFrame = frame;
        Message msg = Message.obtain(mHandler, this);
        msg.setAsynchronous(true);
        mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
    }

    @Override
    public void run() {
        mHavePendingVsync = false;
        doFrame(mTimestampNanos, mFrame);
    }
}

那么我们在继续看下父类DisplayEventReceiver

这个是一个发送器/接收器 发送消息并且接受来自ServiceFlinger的Vsync信号

发送部分:

public void scheduleVsync() {
    if (mReceiverPtr == 0) {
        Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "
                + "receiver has already been disposed.");
    } else {
        nativeScheduleVsync(mReceiverPtr);
    }
}

//调用native方法 发送消息
//参数是System.nanoTime() 获取当前系统的时间,纳秒
//
@FastNative
private static native void nativeScheduleVsync(long receiverPtr);

接收部分:

/*
Called when a vertical sync pulse is received. The recipient should render a frame and then call scheduleVsync to schedule the next vertical sync pulse.
Params:
timestampNanos – The timestamp of the pulse, in the System.nanoTime() timebase.
physicalDisplayId – Stable display ID that uniquely describes a (display, port) pair.
frame – The frame number. Increases by one for each vertical sync interval.
*/
//获得的这个timestampNanos 时间是基于System.nanoTime()
//frame   脉冲index
@UnsupportedAppUsage
public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
}

我们回到FrameDisplayEventReceiver 中的onVsync,看具体做了什么

public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
        // Post the vsync event to the Handler.
        // The idea is to prevent incoming vsync events from completely starving
        // the message queue.  If there are no messages in the queue with timestamps
        // earlier than the frame time, then the vsync event will be processed immediately.
        // Otherwise, messages that predate the vsync event will be handled first.
        long now = System.nanoTime();
        if (timestampNanos > now) {
            Log.w(TAG, "Frame time is " + ((timestampNanos - now) * 0.000001f)

   + " ms in the future!  Check that graphics HAL is generating vsync "
     ing the correct timebase.");
                 timestampNanos = now;
             }    
if (mHavePendingVsync) {
        Log.w(TAG, "Already have a pending vsync event.  There should only be "
                + "one at a time.");
    } else {
        mHavePendingVsync = true;
    }
	
    mTimestampNanos = timestampNanos;
    mFrame = frame;
                  //把当前的this,其实是一个runable 发送都Looper中等待执行
    Message msg = Message.obtain(mHandler, this);
    msg.setAsynchronous(true);
    mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}
                  
 @Override
 public void run() {
     //最终会走到这里来
        mHavePendingVsync = false;
     //这个最后会回调之前设置的回调,这些回调
        doFrame(mTimestampNanos, mFrame);
 }
                  

doFrame:

@UnsupportedAppUsage
void doFrame(long frameTimeNanos, int frame) {
    final long startNanos;
    synchronized (mLock) {
        if (!mFrameScheduled) {
            return; // no work to do
        }

        if (DEBUG_JANK && mDebugPrintNextFrameTimeDelta) {
            mDebugPrintNextFrameTimeDelta = false;
            Log.d(TAG, "Frame time delta: "
                    + ((frameTimeNanos - mLastFrameTimeNanos) * 0.000001f) + " ms");
        }

        long intendedFrameTimeNanos = frameTimeNanos;
        startNanos = System.nanoTime();
        final long jitterNanos = startNanos - frameTimeNanos;
        if (jitterNanos >= mFrameIntervalNanos) {
            final long skippedFrames = jitterNanos / mFrameIntervalNanos;
            if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
                Log.i(TAG, "Skipped " + skippedFrames + " frames!  "
                        + "The application may be doing too much work on its main thread.");
            }
            final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
            if (DEBUG_JANK) {
                Log.d(TAG, "Missed vsync by " + (jitterNanos * 0.000001f) + " ms "
                        + "which is more than the frame interval of "
                        + (mFrameIntervalNanos * 0.000001f) + " ms!  "
                        + "Skipping " + skippedFrames + " frames and setting frame "
                        + "time to " + (lastFrameOffset * 0.000001f) + " ms in the past.");
            }
            frameTimeNanos = startNanos - lastFrameOffset;
        }

        if (frameTimeNanos < mLastFrameTimeNanos) {
            if (DEBUG_JANK) {
                Log.d(TAG, "Frame time appears to be going backwards.  May be due to a "
                        + "previously skipped frame.  Waiting for next vsync.");
            }
            scheduleVsyncLocked();
            return;
        }

        if (mFPSDivisor > 1) {
            long timeSinceVsync = frameTimeNanos - mLastFrameTimeNanos;
            if (timeSinceVsync < (mFrameIntervalNanos * mFPSDivisor) && timeSinceVsync > 0) {
                scheduleVsyncLocked();
                return;
            }
        }

        mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
        mFrameScheduled = false;
        mLastFrameTimeNanos = frameTimeNanos;
    }

    try {
        /**
        下面这些是处理核心,主要是做了分类处理
        我们之前说了的,Choreographer是,系统的动画,视图绘制,输入的时间上的统一,都是根据系统的统一协调,所以都是统一时间调度
        比如:view的mesureLayout是经过这个时间调度的
        类型是CALLBACK_TRAVERSAL
        具体这里不展开说了
        */
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
        AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);
		//当前帧信息标记为Input处理的开始
        mFrameInfo.markInputHandlingStart();
        doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);

        mFrameInfo.markAnimationsStart();
        doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
        doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos);

        mFrameInfo.markPerformTraversalsStart();
        doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);

        doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
    } finally {
        AnimationUtils.unlockAnimationClock();
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }

    if (DEBUG_FRAMES) {
        final long endNanos = System.nanoTime();
        Log.d(TAG, "Frame " + frame + ": Finished, took "
                + (endNanos - startNanos) * 0.000001f + " ms, latency "
                + (startNanos - frameTimeNanos) * 0.000001f + " ms.");
    }
}

我们在继续看下doCallbacks

void doCallbacks(int callbackType, long frameTimeNanos) {
    //这个是执行动作的封装
    /*
    private static final class CallbackRecord {
        public CallbackRecord next;
        public long dueTime;
        public Object action; // Runnable or FrameCallback
        public Object token;

        @UnsupportedAppUsage
        public void run(long frameTimeNanos) {
            if (token == FRAME_CALLBACK_TOKEN) {
                ((FrameCallback)action).doFrame(frameTimeNanos);
            } else {
                ((Runnable)action).run();
            }
        }
    }
    最终的执行是要调用CallbackRecord的run方法
    */
    CallbackRecord callbacks;
    synchronized (mLock) {
        // We use "now" to determine when callbacks become due because it's possible
        // for earlier processing phases in a frame to post callbacks that should run
        // in a following phase, such as an input event that causes an animation to start.
        final long now = System.nanoTime();
        //mCallbackQueues 是存储这不种类的callback的 比如动画,输入,绘制等,他们的回调是分开的
        //根据type来还有时间来查找需要回调的CallbackRecord
        callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(
                now / TimeUtils.NANOS_PER_MS);
        if (callbacks == null) {
            return;
        }
        mCallbacksRunning = true;

        // Update the frame time if necessary when committing the frame.
        // We only update the frame time if we are more than 2 frames late reaching
        // the commit phase.  This ensures that the frame time which is observed by the
        // callbacks will always increase from one frame to the next and never repeat.
        // We never want the next frame's starting frame time to end up being less than
        // or equal to the previous frame's commit frame time.  Keep in mind that the
        // next frame has most likely already been scheduled by now so we play it
        // safe by ensuring the commit time is always at least one frame behind.
        
        
        //CALLBACK_COMMIT 这个类型是除了系统那几种之外的类型,用户自动逸的可以发这种
        if (callbackType == Choreographer.CALLBACK_COMMIT) {
            final long jitterNanos = now - frameTimeNanos;
            Trace.traceCounter(Trace.TRACE_TAG_VIEW, "jitterNanos", (int) jitterNanos);
            if (jitterNanos >= 2 * mFrameIntervalNanos) {
                final long lastFrameOffset = jitterNanos % mFrameIntervalNanos
                        + mFrameIntervalNanos;
                if (DEBUG_JANK) {
                    Log.d(TAG, "Commit callback delayed by " + (jitterNanos * 0.000001f)
                            + " ms which is more than twice the frame interval of "
                            + (mFrameIntervalNanos * 0.000001f) + " ms!  "
                            + "Setting frame time to " + (lastFrameOffset * 0.000001f)
                            + " ms in the past.");
                    mDebugPrintNextFrameTimeDelta = true;
                }
                frameTimeNanos = now - lastFrameOffset;
                mLastFrameTimeNanos = frameTimeNanos;
            }
        }
    }
    try {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, CALLBACK_TRACE_TITLES[callbackType]);
        //
        for (CallbackRecord c = callbacks; c != null; c = c.next) {
            if (DEBUG_FRAMES) {
                Log.d(TAG, "RunCallback: type=" + callbackType
                        + ", action=" + c.action + ", token=" + c.token
                        + ", latencyMillis=" + (SystemClock.uptimeMillis() - c.dueTime));
            }
            //最终调用了回调,
            c.run(frameTimeNanos);
        }
    } finally {
        synchronized (mLock) {
            mCallbacksRunning = false;
            do {
                final CallbackRecord next = callbacks.next;
                recycleCallbackLocked(callbacks);
                callbacks = next;
            } while (callbacks != null);
        }
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

上面就是整个Choreographer (系统时间编排者)的请求VSync,以及接受的onVsync的过程

四、总结

ValueAnimator的时间脉冲是很好的设计,因为动画的过程速度,响应快慢,不能依靠自己的程序乱设定,统一执行可以带来最优的体验,

ValueAnimator完成了基本的属性动画的调度逻辑,后面的ObjectAnimator 可以在这个基础上扩展

心得:学习一个东西,不仅仅要知道怎么使用,还要学习里面的原理,这样遇到问题才能从容并不破的解决,还可以学习系统优秀的设计思想,提升自己的设计能力

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-10-29 13:09:42  更:2021-10-29 13:11:36 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 1:00:03-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码