• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

ValueAnimation 是刷新的

武飞扬头像
GuoXuan_CHN
帮助1

ValueAnimation 是如何刷新的

背景

之前,技术分享讲过ValueAnimation底层源码。但是,没有提到,Animation的更新。此篇文章,聚焦于ValueAnimation start方法开始,到Choreographer的响应回调。

ValueAnimator部分

ValueAnimator.start()

从思维逻辑上,start方法是动画启动的入口,那绘制应该也从这里开始寻找

	//ValueAnimator
    @Override
    public void start() {
        start(false);
    }
    
    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) {
                // 计算当前迭代的分数。
                float fraction = (float) (mSeekFraction - Math.floor(mSeekFraction));
                mSeekFraction = 1 - fraction;
            } else {
                mSeekFraction = 1   mRepeatCount - mSeekFraction;
            }
        }
        mStarted = true;
        mPaused = false;
        mRunning = false;
        mAnimationEndRequested = false;
        // 当start()被调用时,重置mLastFrameTime,这样如果动画正在运行,
        // 调用start()将把动画放到已经开始但尚未到达的第一帧阶段。
        mLastFrameTime = -1;
        mFirstFrameTime = -1;
        mStartTime = -1;
        addAnimationCallback(0);

        if (mStartDelay == 0 || mSeekFraction >= 0 || mReversing) {
            // 如果没有启动延迟,初始化动画并立即通知启动监听器,
            // 以与前面的行为保持一致。否则,将此延迟到开始延迟后的第一帧。
            startAnimation();
            if (mSeekFraction == -1) {
                // 无seek, 0时开始。注意,我们没有使用分数0的原因是,对于持续时间为0的动画,
                // 我们希望与前n的行为保持一致:立即跳到最终值。
                setCurrentPlayTime(0);
            } else {
                setCurrentFraction(mSeekFraction);
            }
        }
    }
学新通

需求要注意的是,在startAnimation方法中进行数值的初始化,和Linstener的调用。但是不在本篇的范围内,不做分析。其中重点是addAnimationCallback方法。

	//ValueAnimator
	private void addAnimationCallback(long delay) {
        if (!mSelfPulse) {
            return;
        }
        getAnimationHandler().addAnimationFrameCallback(this, delay);
    }
    
    public AnimationHandler getAnimationHandler() {
        return mAnimationHandler != null ? mAnimationHandler : AnimationHandler.getInstance();
    }

AnimationHandler

可以看到,addAnimationCallback方法,最终调用到的是AnimationHandleraddAnimationFrameCallback方法。

	 //AnimationHandler
 	public void addAnimationFrameCallback(final AnimationFrameCallback callback, long delay) {
 		//如果之前没有动画要展示,就找provider启动动画循环(假循环)
        if (mAnimationCallbacks.size() == 0) {
            getProvider().postFrameCallback(mFrameCallback);
        }
        
        //之前不存在,就往进加
        if (!mAnimationCallbacks.contains(callback)) {
            mAnimationCallbacks.add(callback);
        }

        if (delay > 0) {
            mDelayedCallbackStartTime.put(callback, (SystemClock.uptimeMillis()   delay));
        }
    }	
学新通

注释很清楚,我们应该往getProvider().postFrameCallback里看。

	//AnimationHandler
	 private AnimationFrameCallbackProvider getProvider() {
        if (mProvider == null) {
            mProvider = new MyFrameCallbackProvider();
        }
        return mProvider;
    }
    
	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里面塞回调。这个Choreographer我们后期会讲到,我们目前只需要知道,当Choreographer收到Vsync后,会依次回调Callback就好了,其中调用onFrame方法。

这里塞的是什么呢?mFrameCallback

	//AnimationHandler
	private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
        @Override
        public void doFrame(long frameTimeNanos) {
            doAnimationFrame(getProvider().getFrameTime());
            if (mAnimationCallbacks.size() > 0) {
                getProvider().postFrameCallback(this);
            }
        }
    };

doAnimationFrame

我们看到,会调用,doAnimationFrame

	//AnimationHandler
    private void doAnimationFrame(long frameTime) {
        long currentTime = SystemClock.uptimeMillis();
        final int size = mAnimationCallbacks.size();
        for (int i = 0; i < size; i  ) {
            final AnimationFrameCallback callback = mAnimationCallbacks.get(i);
            if (callback == null) {
                continue;
            }
          	//判断这个动画,有没有初始化,有没有延迟。就是该不该运行
            if (isCallbackDue(callback, currentTime)) {
            	//执行Callback。
                callback.doAnimationFrame(frameTime);
                if (mCommitCallbacks.contains(callback)) {
                    getProvider().postCommitCallback(new Runnable() {
                        @Override
                        public void run() {
                            commitAnimationFrame(callback, getProvider().getFrameTime());
                        }
                    });
                }
            }
        }
        cleanUpList();
    }
学新通

我们先关注callback.doAnimationFrame方法。这个callback是什么呢?getAnimationHandler().addAnimationFrameCallback(this, delay);。其实就是这个this嘛。我们应该往ValueAnimation里面找。

	//ValueAnimator
 	public final boolean doAnimationFrame(long frameTime) {
        if (mStartTime < 0) {
            mStartTime = mReversing
                    ? frameTime
                    : frameTime   (long) (mStartDelay * resolveDurationScale());
        }

        if (mPaused) {
            mPauseTime = frameTime;
            removeAnimationCallback();
            return false;
        } else if (mResumed) {
            mResumed = false;
            if (mPauseTime > 0) {
                mStartTime  = (frameTime - mPauseTime);
            }
        }

        if (!mRunning) {
            if (mStartTime > frameTime && mSeekFraction == -1) {
                return false;
            } else {
                // If mRunning is not set by now, that means non-zero start delay,
                // no seeking, not reversing. At this point, start delay has passed.
                mRunning = true;
                startAnimation();
            }
        }

        if (mLastFrameTime < 0) {
            if (mSeekFraction >= 0) {
                long seekTime = (long) (getScaledDuration() * mSeekFraction);
                mStartTime = frameTime - seekTime;
                mSeekFraction = -1;
            }
            mStartTimeCommitted = false; 
        }
        mLastFrameTime = frameTime;
    
    	//就是在这里形成了数据更新
        final long currentTime = Math.max(frameTime, mStartTime);
        boolean finished = animateBasedOnTime(currentTime);

        if (finished) {
        	//关注这里,当动画结束,怎么操作的。
            endAnimation();
        }
        return finished;
    }
学新通

在这里,我们关注的应该只有两个点,一个就是动画的更新,一个就是动画结束的处理。

动画更新

	//ValueAnimator
	boolean animateBasedOnTime(long currentTime) {
        boolean done = false;
        if (mRunning) {
            final long scaledDuration = getScaledDuration();
            final float fraction = scaledDuration > 0 ?
                    (float)(currentTime - mStartTime) / scaledDuration : 1f;
            final float lastFraction = mOverallFraction;
            final boolean newIteration = (int) fraction > (int) lastFraction;
            final boolean lastIterationFinished = (fraction >= mRepeatCount   1) &&
                    (mRepeatCount != INFINITE);
                    
            //判断是否结束啥的
            if (scaledDuration == 0) {
                done = true;
            } else if (newIteration && !lastIterationFinished) {
                if (mListeners != null) {
                    int numListeners = mListeners.size();
                    for (int i = 0; i < numListeners;   i) {
                        mListeners.get(i).onAnimationRepeat(this);
                    }
                }
            } else if (lastIterationFinished) {
                done = true;
            }
            mOverallFraction = clampFraction(fraction);
            
           	//计算当前迭代的部分,并考虑动画是否应该向后播放。当动画在迭代中向后播放时,迭代的分数将从1f到0f。
            float currentIterationFraction = getCurrentIterationFraction(
                    mOverallFraction, mReversing);
                    
             //刷新
            animateValue(currentIterationFraction);
        }
        return done;
    }
    
    void animateValue(float fraction) {
        fraction = mInterpolator.getInterpolation(fraction);
        mCurrentFraction = fraction;
        int numValues = mValues.length;
        for (int i = 0; i < numValues;   i) {
            mValues[i].calculateValue(fraction);
        }
        if (mUpdateListeners != null) {
            int numListeners = mUpdateListeners.size();
            for (int i = 0; i < numListeners;   i) {
            	//回调 - 我们在里面做我们设置的骚操作
                mUpdateListeners.get(i).onAnimationUpdate(this);
            }
        }
    }
学新通

注释写的很清楚,在这里,ValueAnimation就得到了更新

动画结束

animateBasedOnTime的返回值,决定了,这个动画是否结束。如果结束了就执行,endAnimation()

	//ValueAnimator
	 private void endAnimation() {
        if (mAnimationEndRequested) {
            return;
        }
        
        //从AnimationHandler移除Callback
        removeAnimationCallback();

        mAnimationEndRequested = true;
        mPaused = false;
        boolean notify = (mStarted || mRunning) && mListeners != null;
        if (notify && !mRunning) {
            // If it's not yet running, then start listeners weren't called. Call them now.
            notifyStartListeners();
        }
        mRunning = false;
        mStarted = false;
        mStartListenersCalled = false;
        mLastFrameTime = -1;
        mFirstFrameTime = -1;
        mStartTime = -1;
        if (notify && mListeners != null) {
            ArrayList<AnimatorListener> tmpListeners =
                    (ArrayList<AnimatorListener>) mListeners.clone();
            int numListeners = tmpListeners.size();
            for (int i = 0; i < numListeners;   i) {
            	//通知回调
                tmpListeners.get(i).onAnimationEnd(this, mReversing);
            }
        }
        // mReversing needs to be reset *after* notifying the listeners for the end callbacks.
        mReversing = false;
        if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) {
            Trace.asyncTraceEnd(Trace.TRACE_TAG_VIEW, getNameForTrace(),
                    System.identityHashCode(this));
        }
    }
    
    private void removeAnimationCallback() {
        if (!mSelfPulse) {
            return;
        }
        getAnimationHandler().removeCallback(this);
    }
学新通

我们看到,在endAnimation中,如果动画结束,就通知删除回调。

doFrame

到这里,AnimationHandlerdoAnimationFrame就分析完了。我们回到mFrameCallback

	//AnimationHandler
    private final Choreographer.FrameCallback mFrameCallback = new Choreographer.FrameCallback() {
        @Override
        public void doFrame(long frameTimeNanos) {
            doAnimationFrame(getProvider().getFrameTime());
            if (mAnimationCallbacks.size() > 0) {
                getProvider().postFrameCallback(this);
            }
        }
    };

我们刚才分析了,在doAnimationFrame里,如果动画已经结束了,就会从AnimationHandler中删除Callback, 这个Callback就放在mAnimationCallbacks中,这个mAnimationCallbacks.size() > 0的判断就很合理了。如果还有动画没有执行完成,就注册下一次Choreographer收到Vsync的刷新。

总结

ValueAnimator驱动Choreographer监听Vsync,把回调交给Chreographer,等待下次刷新的时候,刷新自己。

AnimationHanlder相当于是一个分发中心。当有动画要执行,就获取一个全局的变量,从AnimationHanlder获取的位置,即AnimationHandler.getInstance(),可以看出。各个动画,也不用直接操作Chreographer,交给AnimationHanlder就可以了。AnimationHanlder会在Vsync到的时候,被Chreographer通知,来调用注册的各个动画更新。

既然,ValueAnimatorAnimationHanlder,推动Chreographer注册下一次Vsync,引起进度更新,那怎么引起页面的刷新呢?

ViewRootImpl 部分

ValueAnimator使用时

  private final ValueAnimator.AnimatorUpdateListener listener = animation -> {
        progress = (Float) animation.getAnimatedValue();
        postInvalidate();
    };
    
    	animator = ValueAnimator.ofFloat(ShrinkModel.PROGRESS_START, ShrinkModel.PROGRESS_END);
        animator.setInterpolator(new LinearInterpolator());
        animator.setDuration(duration);
        animator.addUpdateListener(listener);
        animator.start();

我们可以看到,当onAnimationUpdate时,我们手动调用了postInvalidate()postInvalidate()相当于在UI线程调用invalidate()。这个是同事写的代码。当然我们可以直接写invalidate()

//View
 public void postInvalidate() {
        postInvalidateDelayed(0);
    }
    
    //View
    public void postInvalidateDelayed(long delayMilliseconds) {
        //调到ViewRootImpl
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
        }
    }
    
    //ViewRootImpl
    public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
    	  //往Handler里放,这个Handler绑定的UI线程
        Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
        mHandler.sendMessageDelayed(msg, delayMilliseconds);
    }
    
    //ViewRootImpl
     @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_INVALIDATE:
                    ((View) msg.obj).invalidate();
                    break;
               //剩下的忽略
             }
         }
学新通

view.invalidata时

View调用invalidata时,会调用到ViewRootImplinvalidateChildInParent,为什么会这样?

不熟悉绘制流程的,应该去补习了。另外检验自己懂不懂的标准就是,能不能回答出postInvalidatainvalidatarequestLayout的区别。

但是提两个点,在Activityresume后,会讲DecorView放进PhoneWindow,通过WindowManager.addView方法。在这里,最终会调用到ViewRootImpl.setView。在里面,会把自己赋值为DecorViewParent。所以,不管是invalidata还是requestLayout,都会调用到ViewRootImpl

//ViewRootImpl
 public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
        checkThread();
        if (DEBUG_DRAW) Log.v(mTag, "Invalidate child: "   dirty);

        if (dirty == null) {
            invalidate();
            return null;
        } else if (dirty.isEmpty() && !mIsAnimating) {
            return null;
        }

        if (mCurScrollY != 0 || mTranslator != null) {
            mTempRect.set(dirty);
            dirty = mTempRect;
            if (mCurScrollY != 0) {
                dirty.offset(0, -mCurScrollY);
            }
            if (mTranslator != null) {
                mTranslator.translateRectInAppWindowToScreen(dirty);
            }
            if (mAttachInfo.mScalingRequired) {
                dirty.inset(-1, -1);
            }
        }

        invalidateRectOnScreen(dirty);

        return null;
    }
    
     private void invalidateRectOnScreen(Rect dirty) {
        final Rect localDirty = mDirty;
        if (!localDirty.isEmpty() && !localDirty.contains(dirty)) {
            mAttachInfo.mSetIgnoreDirtyState = true;
            mAttachInfo.mIgnoreDirtyState = true;
        }

        // Add the new dirty rect to the current one
        localDirty.union(dirty.left, dirty.top, dirty.right, dirty.bottom);
        // Intersect with the bounds of the window to skip
        // updates that lie outside of the visible region
        final float appScale = mAttachInfo.mApplicationScale;
        final boolean intersected = localDirty.intersect(0, 0,
                (int) (mWidth * appScale   0.5f), (int) (mHeight * appScale   0.5f));
        if (!intersected) {
            localDirty.setEmpty();
        }
        if (!mWillDrawSoon && (intersected || mIsAnimating)) {
            scheduleTraversals();
        }
    } 
学新通

我们看到,如果没有特殊情况,会走scheduleTraversals。这个方法,大家应该很熟了吧。引发绘制嘛。

ViewRootImpl.scheduleTraversals

    //ViewRootImpl
    void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            
            	//防止同步消息屏障
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            
            	//向Choreographer注册Vsync回调
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
                    
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }
学新通

这里有两个比较重要的内容:

  • 同步消息屏障
  • Choreographer注册Vsync回调

我们先看回调吧,当Vsync来的时候,ViewRootImpl会怎么走

 //ViewRootImpl
    final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }
    final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
    
    void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            	//解除同步消息屏障
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

            if (mProfile) {
                Debug.startMethodTracing("ViewAncestor");
            }
		
		//里面进行 m - l - d,页面就重绘了。
            performTraversals();

            if (mProfile) {
                Debug.stopMethodTracing();
                mProfile = false;
            }
        }
    }
学新通

我们可以看到,当Vsync到的时候,会解除同步消息屏障。再触发重绘。

下面我们来看看Choreographer

Choreographer部分

postCallback

	//Choreographer
    public void postCallback(int callbackType, Runnable action, Object token) {
        postCallbackDelayed(callbackType, action, token, 0);
    }
    
        public void postCallbackDelayed(int callbackType,
            Runnable action, Object token, long delayMillis) {
        //去掉了错误检查
        postCallbackDelayedInternal(callbackType, action, token, delayMillis);
    }
    
    private void postCallbackDelayedInternal(int callbackType,
            Object action, Object token, long delayMillis) {
            
         synchronized (mLock) {
            final long now = SystemClock.uptimeMillis();
            final long dueTime = now   delayMillis;
            
            	//mCallbackQueues是一个回调队列。数组加链表的结构。
            mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

            if (dueTime <= now) {
            		//如果理解执行,就跑这个
                scheduleFrameLocked(now);
            } else {
            		//不立即执行,就放一个异步消息,保证优先执行
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
                msg.arg1 = callbackType;
                msg.setAsynchronous(true);
                mHandler.sendMessageAtTime(msg, dueTime);
            }
        }
    }
    
       private void scheduleFrameLocked(long now) {
        if (!mFrameScheduled) {
            mFrameScheduled = true;
            if (USE_VSYNC) {
                if (isRunningOnLooperThreadLocked()) {
                	//使用了垂直消息同步,还通过了检查,就这么跑
                    scheduleVsyncLocked();
                } else {
                    Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
                    msg.setAsynchronous(true);
                    mHandler.sendMessageAtFrontOfQueue(msg);
                }
            } else {
                final long nextFrameTime = Math.max(
                        mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS   sFrameDelay, now);
                Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
                msg.setAsynchronous(true);
                mHandler.sendMessageAtTime(msg, nextFrameTime);
            }
        }
    }
    
    private void scheduleVsyncLocked() {
        mDisplayEventReceiver.scheduleVsync();
    }
    
    //
     public void scheduleVsync() {
        if (mReceiverPtr == 0) {
        } else {
        	//向Native请求要来的Vsync。并且,只会回调一次
            nativeScheduleVsync(mReceiverPtr);
        }
    }
学新通

Vsyn到来的时候,会调用FrameDisplayEventReceiveronVsync方法

    private final class FrameDisplayEventReceiver extends DisplayEventReceiver
            implements Runnable {
        //去掉构造和本地变量

        @Override
        public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
        	//这些都是特殊情况
            if (builtInDisplayId != SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN) {
                scheduleVsync();
                return;
            }
            long now = System.nanoTime();
            if (timestampNanos > now) {
                  timestampNanos = now;
            }

            if (mHavePendingVsync) {
            } 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);
        }
    }
学新通

我们可以看到,最总会走到doFrame方法

doFrame

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

            if (DEBUG_JANK && mDebugPrintNextFrameTimeDelta) {
                mDebugPrintNextFrameTimeDelta = false;
            }

            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) {
                }
                final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
                frameTimeNanos = startNanos - lastFrameOffset;
            }

            if (frameTimeNanos < mLastFrameTimeNanos) {
                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 {
        	//在这里,处理的消息回调 - 包括动画和绘制
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
            AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);

            mFrameInfo.markInputHandlingStart();
            doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);

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

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

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

学新通

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhibceje
系列文章
更多 icon
同类精品
更多 icon
继续加载