精华内容
下载资源
问答
  • 最近很多朋友前来反映在使用电脑的时候发现电脑屏幕一直闪烁,眼睛非常不舒服。朋友们并不知从何下手,那么如果我们遇到这种情况该如何解决呢?其实解决方法挺多的,下面小编和大家分享下怎么解决问题的方法,希望...

    如今随着互联网时代的发展,电脑早已普及各地。最近很多朋友前来反映在使用电脑的时候发现电脑屏幕一直闪烁,眼睛非常不舒服。朋友们并不知从何下手,那么如果我们遇到这种情况该如何解决呢?其实解决方法挺多的,下面小编和大家分享下怎么解决问题的方法,希望能够帮助大家!

    方法一:1、首先屏幕刷新率不够导致的,需要右击桌面空白处选择屏幕分辨率。

    eb99fbf9389f866eb4d5e0553eeda4f6.png

    2、点击高级设置,之后将切换为监视器,修改屏幕刷新频率60赫兹,点击确定。

    a2268030112ec0e66988e09acd008b9d.png
    6bca3f80212e76eac762cb7a5cc1d7e1.png

    方法二:1、可能是显卡驱动问题导致,显卡驱动没装好或没装。

    e9b6c27d838564767d660028fe5a47df.png

    2、首先打开360安全卫士,点击上方的功能大全,再点击驱动大师。

    0fe9dda316bf974f7010434aa8caf5d8.png

    3、检测诊断下完成后,出现以下问题点击一键修复。

    5afc6942bddb4ec3090086da09f1db36.png

    方法三:还也可能是电源老化,供电不足导致,显卡驱动不稳定,换下新电源试试。

    c2ea084fa56f2b4737f97957099470f6.png

    方法四:1、还可能是显示器或显卡硬件问题导致,换下显示器或者把显卡换了。

    6652fb9462a16c5b773b5f08c6ee24bf.png

    综上所述,以上内容就是电脑屏幕闪烁是什么原因的全部内容了,你知道了吗?相信大家看了以上这篇文章对该问题有了更多的了解。

    展开全文
  • 之前在整理知识的时候,看到android屏幕刷新机制这一块,以前一直只是知道,Android每16.6ms会去刷新一次屏幕,也就是我们常说的60fpx,那么问题也来了: 16.6ms刷新一次是什么一次,是以这个固定的频率去重新绘制...

    前言

    之前在整理知识的时候,看到android屏幕刷新机制这一块,以前一直只是知道,Android每16.6ms会去刷新一次屏幕,也就是我们常说的60fpx,那么问题也来了:

    16.6ms刷新一次是什么一次,是以这个固定的频率去重新绘制吗?但是请求绘制的代码时机调用是不同的,如果操作是在16.6ms快结束的时候去绘制的,那么岂不是就是时间少于16.6ms,也会产生丢帧的问题?再者熟悉绘制的朋友都知道请求绘制是一个Message对象,那这个Message是会放进主线程Looper的队列中吗,那怎么能保证在16.6ms之内会执行到这个Message呢?

    文章较长,请耐心观看,水平不足,如果错误,还望指出

    View ## invalidate()

    既然是绘制,那么就从这个方法看起吧

    public void invalidate() {
            invalidate(true);
        }
        public void invalidate(boolean invalidateCache) {
            invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
        }
        void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
                boolean fullInvalidate) {
                ......
                final AttachInfo ai = mAttachInfo;
                final ViewParent p = mParent;
                if (p != null && ai != null && l < r && t < b) {
                    final Rect damage = ai.mTmpInvalRect;
                    damage.set(l, t, r, b);
                    p.invalidateChild(this, damage);
                }
               .....
            }
        }
    

    主要关注这个p,最终调用的是它的invalidateChild()方法,那么这个p到底是个啥,ViewParent是一个接口,那很明显p是一个实现类,答案是ViewRootImpl,我们知道View树的根节点是DecorView,那DecorView的Parent是不是ViewRootImpl呢

    熟悉Activity启动流程的朋友都知道,Activity 的启动是在 ActivityThread 里完成的,handleLaunchActivity() 会依次间接的执行到 Activity 的 onCreate(), onStart(), onResume()。在执行完这些后 ActivityThread 会调用 WindowManager#addView(),而这个 addView()最终其实是调用了 WindowManagerGlobal 的 addView() 方法,我们就从这里开始看,因为是隐藏类,所以这里借助Source Insight查看WindowManagerGlobal

    public void addView(View view, ViewGroup.LayoutParams params,
                Display display, Window parentWindow) {
            synchronized (mLock) {
                .....
                root = new ViewRootImpl(view.getContext(), display);
                view.setLayoutParams(wparams);
                mViews.add(view);
                mRoots.add(root);
                mParams.add(wparams);
            }
            try {
                root.setView(view, wparams, panelParentView);
            } catch (RuntimeException e) {
                // BadTokenException or InvalidDisplayException, clean up.
                synchronized (mLock) {
                    final int index = findViewLocked(view, false);
                    if (index >= 0) {
                        removeViewLocked(index, true);
                    }
                }
                throw e;
            }
        }
     public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
            synchronized (this) {
                    ....
                    view.assignParent(this);
                    ...
                }
            }
    void assignParent(ViewParent parent) {
            if (mParent == null) {
                mParent = parent;
            } else if (parent == null) {
                mParent = null;
            } 
          }
    

    参数是ViewParent,所以在这里就直接将DecorView和ViewRootImpl给绑定起来了,所以也验证了上述的结论,子 View 里执行 invalidate() 之类的操作,最后都会走到 ViewRootImpl 里来

    ViewRootImpl##scheduleTraversals

    根据上面的链路最终是会执行到scheduleTraversals方法

    void scheduleTraversals() {
            if (!mTraversalScheduled) {
                mTraversalScheduled = true;
                mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
                mChoreographer.postCallback(
                        Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
                if (!mUnbufferedInputDispatch) {
                    scheduleConsumeBatchedInput();
                }
                notifyRendererOfFramePending();
                pokeDrawLockIfNeeded();
            }
        }
    方法不长,首先如果mTraversalScheduled为false,进入判断,同时将此标志位置位true,第二句暂时不管,后续会讲到,主要看postCallback方法,传递进去了一个mTraversalRunnable对象,可以看到这里是一个请求绘制的Runnable对象
    final class TraversalRunnable implements Runnable {
            @Override
            public void run() {
                doTraversal();
            }
        }
    void doTraversal() {
            if (mTraversalScheduled) {
                mTraversalScheduled = false;
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
                if (mProfile) {
                    Debug.startMethodTracing("ViewAncestor");
                }
                performTraversals();
                if (mProfile) {
                    Debug.stopMethodTracing();
                    mProfile = false;
                }
            }
        }
    

    doTraversal方法里面,又将mTraversalScheduled置位了false,对应上面的scheduleTraversals方法,可以看到一个是postSyncBarrier(),而在这里又是removeSyncBarrier(),这里其实涉及到一个很有意思的东西,叫同步屏障,等会会拉出来单独讲解,然后调用了performTraversals(),这个方法应该都知道了,View 的测量、布局、绘制都是在这个方法里面发起的,代码逻辑太多了,就不贴出来了,暂时只需要知道这个方法是发起测量的开始。

    这里我们暂时总结一下,当子View调用invalidate的时候,最终是调用到ViewRootImpl的performTraversals()方法的,performTraversals()方法又是在doTraversal里面调用的,doTraversal又是封装在mTraversalRunnable之中的,那么这个Runnable的执行时机又在哪呢

    Choreographer##postCallback

    回到上面的scheduleTraversals方法中,mTraversalRunnable是传递进了Choreographer的postCallback方法之中

    private void postCallbackDelayedInternal(int callbackType,
                Object action, Object token, long delayMillis) {
            if (DEBUG_FRAMES) {
            synchronized (mLock) {
                final long now = SystemClock.uptimeMillis();
                final long dueTime = now + delayMillis;
                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);
                }
            }
        }
    

    可以看到内部好像有一个类似MessageQueue的东西,将Runnable通过delay时间给存储起来的,因为我们这里传递进来的delay是0,所以执行scheduleFrameLocked(now)方法

    private void scheduleFrameLocked(long now) {
            if (!mFrameScheduled) {
                mFrameScheduled = true;
                    if (isRunningOnLooperThreadLocked()) {
                        scheduleVsyncLocked();
                    } else {
                        Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
                        msg.setAsynchronous(true);
                        mHandler.sendMessageAtFrontOfQueue(msg);
                    }
            }
        }
    private boolean isRunningOnLooperThreadLocked() {
            return Looper.myLooper() == mLooper;
        }
    

    这里有一个判断isRunningOnLooperThreadLocked,看着像是判断当前线程是否是主线程,如果是的话,调用scheduleVsyncLocked()方法,不是的话会发送一个MSG_DO_SCHEDULE_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);
            }
        }
    

    如果mReceiverPtr不等于0的话,会去调用nativeScheduleVsync(mReceiverPtr),这是个native方法,暂不跟踪到C++里面去了,看着英文方法像是一个安排信号的意思
    之前是把CallBack存储在一个Queue之中了,那么必然有执行的方法

    void doCallbacks(int callbackType, long frameTimeNanos) {
            CallbackRecord callbacks;
            synchronized (mLock) {
            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);
            }
        }
    

    看一下这个方法在哪里调用的,走到了doFrame方法里面

    void doFrame(long frameTimeNanos, int frame) {
            final long startNanos;
            synchronized (mLock) {
            try {
                .....
                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);
            }
                .....
        }
    

    那么这样一来,就找到关键的方法了:doFrame(),这个方法里会根据一个时间戳去队列里取任务出来执行,而这个任务就是 ViewRootImpl 封装起来的 doTraversal() 操作,而 doTraversal() 会去调用 performTraversals() 开始根据需要测量、布局、绘制整颗 View 树。所以剩下的问题就是 doFrame() 这个方法在哪里被调用了。

    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);
            }
    
            @Override
            public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {     
                    scheduleVsync();
                    return;
                }
                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);
            }
        }
    

    可以看到,是在onVsync回调中,Message msg = Message.obtain(mHandler, this),传入了this,然后会执行到run方法里面,自然也就执行了doFrame方法,所以最终问题也就来了,这个onVsync()这个是什么时候回调来的,这里查询了网上的一些资料,是这么解释的

    FrameDisplayEventReceiver继承自DisplayEventReceiver接收底层的VSync信号开始处理UI过程。VSync信号由SurfaceFlinger实现并定时发送。FrameDisplayEventReceiver收到信号后,调用onVsync方法组织消息发送到主线程处理。这个消息主要内容就是run方法里面的doFrame了,这里mTimestampNanos是信号到来的时间参数。

    那也就是说,onVsync是底层回调回来的,那也就是每16.6ms,底层会发出一个屏幕刷新的信号,然后会回调到onVsync方法之中,但是有一点很奇怪,底层怎么知道上层是哪个app需要这个信号来刷新呢,结合日常开发,要实现这种一般使用观察者模式,将app本身注册下去,但是好像也没有看到哪里有往底层注册的方法,对了,再次回到上面的那个native方法,nativeScheduleVsync(mReceiverPtr),那么这个方法大体的作用也就是注册监听了,

    同步屏障

    总结下上面的知识,我们调用了 invalidate(),requestLayout(),等之类刷新界面的操作时,并不是马上就会执行这些刷新的操作,而是通过 ViewRootImpl 的 scheduleTraversals() 先向底层注册监听下一个屏幕刷新信号事件,然后等下一个屏幕刷新信号来的时候,才会去通过 performTraversals() 遍历绘制 View 树来执行这些刷新操作。

    那么这样是不是产生一个问题,因为我们知道,平常Handler发送的消息都是同步消息的,也就是Looper会从MessageQueue中不断去取Message对象,一个Message处理完了之后,再去取下一个Message,那么绘制的这个Message如何尽量保证能够在16.6ms之内执行到呢,

    这里就使用到了一个同步屏障的东西,再次回到scheduleTraversals代码

    void scheduleTraversals() {
            if (!mTraversalScheduled) {
                mTraversalScheduled = true;
                mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
                mChoreographer.postCallback(
                        Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
                if (!mUnbufferedInputDispatch) {
                    scheduleConsumeBatchedInput();
                }
                notifyRendererOfFramePending();
                pokeDrawLockIfNeeded();
            }
        }
    

    mHandler.getLooper().getQueue().postSyncBarrier(),这一句上面没有分析,进入到方法里

    private int postSyncBarrier(long when) {
            synchronized (this) {
                final int token = mNextBarrierToken++;
                final Message msg = Message.obtain();
                msg.markInUse();
                msg.when = when;
                msg.arg1 = token;
                Message prev = null;
                Message p = mMessages;
                if (when != 0) {
                    while (p != null && p.when <= when) {
                        prev = p;
                        p = p.next;
                    }
                }
                if (prev != null) { // invariant: p == prev.next
                    msg.next = p;
                    prev.next = msg;
                } else {
                    msg.next = p;
                    mMessages = msg;
                }
                return token;
            }
        }
    

    可以看到,就是生成Message对象,然后进一些赋值,但是细心的朋友可能会发现,这个msg为什么没有设置target,熟悉Handler流程的朋友应该清楚,最终消息是通过msg.target发送出去的,一般是指Handler对象

    那我们再次回到MessageQueue的next方法中看看

    Message next() {
            for (;;) {
                ....
                synchronized (this) {
                    ...
                    //对,就是这里了,target==null
                    if (msg != null && msg.target == null) {                 
                        do {
                            prevMsg = msg;
                            msg = msg.next;
                        } while (msg != null && !msg.isAsynchronous());
                    }
                    if (msg != null) {
                        if (now < msg.when) {                     
                            nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                        } else {
                            // Got a message.
                            mBlocked = false;
                            if (prevMsg != null) {
                                prevMsg.next = msg.next;
                            } else {
                                mMessages = msg.next;
                            }
                            msg.next = null;
                            if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                            msg.markInUse();
                            return msg;
                        }
                    } else {                
                        nextPollTimeoutMillis = -1;
                    }
            }
        }
    

    可以看到有一个Message.target==null的判断, do while循环遍历消息链表,跳出循环时,msg指向离表头最近的一个消息,此时返回msg对象

    可以看到,当设置了同步屏障之后,next函数将会忽略所有的同步消息,返回异步消息。换句话说就是,设置了同步屏障之后,Handler只会处理异步消息。再换句话说,同步屏障为Handler消息机制增加了一种简单的优先级机制,异步消息的优先级要高于同步消息

    这样的话,能够尽快的将绘制的Message给取出来执行,嗯,这里为什么说是尽快呢,因为,同步屏障是在 scheduleTraversals() 被调用时才发送到消息队列里的,也就是说,只有当某个 View 发起了刷新请求时,在这个时刻后面的同步消息才会被拦截掉。如果在 scheduleTraversals() 之前就发送到消息队列里的工作仍然会按顺序依次被取出来执行

    总结

    • View的刷新请求都会走到ViewRootImpl的scheduleTraversals() 中,通过Runnable的形式形成Message放进队列中,并且发送同步屏障,拦截所以同步消息,处理异步消息,以此尽快的保证执行刷新任务
    • 常说的每隔16.6ms刷新一次屏幕实际上是,底层会以这个频率来切换每一帧的画面,只有当View发起了刷新请求时,App才会想底层去注册监听下一个屏幕的刷新信号,并且才能受到下一次信号到来的通知回调onVsync
    • App负责计算屏幕刷新的数据,但是并非计算完成后就会立即刷新数据,更多的取决于是否到了下一次底层要刷新屏幕的指令回调的时机,所以也就回答了上面的问题,每次指令到达时才会去刷新数据,尽可能的保证刷新数据的任务有足够16.6ms的时间
    • 造成屏幕丢帧的原因也就很明显了,1.View树绘制的任务时长大于了16.6ms,此时下一个信号来临,导致丢帧 2.虽然采用了同步屏障的方法来保证足够View绘制任务的时间,但是如果同步屏障之间的Message耗时过长,也导致遍历绘制 View 树的工作迟迟不能开始,从而超过了 16.6 ms 底层切换下一帧画面的时机,这也就是主线程不要做耗时操作的原因了

    最后

    如果你看到了这里,觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言。一定会认真查询,修正不足。谢谢。

    最后针对Android程序员,除了上面的知识体系,我这边给大家整理了一些资料,其中分享内容包括不限于高级UI、性能优化、移动架构师、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter等全方面的Android进阶实践技术;希望能帮助到大家,也节省大家在网上搜索资料的时间来学习,也可以分享动态给身边好友一起学习!关注我的主页个人说明有惊喜哦~

    展开全文
  • 我的linux的vim最大化后拖动滑块下拉代码,qt等软件添加文件窗口出现花屏,切换下窗口或鼠标选择就恢复了,有没有大神知道是什么问题,困扰好长时间了,一直没有解决。 ![图片说明]...
  • 看到android屏幕刷新机制这一块,以前一直只是知道,Android每16.6ms会去刷新一次屏幕,也就是我们常说的60fpx,那么问题也来了:16.6ms刷新一次是什么一次,是以这个固定的频率去重新绘制吗?但是请求绘制的代码...

    作者:散人丶
    来源:https://juejin.im/post/5ce686a46fb9a07ec754f470

    前言

    之前在整理知识的时候,看到android屏幕刷新机制这一块,以前一直只是知道,Android每16.6ms会去刷新一次屏幕,也就是我们常说的60fpx,那么问题也来了:

    16.6ms刷新一次是什么一次,是以这个固定的频率去重新绘制吗?但是请求绘制的代码时机调用是不同的,如果操作是在16.6ms快结束的时候去绘制的,那么岂不是就是时间少于16.6ms,也会产生丢帧的问题?再者熟悉绘制的朋友都知道请求绘制是一个Message对象,那这个Message是会放进主线程Looper的队列中吗,那怎么能保证在16.6ms之内会执行到这个Message呢?

    文章较长,请耐心观看,水平不足,如果错误,还望指出

    View ## invalidate()

    既然是绘制,那么就从这个方法看起吧

     1public void invalidate() {
    2        invalidate(true);
    3    }
    4    public void invalidate(boolean invalidateCache) {
    5        invalidateInternal(00, mRight - mLeft, mBottom - mTop, invalidateCache, true);
    6    }
    7    void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache, 8            boolean fullInvalidate) {
    9            ......
    10            final AttachInfo ai = mAttachInfo;
    11            final ViewParent p = mParent;
    12            if (p != null && ai != null && l 13                final Rect damage = ai.mTmpInvalRect;
    14                damage.set(l, t, r, b);
    15                p.invalidateChild(this, damage);
    16            }
    17           .....
    18        }
    19    }

    主要关注这个p,最终调用的是它的invalidateChild()方法,那么这个p到底是个啥,ViewParent是一个接口,那很明显p是一个实现类,答案是ViewRootImpl,我们知道View树的根节点是DecorView,那DecorView的Parent是不是ViewRootImpl

    熟悉Activity启动流程的朋友都知道,Activity 的启动是在 ActivityThread
    里完成的,handleLaunchActivity() 会依次间接的执行到 Activity 的 onCreate()onStart(),onResume()。在执行完这些后 ActivityThread 会调用 WindowManager#addView(),而这个addView()最终其实是调用了 WindowManagerGlobal 的 addView()
    方法,我们就从这里开始看,因为是隐藏类,所以这里借助Source Insight查看WindowManagerGlobal

     1public void addView(View view, ViewGroup.LayoutParams params, 2            Display display, Window parentWindow{
    3        synchronized (mLock) {
    4            .....
    5            root = new ViewRootImpl(view.getContext(), display);
    6            view.setLayoutParams(wparams);
    7            mViews.add(view);
    8            mRoots.add(root);
    9            mParams.add(wparams);
    10        }
    11        try {
    12            root.setView(view, wparams, panelParentView);
    13        } catch (RuntimeException e) {
    14            // BadTokenException or InvalidDisplayException, clean up.
    15            synchronized (mLock) {
    16                final int index = findViewLocked(view, false);
    17                if (index >= 0) {
    18                    removeViewLocked(index, true);
    19                }
    20            }
    21            throw e;
    22        }
    23    }
    24 public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView{
    25        synchronized (this) {
    26                 ....
    27                view.assignParent(this);
    28                   ...
    29            }
    30        }
    31void assignParent(ViewParent parent{
    32        if (mParent == null) {
    33            mParent = parent;
    34        } else if (parent == null) {
    35            mParent = null;
    36        } 
    37      }

    参数是ViewParent,所以在这里就直接将DecorViewViewRootImpl给绑定起来了,所以也验证了上述的结论,子 View
    里执行 invalidate() 之类的操作,最后都会走到 ViewRootImpl 里来

    ViewRootImpl##scheduleTraversals

    根据上面的链路最终是会执行到scheduleTraversals方法

     1void scheduleTraversals({
    2        if (!mTraversalScheduled) {
    3            mTraversalScheduled = true;
    4            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
    5            mChoreographer.postCallback(
    6                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
    7            if (!mUnbufferedInputDispatch) {
    8                scheduleConsumeBatchedInput();
    9            }
    10            notifyRendererOfFramePending();
    11            pokeDrawLockIfNeeded();
    12        }
    13    }

    方法不长,首先如果mTraversalScheduled为false,进入判断,同时将此标志位置位true,第二句暂时不管,后续会讲到,主要看postCallback方法,传递进去了一个mTraversalRunnable对象,可以看到这里是一个请求绘制的Runnable对象

     1final class TraversalRunnable implements Runnable {
    2        @Override
    3        public void run() {
    4            doTraversal();
    5        }
    6    }
    7void doTraversal() {
    8        if (mTraversalScheduled) {
    9            mTraversalScheduled = false;
    10    mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
    11            if (mProfile) {
    12                Debug.startMethodTracing("ViewAncestor");
    13            }
    14            performTraversals();
    15            if (mProfile) {
    16                Debug.stopMethodTracing();
    17                mProfile = false;
    18            }
    19        }
    20    }

    doTraversal方法里面,又将mTraversalScheduled置位了false,对应上面的scheduleTraversals方法,可以看到一个是postSyncBarrier(),而在这里又是removeSyncBarrier(),这里其实涉及到一个很有意思的东西,叫同步屏障,等会会拉出来单独讲解,然后调用了performTraversals(),这个方法应该都知道了,View
    的测量、布局、绘制都是在这个方法里面发起的,代码逻辑太多了,就不贴出来了,暂时只需要知道这个方法是发起测量的开始。

    这里我们暂时总结一下,当子View调用invalidate的时候,最终是调用到ViewRootImpl的performTraversals()方法的,performTraversals()方法又是在doTraversal里面调用的,doTraversal又是封装在mTraversalRunnable之中的,那么这个Runnable的执行时机又在哪呢

    Choreographer##postCallback

    回到上面的scheduleTraversals方法中,mTraversalRunnable是传递进了Choreographer的postCallback方法之中

     1private void postCallbackDelayedInternal(int callbackType, 2            Object action, Object token, long delayMillis) {
    3        if (DEBUG_FRAMES) {
    4        synchronized (mLock) {
    5            final long now = SystemClock.uptimeMillis();
    6            final long dueTime = now + delayMillis;
    7            mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
    8
    9            if (dueTime <= now) {
    10                scheduleFrameLocked(now);
    11            } else {
    12                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
    13                msg.arg1 = callbackType;
    14                msg.setAsynchronous(true);
    15                mHandler.sendMessageAtTime(msg, dueTime);
    16            }
    17        }
    18    }

    可以看到内部好像有一个类似MessageQueue的东西,将Runnable通过delay时间给存储起来的,因为我们这里传递进来的delay是0,所以执行scheduleFrameLocked(now)方法

     1private void scheduleFrameLocked(long now) {
    2        if (!mFrameScheduled) {
    3            mFrameScheduled = true;
    4                if (isRunningOnLooperThreadLocked()) {
    5                    scheduleVsyncLocked();
    6                } else {
    7                    Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
    8                    msg.setAsynchronous(true);
    9                    mHandler.sendMessageAtFrontOfQueue(msg);
    10                }
    11        }
    12    }
    13private boolean isRunningOnLooperThreadLocked() {
    14        return Looper.myLooper() == mLooper;
    15    }

    这里有一个判断isRunningOnLooperThreadLocked,看着像是判断当前线程是否是主线程,如果是的话,调用scheduleVsyncLocked()方法,不是的话会发送一个MSG_DO_SCHEDULE_VSYNC消息,但是最终都会调用这个方法

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

    如果mReceiverPtr不等于0的话,会去调用nativeScheduleVsync(mReceiverPtr),这是个native方法,暂不跟踪到C++里面去了,看着英文方法像是一个安排信号的意思

    之前是把CallBack存储在一个Queue之中了,那么必然有执行的方法

     1void doCallbacks(int callbackType, long frameTimeNanos) {
    2        CallbackRecord callbacks;
    3        synchronized (mLock) {
    4        try {
    5            Trace.traceBegin(Trace.TRACE_TAG_VIEW, CALLBACK_TRACE_TITLES[callbackType]);
    6            for (CallbackRecord c = callbacks; c != null; c = c.next) {
    7                if (DEBUG_FRAMES) {
    8                    Log.d(TAG, "RunCallback: type=" + callbackType
    9                            + ", action=" + c.action + ", token=" + c.token
    10                            + ", latencyMillis=" + (SystemClock.uptimeMillis() - c.dueTime));
    11                }
    12                c.run(frameTimeNanos);
    13            }
    14        } finally {
    15            synchronized (mLock) {
    16                mCallbacksRunning = false;
    17                do {
    18                    final CallbackRecord next = callbacks.next;
    19                    recycleCallbackLocked(callbacks);
    20                    callbacks = next;
    21                } while (callbacks != null);
    22            }
    23            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    24        }
    25    }

    看一下这个方法在哪里调用的,走到了doFrame方法里面

     1void doFrame(long frameTimeNanos, int frame) {
    2        final long startNanos;
    3        synchronized (mLock) {
    4        try {
    5             .....
    6            mFrameInfo.markInputHandlingStart();
    7            doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
    8
    9            mFrameInfo.markAnimationsStart();
    10            doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
    11
    12            mFrameInfo.markPerformTraversalsStart();
    13            doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
    14
    15            doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
    16        } finally {
    17            AnimationUtils.unlockAnimationClock();
    18            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    19        }
    20              .....
    21    }

    那么这样一来,就找到关键的方法了:doFrame(),这个方法里会根据一个时间戳去队列里取任务出来执行,而这个任务就是 ViewRootImpl
    封装起来的 doTraversal() 操作,而 doTraversal() 会去调用 performTraversals()
    开始根据需要测量、布局、绘制整颗 View 树。所以剩下的问题就是 doFrame() 这个方法在哪里被调用了。

     1private final class FrameDisplayEventReceiver extends DisplayEventReceiver 2            implements Runnable {
    3        private boolean mHavePendingVsync;
    4        private long mTimestampNanos;
    5        private int mFrame;
    6
    7        public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
    8            super(looper, vsyncSource);
    9        }
    10
    11        @Override
    12        public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {     
    13                scheduleVsync();
    14                return;
    15            }
    16            mTimestampNanos = timestampNanos;
    17            mFrame = frame;
    18            Message msg = Message.obtain(mHandler, this);
    19            msg.setAsynchronous(true);
    20            mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
    21        }
    22        @Override
    23        public void run() {
    24            mHavePendingVsync = false;
    25            doFrame(mTimestampNanos, mFrame);
    26        }
    27    }

    可以看到,是在onVsync回调中,Message msg = Message.obtain(mHandler,
    this),传入了this,然后会执行到run方法里面,自然也就执行了doFrame方法,所以最终问题也就来了,这个onVsync()这个是什么时候回调来的,这里查询了网上的一些资料,是这么解释的

    >
    FrameDisplayEventReceiver继承自DisplayEventReceiver接收底层的VSync信号开始处理UI过程。VSync信号由SurfaceFlinger实现并定时发送。FrameDisplayEventReceiver收到信号后,调用onVsync方法组织消息发送到主线程处理。这个消息主要内容就是run方法里面的doFrame了,这里mTimestampNanos是信号到来的时间参数。

    那也就是说,onVsync是底层回调回来的,那也就是每16.6ms,底层会发出一个屏幕刷新的信号,然后会回调到onVsync方法之中,但是有一点很奇怪,底层怎么知道上层是哪个app需要这个信号来刷新呢,结合日常开发,要实现这种一般使用观察者模式,将app本身注册下去,但是好像也没有看到哪里有往底层注册的方法,对了,再次回到上面的那个native方法,nativeScheduleVsync(mReceiverPtr),那么这个方法大体的作用也就是注册监听了,

    同步屏障

    总结下上面的知识,我们调用了 invalidate(),requestLayout(),等之类刷新界面的操作时,并不是马上就会执行这些刷新的操作,而是通过
    ViewRootImpl 的 scheduleTraversals() 先向底层注册监听下一个屏幕刷新信号事件,然后等下一个屏幕刷新信号来的时候,才会去通过
    performTraversals() 遍历绘制 View 树来执行这些刷新操作。

    那么这样是不是产生一个问题,因为我们知道,平常Handler发送的消息都是同步消息的,也就是Looper会从MessageQueue中不断去取Message对象,一个Message处理完了之后,再去取下一个Message,那么绘制的这个Message如何尽量保证能够在16.6ms之内执行到呢,

    这里就使用到了一个同步屏障的东西,再次回到scheduleTraversals代码

     1void scheduleTraversals({
    2        if (!mTraversalScheduled) {
    3            mTraversalScheduled = true;
    4            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
    5            mChoreographer.postCallback(
    6                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
    7            if (!mUnbufferedInputDispatch) {
    8                scheduleConsumeBatchedInput();
    9            }
    10            notifyRendererOfFramePending();
    11            pokeDrawLockIfNeeded();
    12        }
    13    }

    mHandler.getLooper().getQueue().postSyncBarrier(),这一句上面没有分析,进入到方法里

     1private int postSyncBarrier(long when) {
    2        synchronized (this) {
    3            final int token = mNextBarrierToken++;
    4            final Message msg = Message.obtain();
    5            msg.markInUse();
    6            msg.when = when;
    7            msg.arg1 = token;
    8            Message prev = null;
    9            Message p = mMessages;
    10            if (when != 0) {
    11                while (p != null && p.when <= when) {
    12                    prev = p;
    13                    p = p.next;
    14                }
    15            }
    16            if (prev != null) { // invariant: p == prev.next
    17                msg.next = p;
    18                prev.next = msg;
    19            } else {
    20                msg.next = p;
    21                mMessages = msg;
    22            }
    23            return token;
    24        }
    25    }

    可以看到,就是生成Message对象,然后进一些赋值,但是细心的朋友可能会发现,这个msg为什么没有设置target,熟悉Handler流程的朋友应该清楚,最终消息是通过msg.target发送出去的,一般是指Handler对象

    那我们再次回到MessageQueue的next方法中看看

     1Message next() {
    2        for (;;) {
    3              ....
    4            synchronized (this) {
    5                   ...
    6                //对,就是这里了,target==null
    7                if (msg != null && msg.target == null) {                 
    8                    do {
    9                        prevMsg = msg;
    10                        msg = msg.next;
    11                    } while (msg != null && !msg.isAsynchronous());
    12                }
    13                if (msg != null) {
    14                    if (now when) {                     
    15                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
    16                    } else {
    17                        // Got a message.
    18                        mBlocked = false;
    19                        if (prevMsg != null) {
    20                            prevMsg.next = msg.next;
    21                        } else {
    22                            mMessages = msg.next;
    23                        }
    24                        msg.next = null;
    25                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
    26                        msg.markInUse();
    27                        return msg;
    28                    }
    29                } else {                
    30                    nextPollTimeoutMillis = -1;
    31                }
    32        }
    33    }

    可以看到有一个Message.target==null的判断, do
    while循环遍历消息链表,跳出循环时,msg指向离表头最近的一个消息,此时返回msg对象

    可以看到,当设置了同步屏障之后,next函数将会忽略所有的同步消息,返回异步消息。换句话说就是,设置了同步屏障之后,Handler只会处理异步消息。再换句话说,同步屏障为Handler消息机制增加了一种简单的优先级机制,异步消息的优先级要高于同步消息

    这样的话,能够尽快的将绘制的Message给取出来执行,嗯,这里为什么说是尽快呢,因为,同步屏障是在 scheduleTraversals()
    被调用时才发送到消息队列里的,也就是说,只有当某个 View 发起了刷新请求时,在这个时刻后面的同步消息才会被拦截掉。如果在scheduleTraversals() 之前就发送到消息队列里的工作仍然会按顺序依次被取出来执行

    总结

    • View的刷新请求都会走到ViewRootImpl的scheduleTraversals() 中,通过Runnable的形式形成Message放进队列中,并且发送同步屏障,拦截所以同步消息,处理异步消息,以此尽快的保证执行刷新任务

    • 常说的每隔16.6ms刷新一次屏幕实际上是,底层会以这个频率来切换每一帧的画面,只有当View发起了刷新请求时,App才会想底层去注册监听下一个屏幕的刷新信号,并且才能受到下一次信号到来的通知回调onVsync

    • App负责计算屏幕刷新的数据,但是并非计算完成后就会立即刷新数据,更多的取决于是否到了下一次底层要刷新屏幕的指令回调的时机,所以也就回答了上面的问题,每次指令到达时才会去刷新数据,尽可能的保证刷新数据的任务有足够16.6ms的时间

    • 造成屏幕丢帧的原因也就很明显了,1.View树绘制的任务时长大于了16.6ms,此时下一个信号来临,导致丢帧 2.虽然采用了同步屏障的方法来保证足够View绘制任务的时间,但是如果同步屏障之间的Message耗时过长,也导致遍历绘制 View 树的工作迟迟不能开始,从而超过了 16.6 ms 底层切换下一帧画面的时机,这也就是主线程不要做耗时操作的原因了

    参考资料

    https://www.jianshu.com/p/a769a6028e51

    https://blog.csdn.net/asdgbc/article/details/79148180

    推荐阅读

    一步步带你读懂 Okhttp 源码Android 点九图机制讲解及在聊天气泡中的应用职场上这四件事,越早知道越好自定义View之双层波纹气泡(xFermode)Android-自定义气泡View,让我们告别.9图面试官:今日头条启动很快,你觉得可能是做了哪些优化?

    面试官:你有m个鸡蛋,如何用最少的次数测出鸡蛋会在哪一层碎?

    花式实现时间轴,样式由你来定!

    面试官:Android 子线程更新UI了解吗?

    Android 优雅地处理后台返回的骚数据

    Android自动化脚本多渠道加固、打包

    368bc05ac1bc87bbd7ff1492d9223569.png

    扫一扫,欢迎关注我的微信公众号 stormjun94(徐公码字)

    展开全文
  • 刷新率越高无疑能给用户带来更流畅的体验,但所谓的刷新率原理是什么?它是不是越高越好以及高刷新率会带来哪些问题?这些都要求我们对刷新率有一个全面的了解。首先,当我们使用智能手机的时候,其显示屏一直处在...

    自去年开始,屏幕高刷新率就成为手机厂商宣传的卖点,而引领这个趋势的一加也确实吃到不少甜头。于是,我们可以看到今年的大多数旗舰手机以及中端机都用上了90Hz甚至120Hz的高刷新率。刷新率越高无疑能给用户带来更流畅的体验,但所谓的刷新率原理是什么?它是不是越高越好以及高刷新率会带来哪些问题?这些都要求我们对刷新率有一个全面的了解。

    5c7d36d80674698016d363e20e434a53.png

    首先,当我们使用智能手机的时候,其显示屏一直处在工作的状态,每当有新的画面要呈现时,显示屏上的每个像素都必须更新,更新方式是从上到下,整排像素同时刷新。当所有的像素从上到下更新完时,意味着显示屏已经刷新了一次,因此,显示屏的刷新率指的就是显示屏刷新或更新的频率。

    大多数电视、PC和智能手机显示屏的典型刷新率是60Hz,它表示显示器每秒刷新60次,换句话说,显示器上的图像每16.67毫秒更新一次。一个画面或图像占据显示屏的这段时间称为刷新时间,而刷新时间与刷新率总是成反比。

    同理,90Hz刷新率每秒刷新90次,120Hz刷新率每秒刷新120次。因此,90Hz和120Hz显示屏的刷新时间较短,分别为11.11毫秒和8.33毫秒。但同时,具有较高刷新率的智能手机必须承受每秒推送更多像素带来的额外负担。

    440354a7efb50ac1bfb0988a21a47e91.gif

    那么,60Hz到90Hz、120Hz以及144Hz它们的差别主要体现在什么地方?答案是动画中,虽然我们无法看清单一的刷新帧,但可以感受到智能手机显示屏上的连续帧数。在播放同样的动画时,90Hz刷新率的显示屏与60Hz刷新率的显示屏相比,帧数要多出1.5倍。而这些额外的帧数,让整个动画的运动会更加流畅。

    b3d2d5e8707b5275957577392e0a46ad.png

    尽管高刷新率对UI流畅性有很多明显的好处,但更高的刷新率有一个明显的缺点,那就是会增加功耗。与60Hz相比,当显示刷新率设置为90Hz时,手机会消耗更多的电量,因为每个动画要渲染更多的帧数,需要做额外的工作。而120Hz、144Hz刷新率自然将带来更快的电池消耗。

    考虑到高刷新率带来的功耗问题,许多手机厂商在其定制的安卓系统中提供了一个“自动刷新率”的模式。通常情况下,“自动模式”会根据应用、亮度、电量或其他因素,在设定值之间改变显示器的刷新率。例如,在支持90Hz刷新率的屏幕上,系统会自动在60Hz和90Hz之间进行切换。这种自动切换的方式可以在保证用户有良好体验的同时,确保电池不会消耗的太快。

    f635e1ef5c753016dbf6c3462b44bd26.png

    在一加7Pro推出后,智能手机行业出现了对更高刷新率显示屏的需求热潮,努比亚红魔3、Realme X2 Pro和OPPO Reno 3 Pro等都争先恐后的用上了90Hz刷新率的屏幕,而华硕ROG Phone 2更进一步,将刷新率提升到了120Hz。

    而到2020年,国内的众多手机厂商比如小米、华为等都在旗舰机型上开始标配90Hz刷新率。同时,一加和OPPO更是在今年的旗舰产品一加8Pro和OPPO Find x2 Pro上用上了120Hz刷新率屏幕。作为最大高刷新率屏幕面板供应商的三星也是在今年凭借Galaxy S20进入高刷俱乐部。而华硕今年又是抢先一步,推出了配备144Hz刷新率并可超频至160Hz的华硕ROG Phone 3,这是迄今为止我们在商用智能手机上能看到的最高刷新率。

    e21b9af6876a965bcf08d2d4d101f54d.png

    屏幕的高刷新率已经成为许多智能手机厂商在宣传手机时的重要卖点。虽然高于60Hz的刷新率被认为可以获得更流畅的使用体验,但同时它也越来越多地被视为是更高显示质量的指标。然而,90Hz、120Hz或更高的刷新率并不一定意味着屏幕的显示质量很高。屏幕的质量主要是取决于显示器背后的技术、校准以及软硬件层面的优化。所以,单靠刷新率一个参数并不能直接代表屏幕的质量,而我们也应该根据自己的实际需求进行选择。

    展开全文
  • 一、为什么使用双缓冲图形刷新技术 双缓冲图形刷新技术能解决绘图时屏幕闪烁...很长一段时间我一直认为绘图过程中出现的屏幕闪烁图形刷新速度过快而造成的(相信有很多朋友也跟我一样有这样的想法),但是通过编
  • 之前在整理知识的时候,看到android屏幕刷新机制这一块,以前一直只是知道,Android每16.6ms会去刷新一次屏幕,也就是我们常说的60fpx,那么问题也来了: 16.6ms刷新一次是什么一次,是以这个固定的频率去重新绘制吗...
  • 二、绘图时屏幕闪烁的原因分析很长一段时间我一直认为绘图过程中出现的屏幕闪烁图形刷新速度过快而造成的(相信有很多朋友也跟我一样有这样的想法),但是通过编写一些绘图程序,我发现事情并非如此,至少刷新速度...
  • 事先声明,此问题已经困扰了我近半年,我一直没搞明白jquery的弹出窗口在ie6里面为什么会错位,关闭按钮跑到了屏幕最上方,图片跑到了屏幕最下方。我深信jquery的程序不会错的,他们一直声明能兼容任何浏览器,...
  • 前言首先回顾一下上期的问题:如上图所示的 Systrace 中,VSYNC-app 基本上没有什么变化,但是 VSYNC-sf 却一直在更新有可能是什么原因?要解释这个现象我们得先理解一下 VSYNC-app 的作用是什么?VSYNC-app 的作用...
  • 很长一段时间我一直认为绘图过程中出现的屏幕闪烁图形刷新速度过快而造成的(相信有很多朋友也跟我一样有这样的想法),但是通过编写一些绘图程序,我发现事情并非如此,至少刷新速度快不会造成屏幕闪烁的根本...
  • MFC 双缓冲技术

    千次阅读 2012-04-18 19:19:59
    很长一段时间我一直认为绘图过程中出现的屏幕闪烁图形刷新速度过快而造成的(相信有很多朋友也跟我一样有这样的想法),但是通过编写一些绘图程序,我发现事情并非如此,至少刷新速度快不会造成屏幕闪烁的根本...
  • <p><strong>你遇到了什么样的问题 / The problem you've met: 操作一段时间之后就卡死,有时黑屏,有时正常屏幕刷新后好用一段时间又会卡死(左上角的圆圈还是会转的,但是点击...
  • 使用豆瓣为了获取信息,但信息的获取基于条目和算法,还是基于友邻和人,这个问题在豆瓣的多次改版中大概一直悬而未决。 这次,一个叫“豆瓣”的应用选择的基于条目的推荐。但我个人作为一个重度豆瓣用户,...
  • 这个60HZ是什么意思?就是指屏幕每秒钟刷新60次。所以我们可以通过屏幕作为参考,如果我们的网页也可以每秒钟往屏幕传输60个画面,用户就会觉得这个网页是流畅的,有一个单位叫做FPS,...
  • 手机 pdf 阅读器

    2009-02-12 23:00:29
    对于大家意见比较大的耗电量的问题,改进阅读时的算法及绘图方式,运算量最高可降至上一版本的2%,平均值为上一版本的6%左右,但由于阅读时耗电量的大头仍然是屏幕发光引起的,因此,只能在一定程序上减少耗电量。...
  • 目录介绍 01.项目介绍 02.项目运行 03.项目部分介绍 ...有的建议Clean然后Rebuild,有的建议修改使用内存,有的说是代码问题,也有的说是资源问题,比如本来jpg图片或者.9图片,文件后缀却png也会导致...
  • 1、至 1.6 版本为止一直存在原始尺寸处理的问题,在新的 1.7 版本里已经修正了这个问题。 2、同样的软件问题还包括类似另存为无法保存的问题,虽然之前版本版本提供了一个另存为按钮,但它实际上没有作用。在新的 ...
  • 俄罗斯方块是一直各类程序语言热衷实现的经典游戏,JavaScript的实现版本也有很多,用React 做好俄罗斯方块则成了我一个目标。 戳:https://chvin.github.io/react-tetris/ 玩一玩! 效果预览 正常速度的录制,...
  • 一天下午,在开发一个邮件和文本信息论坛时,我有了一个不可思议、令人兴奋的想法:在隐藏帧(hidden frame)中检查新的消息,无需刷新屏幕就可以将消息添加到用户界面中。经过几个小时的狂热编码,我成功了,甚至...
  • 认真听课、多思考问题、多动手操作、有问题一定要问、多参与讨论、多帮组同学 五、 体系结构 oracle的体系很庞大,要学习它,首先要了解oracle的框架。oracle的框架主要由物理结构、逻辑结构、内存分配、后台进程...
  • 商城默认中的用户积分和预存款兑换比率一直是一比二,很多用户不知如何修改,本次更新中增加了用户积分和预存款兑换比率设置,在后台可以方便的设置兑换比率,方便用户进行修改设置。 二三、帮助中心栏目无限量...
  • 商城默认中的用户积分和预存款兑换比率一直是一比二,很多用户不知如何修改,本次更新中增加了用户积分和预存款兑换比率设置,在后台可以方便的设置兑换比率,方便用户进行修改设置。 二六、增加缩略图弹出显示...
  • 商城默认中的用户积分和预存款兑换比率一直是一比二,很多用户不知如何修改,本次更新中增加了用户积分和预存款兑换比率设置,在后台可以方便的设置兑换比率,方便用户进行修改设置。 二五、帮助中心栏目无限量...
  • 商城默认中的用户积分和预存款兑换比率一直是一比二,很多用户不知如何修改,本次更新中增加了用户积分和预存款兑换比率设置,在后台可以方便的设置兑换比率,方便用户进行修改设置。 二六、帮助中心栏目无限量...
  • 网趣商城ASP源码

    2013-02-17 17:11:35
    商城默认中的用户积分和预存款兑换比率一直是一比二,很多用户不知如何修改,本次更新中增加了用户积分和预存款兑换比率设置,在后台可以方便的设置兑换比率,方便用户进行修改设置。 二六、帮助中心栏目无限量...
  • 商城默认中的用户积分和预存款兑换比率一直是一比二,很多用户不知如何修改,本次更新中增加了用户积分和预存款兑换比率设置,在后台可以方便的设置兑换比率,方便用户进行修改设置。 二六、帮助中心栏目无限量...

空空如也

空空如也

1 2
收藏数 33
精华内容 13
关键字:

屏幕一直刷新是什么问题