当前位置: 首页 > news >正文

软件公司网站源码/巨量数据分析入口

软件公司网站源码,巨量数据分析入口,零基础网站建设视频教程,平面设计提升班一、背景 前段时间由于工作上的需求,需要实现一个帧动画,由于时间紧迫,于是就想参考一下网上开源的库,找了一圈,都没有找到特别合适的,有些甚至一大堆问题。于是参考大佬的,自己写了一个。 二…

一、背景

前段时间由于工作上的需求,需要实现一个帧动画,由于时间紧迫,于是就想参考一下网上开源的库,找了一圈,都没有找到特别合适的,有些甚至一大堆问题。于是参考大佬的,自己写了一个。

二、为什么不用Android原生帧动画?

时间紧迫?那为什么不用Android原生帧动画?有什么缺点吗?

Android 提供了AnimationDrawable用于实现帧动画。在动画开始之前,所有帧的图片都被解析到内存中,一旦动画较复杂帧数较多,在低配置手机上容易发生 OOM,即使不发生 OOM,也会对内存造成不小的压力。

这是一种以空间换时间的做法,虽然动画流畅无瑕疵,但占用的内存不容忽视。

AnimationDrawable drawable = new AnimationDrawable();
drawable.addFrame(getDrawable(R.drawable.frame1), frameDuration);
drawable.addFrame(getDrawable(R.drawable.frame2), frameDuration);
drawable.addFrame(getDrawable(R.drawable.frame3), frameDuration);
drawable.addFrame(getDrawable(R.drawable.frame4), frameDuration);
。。。
ImageView ivFrameAnim = ((ImageView) findViewById(R.id.frame_anim));
ivFrameAnim.setImageDrawable(drawable);
drawable.start();

所以,即使时间紧迫,也绝不会采用原生的帧动画来实现,只能另找办法了。

三、为什么用TextureView而不是SurfaceView?

为什么用TextureView而不是SurfaceView?有什么区别吗?

这个问题我就不详细解释,网上的解答一大堆,而且很详细,推荐这篇文章
Android 5.0(Lollipop)中的SurfaceTexture,TextureView, SurfaceView和GLSurfaceView
这里我简单写(摘抄)一下:

SurfaceView:

优点:

  • 可以在一个独立的线程中进行绘制,不会影响主线程;

  • 使用双缓冲机制,播放视频时画面更流畅;

缺点:

  • Surface不在View hierachy中,它的显示也不受View的属性控制,所以不能进行平移,缩放等变换,也不能放在其它ViewGroup中,一些View中的特性也无法使用;
  • SurfaceView不能嵌套使用;

TextureView

优点:

  • 支持移动、旋转、缩放等动画,支持截图;

缺点:

  • 必须在硬件加速的窗口中使用,占用内存比SurfaceView高,在5.0以前在主线程渲染,5.0以后有单独的渲染线程;

所以,这里为什么用TextureView而不是SurfaceView呢?很简单,两个原因:

  1. 因为我们的业务场景很多都是需要把动画嵌套到ViewGroup中,甚至需要做简单的移动、旋转、透明度等动画,所以SurfaceView是无法满足的;
  2. 现在绝大多数的APP都会把硬件加速打开,所以TextureView的这个条件也可以满足;

四、TextureView逐帧动画?

什么是逐帧动画?

逐帧动画就跟播放视频一样,把连续多帧图片,在一定的时间内,连续的展示,就形成了一个连续的播放效果,这就是逐帧动画。

用TextureView能实现逐帧动画吗?怎么实现呢?

我们可以使用TextureView在一定的间隔时间内绘制不同帧图片,就能达到动画的效果。需要考虑到以下几个问题:

  1. 如何实现逐帧绘制?
  2. 逐帧绘制的图片如何准备?
  3. 如何复用每一帧或者说如何去节省内存占用?

针对上面的问题,捋一下,看需要些什么准备:

  • 两个线程:一个绘制线程;一个图片解码线程
  • 两个图片队列:一个等待绘制的图片队列;一个已绘制过等待取出来复用的图片队列

两个线程,很好理解,一个绘制,一个解码,但为什么需要两个队列呢?

原因很简单,我举个例子,假设我的绘制间隔时间是80ms,解码一张图片需要100ms,那么就会出现绘制时间到了,但却图片还没准备好的情况,这就会出现“丢帧”的现象。那怎么办?

起两个队列,每个队列最大容量为3(可以自主设置),动画开始前,我们先预解码2或3个图片到绘制队列:
队列
解码线程预解码好图片后,调用复用队列的take方法(如果队列为空,会阻塞)继续解码图片。动画开始后,绘制线程开始按照一定的时间间隔不断的从绘制队列取图片绘制,绘制完一帧后,把图片put到复用队列,并通知解码线程(如果已经阻塞中了),就这样不断的循环。

我们来捋一下实现思路:

  1. 先开启绘制线程和图片解码线程;
  2. 解码线程不断从复用队列获取Bitmap,复用并解码下一帧图片;
  3. 解码线程把已经准备好的图片放进待绘制图片队列;
  4. 绘制线程按照一定的时间间隔从待绘制队列取出图片,进行绘制;
  5. 绘制线程绘制完毕后,把图片Bitmap放进待复用队列;

思路就是这么简单,文字表达可能不清晰,画个图吧:
流程图

五、FrameTextureView

继承TextureView,创建我们的FrameTextureView。按照上面的思路,我们一步一步来实现。

创建绘制线程和解码线程

采用HandlerThread来作为线程,通过handler与其绑定,能很方便的实现按照一定的帧率来进行刷新

public class FrameTextureView extends TextureView {/*** 绘制线程*/private HandlerThread mDrawHandlerThread;/*** bitmap解码线程*/private HandlerThread mDecodeHandlerThread;/*** 解码handler*/private Handler mDecodeHandler;/*** 绘制handler*/private Handler mDrawHandler;...public FrameTextureView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);init(context, attrs);}protected void init(Context context, AttributeSet attrs) {setOpaque(false);setSurfaceTextureListener(new SurfaceTextureListener() {@Overridepublic void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {startDrawThread();}@Overridepublic boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {destroy();return true;}...});}/*** 开启解码线程*/private void startDecodeThread(Runnable runnable) {if (mDecodeHandlerThread == null) {mDecodeHandlerThread = new HandlerThread(DECODE_THREAD_NAME);}if (!mDecodeHandlerThread.isAlive()) {mDecodeHandlerThread.start();}if (mDecodeHandler == null) {mDecodeHandler = new Handler(mDecodeHandlerThread.getLooper());}mDecodeHandler.removeCallbacksAndMessages(null);mDecodeHandler.post(runnable);}/*** 开启绘制线程*/private void startDrawThread() {if (mDrawHandlerThread == null) {mDrawHandlerThread = new HandlerThread(DRAW_THREAD_NAME);}if (!mDrawHandlerThread.isAlive()) {mDrawHandlerThread.start();}if (mDrawHandler == null) {mDrawHandler = new Handler(mDrawHandlerThread.getLooper());}mDrawHandler.removeCallbacksAndMessages(null);mDrawHandler.post(new DrawRunnable());}

解码线程不断从复用队列获取Bitmap,复用并解码下一帧图片

先从复用队列获取可复用的Bitmap,使用的是take方法,该方法是有阻塞的可能,为什么?

这是为了避免快速解码导致内存溢出:假设复用队列为空,但绘制线程还未完成当前帧的绘制,此时解码线程完成了一帧的解码,并正在向复用队列取帧,若不采取阻塞方法,则解码线程复用帧失败,一块新的内存被申请用于存放解析出来的下一帧。

	/*** 从【已绘制的bitmap队列】里取废弃的已绘制的bitmap*/private LinkedBitmap getDrawnBitmap() {if (isDestroy()) {return null;}LinkedBitmap bitmap = null;try {bitmap = mDrawnBitmapQueue.take();} catch (Exception e) {e.printStackTrace();}return bitmap;}
	private class DecodeRunnable implements Runnable {@Overridepublic void run() {...LinkedBitmap linkedBitmap = getDrawnBitmap();...// 通过inBitmap复用BitmapmDecodeOptions.inBitmap = linkedBitmap.bitmap;// 根据下一帧的idframeItem,解码图片Bitmap bitmap = decodeBitmap(frameItem);...if (bitmap == null) {Log.e(TAG, "DecodeRunnable, bitmap is null.");} else {linkedBitmap.bitmap = bitmap;// 解码好一帧图片后,放进绘制队列putDecodedBitmap(linkedBitmap);}if (mDecodeHandler != null) {if (isDestroy()) {return;}// 继续解码下一帧(注意,不需要延时)mDecodeHandler.post(this);}}}

解码线程把已经准备好的图片放进待绘制图片队列

下一帧图片解码好后,调用put方法,该方法也会有阻塞的可能,为什么呢?为了避免快速解码时,绘制队列满了,解码线程还在继续不必要的工作。

	/*** 存储已经解码的bitmap到【已解码的bitmap队列】** @param bitmap 已解码bitmap(可能阻塞)*/private void putDecodedBitmap(LinkedBitmap bitmap) {...try {mDecodedBitmapQueue.put(bitmap);} catch (Exception ex) {ex.printStackTrace();Log.e(TAG, "putDecodedBitmap, ex=" + ex);}}

绘制线程按照一定的时间间隔从待绘制队列取出图片,进行绘制

从绘制队列了取出下一帧图片,调用的take方法,该方法会存在阻塞的可能,因为有可能图片还没解码准备好(这种情况虽然比较少,但要预防)。

	/*** 从【解码bitmap队列】里取bitmap* (可能会阻塞)*/private LinkedBitmap getDecodedBitmap() {...LinkedBitmap bitmap = null;try {bitmap = mDecodedBitmapQueue.take();} catch (Exception e) {e.printStackTrace();}return bitmap;}

开始绘制

	/*** 绘制一帧*/private void drawOneFrame() {...// 取出下一帧图片LinkedBitmap linkedBitmap = getDecodedBitmap();if (linkedBitmap != null && linkedBitmap.bitmap != null) {...Bitmap bitmap = linkedBitmap.bitmap;try {canvas = lockCanvas();...// 绘制canvas.drawBitmap(bitmap, mDrawMatrix, null);...} catch (Exception ex) {ex.printStackTrace();}...// 绘制完,把bitmap放进复用队列putDrawnBitmap(linkedBitmap);}}

绘制线程绘制完毕后,把图片Bitmap放进待复用队列

绘制过的bitmap已经算废弃了,需要放进复用队列,等待解码线程的复用,这里调用的是offer方法,这里并不会出现阻塞,这是为什么?

这是为了避免慢速解析拖累快速绘制:假设复用队列已满,但解析线程还未完成当前解析,此时完成了一帧的绘制,并正在向复用队列存帧,若采用阻塞方法,则绘制线程因慢速解析而被阻塞。

	/*** 存储已绘制的bitmap到【已绘制的Bitmap队列】** @param bitmap 已绘制的bitmap(不阻塞)*/private void putDrawnBitmap(LinkedBitmap bitmap) {if (isDestroy()) {return;}try {mDrawnBitmapQueue.offer(bitmap);} catch (Exception ex) {ex.printStackTrace();Log.e(TAG, "putDrawnBitmap, ex=" + ex);}}

到这里,基本流程就跑通了,但是离完善还有很重要的一步:结束动画和回收资源

六、内存泄漏?崩溃?

在完成了上面的基本流程后,理所当然的,我需要添加结束动画和回收资源的destroy方法。

	/*** 彻底销毁并释放资源*/public void destroy() {...destroyHandler();destroyBitmapQueue();destroyThread();...}private void destroyHandler() {if (mDecodeHandler != null) {mDecodeHandler.removeCallbacksAndMessages(null);mDecodeHandler = null;}if (mDrawHandler != null) {mDrawHandler.removeCallbacksAndMessages(null);mDrawHandler = null;}}private void destroyBitmapQueue() {try {mDecodedBitmapQueue.destroy();mDrawnBitmapQueue.destroy();} catch (Exception ex) {ex.printStackTrace();}}private void destroyThread() {try {if (mDecodeHandlerThread != null) {mDecodeHandlerThread.quit();mDecodeHandlerThread = null;}} catch (Exception ex) {ex.printStackTrace();}try {if (mDrawHandlerThread != null) {mDrawHandlerThread.quit();mDrawHandlerThread = null;}} catch (Exception ex) {ex.printStackTrace();}}

销毁handler、释放队列资源、销毁绘制线程和解码线程,代码上看起来没啥问题,然而压力测试后,内存泄漏和崩溃的问题让我头疼了很久。

明明已经释放了资源,销毁了线程,为什么还会出现内存泄漏?崩溃?难道是逻辑上隐藏了什么问题?

是的,没错,里面确实隐藏了两个很严重的问题。

内存泄漏
首先,为什么会出现内存泄漏?哪个地方导致泄漏了?

通过LeakCanary和代码排查,我发现是绘制线程mDrawHandlerThread和解码线程mDecodeHandlerThread泄漏了。这两个线程并没有释放掉,我明明已经quit了的,为什么没有停止呢?

我们看一下HandlerThreadquit方法:

	# HandlerThread类public boolean quit() {Looper looper = getLooper();if (looper != null) {looper.quit();return true;}return false;}# Looper类public void quit() {mQueue.quit(false);}# MessageQueue类void quit(boolean safe) {...synchronized (this) {if (mQuitting) {return;}mQuitting = true;if (safe) {removeAllFutureMessagesLocked();} else {removeAllMessagesLocked();}...}}

HandlerThread的quit方法里面只是调用了Looper的quit方法,而Looper的quit方法里面也只是调了MessageQueue的quit方法,最后我们看MessageQueue的quit里面也只是设置了一个mQuitting标志位而已。设置这个标志位后,MessageQueue的next方法里会终止循环:

	@UnsupportedAppUsageMessage next() {...// Process the quit message now that all pending messages have been handled.if (mQuitting) {dispose();return null;}...      }

最终,会发现,调用了HandlerThread的quit方法,也只是结束获取下一个执行的消息而已。HandlerThread真正的退出,应该是清空消息队列,并且,当前消息执行完毕。

看到这里,也许就已经恍然大悟了,内存泄漏的原因是HandlerThread的最后一个消息一直没有执行完,导致一直没法真正的释放。

问题又来了,为什么最后一个消息一直没有执行完呢?

还记得前面提到的队列的take和put方法吗,这两个都会有可能阻塞的,如果在退出页面释放资源时,正好绘制线程或解码线程中的某个线程的队列处于阻塞状态,就会导致内存泄漏。

由于原生的LinkedBlockingQueue无法满足,所以这里参照LinkedBlockingQueue自定义了一个队列CustomLinkedBlockingQueue,在destroy时添加标记位,正确处理阻塞状态并释放资源:

public class CustomLinkedBlockingQueue {...private final AtomicBoolean destroy = new AtomicBoolean(false);...void signalAll() {notEmpty.signalAll();notFull.signalAll();}/*** recycle the bitmaps one by one*/public void destroy() {destroy.set(true);clear();}@SuppressLint("LongLogTag")private void clear() {fullyLock();try {signalAll();LinkedBitmap p = head;if (p == null) {p = tail;}if (p == null) {return;}while (p != null) {if (p.bitmap != null && !p.bitmap.isRecycled()) {p.bitmap.recycle();}p.bitmap = null;p = p.next;}head = tail = null;count.set(0);} catch (Exception ex) {ex.printStackTrace();} finally {fullyUnlock();Log.i(TAG, "clean all bitmap, finished.");}}
}

崩溃
在处理完内存泄漏问题后,进行压力测试时,高概率出现了这样的崩溃:

Process: com.xxx.xx.xxx
PID: 3518
Flags: 0x3898be45
Package: com.xxx.xx.xx v400 
Foreground: Yes
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 
...
Revision: '0'
ABI: 'arm64'pid: 3518, tid: 2224, name: DrawingThread  >>> com.xxx.xx.xxx <<<
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0xf23960a77f60x0   0000f23960a77f40  x1   0000f2394112fc60  x2   0000000000000f10  x3   0000f23960a77f40x4   0000f23941130bc0  x5   0000f23960a78ee0  x6   0000000000000000  x7   0000000000000000x8   0000000000000000  x9   0000000000000000  x10  0000000000000000  x11  0000000000000000x12  0000000000000000  x13  0000000000000000  x14  0000f2397d114540  x15  0000000000000000x16  0000f2397d2289d0  x17  0000f2397a2f7330  x18  0000000000000008  x19  0000f23960a77f40x20  0000f2394112fc20  x21  0000000000000fa0  x22  0000000000000fa0  x23  0000000000000fc0x24  0000000000000086  x25  0000f239449ff588  x26  0000f239449fe5d8  x27  0000f239449ff588x28  0000000070bba928  x29  0000f239449fd0d0  x30  0000f2397cc0cbbcsp   0000f239449fd0a0  pc   0000f2397a2f7448  pstate 0000000020000000
backtrace:#00 pc 000000000001c448  /system/lib64/libc.so (memcpy+280)#01 pc 000000000020bbb8  /system/lib64/libskia.so (SkSpriteBlitter_Memcpy::blitRect(int, int, int, int)+196)#02 pc 0000000000305028  /system/lib64/libskia.so (SkScan::FillIRect(SkIRect const&, SkRegion const*, SkBlitter*)+236)#03 pc 00000000003051fc  /system/lib64/libskia.so (SkScan::FillIRect(SkIRect const&, SkRasterClip const&, SkBlitter*)+88)#04 pc 0000000000277e44  /system/lib64/libskia.so (SkDraw::drawBitmap(SkBitmap const&, SkMatrix const&, SkRect const*, SkPaint const&) const+644)#05 pc 00000000001fda28  /system/lib64/libskia.so (SkBitmapDevice::drawBitmapRect(SkBitmap const&, SkRect const*, SkRect const&, SkPaint const&, SkCanvas::SrcRectConstraint)+776)#06 pc 00000000002728c8  /system/lib64/libskia.so (SkBaseDevice::drawImageRect(SkImage const*, SkRect const*, SkRect const&, SkPaint const&, SkCanvas::SrcRectConstraint)+136)#07 pc 0000000000216c64  /system/lib64/libskia.so (SkCanvas::onDrawImageRect(SkImage const*, SkRect const*, SkRect const&, SkPaint const*, SkCanvas::SrcRectConstraint)+832)#08 pc 0000000000262f7c  /system/lib64/libskia.so (SkColorSpaceXformCanvas::onDrawImageRect(SkImage const*, SkRect const*, SkRect const&, SkPaint const*, SkCanvas::SrcRectConstraint)+212)#09 pc 00000000000b2728  /system/lib64/libhwui.so (android::SkiaCanvas::drawBitmap(android::Bitmap&, float, float, float, float, float, float, float, float, SkPaint const*)+92)#10 pc 0000000000125388  /system/lib64/libandroid_runtime.so (android::CanvasJNI::drawBitmapRect(_JNIEnv*, _jobject*, long, _jobject*, float, float, float, float, float, float, float, float, long, int, int)+284)#11 pc 0000000000b1b96c  /system/framework/arm64/boot-framework.oat (offset 0x61c000) (android.graphics.BaseCanvas.nDrawBitmap+268)#12 pc 0000000000b1db4c  /system/framework/arm64/boot-framework.oat (offset 0x61c000) (android.graphics.BaseCanvas.drawBitmap+524)#13 pc 000000000010e744  /dev/ashmem/dalvik-jit-code-cache (deleted)

额,是的,看log,确实是绘制线程崩溃了,而且也写得很清楚,是在绘制图片时Canvas的drawBitmap时崩了

那就尴尬了,为什么在绘制图片时会报这样的崩溃呢?而且还是高概率的偶现

根据整个控件的流程,以及日志,很容易就能定位到,这是由于canvas在绘制bitmap在过程中,退出界面,surface销毁了导致了该崩溃

OK,问题的原因找到了,那应该怎么解决呢?

首先,bitmap的绘制过程我们是无法打断的,但surface销毁也是必需的。 那就只能让它绘制完再销毁

怎么做?加锁阻塞呗!可以在surface销毁之前阻塞,等待bitmap绘制完。

查看源码,surface销毁的地方是在onSurfaceTextureDestroyed回调之后:

# TextureView类
private void releaseSurfaceTexture() {if (mSurface != null) {boolean shouldRelease = true;if (mListener != null) {// 回到onSurfaceTextureDestroyedshouldRelease = mListener.onSurfaceTextureDestroyed(mSurface);}synchronized (mNativeWindowLock) {nDestroyNativeWindow();}if (shouldRelease) {// 销毁surfacemSurface.release();}mSurface = null;mHadSurface = true;}}

我们可以在onSurfaceTextureDestroyed中阻塞等待bitmap绘制完,由于该方法是在主线程所以不能完全阻塞

经过测试,在绘制1M大小的图片时,canvas绘制图片也只需要20-30ms左右,那么我们可以在onSurfaceTextureDestroyed中尝试阻塞50ms:

			@Overridepublic boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {Log.e(TAG, "surface destroy.");mIsSurfaceAlive.set(false);// 尝试获取【绘制锁】,防止正在绘制中,surface销毁导致崩溃(超时50毫秒,防止阻塞主线程)try {mDrawingLock.tryLock(50, TimeUnit.MILLISECONDS);} catch (Exception ex) {ex.printStackTrace();} finally {try {mDrawingLock.unlock();} catch (Exception ex) {ex.printStackTrace();}}destroy();return true;}
	/*** 绘制一帧*/private void drawOneFrame() {...LinkedBitmap linkedBitmap = getDecodedBitmap();if (linkedBitmap != null && linkedBitmap.bitmap != null) {...Canvas canvas = null;try {canvas = lockCanvas();if (canvas != null) {try {// 获取【绘制锁】,防止绘制中,surface销毁了导致崩溃mDrawingLock.lockInterruptibly();if (mIsSurfaceAlive.get() && !bitmap.isRecycled()) {clearCanvas(canvas);canvas.drawBitmap(bitmap, mDrawMatrix, null);}} catch (Exception ex) {ex.printStackTrace();} finally {try {mDrawingLock.unlock();} catch (Exception ex) {ex.printStackTrace();}}}} catch (Exception ex) {ex.printStackTrace();} finally {try {if (canvas != null) {unlockCanvasAndPost(canvas);}} catch (Exception ex) {ex.printStackTrace();}}putDrawnBitmap(linkedBitmap);}...}

加了一个ReentrantLock后,该崩溃总算解决了。经过压力测试,也没有再出现崩溃了。

七、BlobCache

上面的问题解决完了后,该帧动画控件基本可以完美使用了。也许眼尖的人会发现,控件的流程主要分成两部分:绘制和解码。如果出现极端的情况,解码速度确实比较慢怎么办,或者说解码速度远远慢于帧动画间隔时间怎么办?

当然,大部分人可能会想到,把复用队列的容量设置大一点,但这并不是一个完美的方案,因为这会把内存搞大了,我们的初衷是节省内存的同时流畅运行动画。

那就只能把在解码速度方面下功夫了

图片解码还能再快一点吗?还有更好的方法吗?

有一点我们可能没有注意到,我们的动画一般都是循环播放的,而且上面的解码方法都是直接从本地解码的BitmapFactory#decodeResource

既是循环的,又是直接本地解码的,那使用BlobCache再适合不过了。

经过测试,BlobCache与DiskLruCache和BitmapFactory#decodeResource相比,虽然BlobCache在写速度上不是最优的,但读速度是最优的。具体对比情况可以查看我的另一篇文章:BlobCache与DiskLruCache的读写对比

结合BlobCache的使用,可以使图片的解码速度更快,在一定程度上可以让帧动画控件更加完美。

八、总结

帧动画控件使用的场景比较少,所以用的也不多。虽然网上的demo也很多,但大部分都没有维护了,而且年代比较久远,很多都比较坑。所以自己写了一个,总结了一下。

文章的最后,献上GitHub链接:https://github.com/hewuzhao/FrameAnimation

参考文章:

  1. https://juejin.cn/post/6844903841293402125#heading-4
  2. https://www.jianshu.com/p/e6d78fe4ef5f
  3. https://www.jianshu.com/p/225fe1feba60
http://www.jmfq.cn/news/4744441.html

相关文章:

  • 网站建设基础问题/怎样做网站推广
  • 潍坊网站建设联系方式/怎么做一个公司网站
  • 蚌山网站建设/河南网站排名优化
  • 深圳罗湖做网站公司哪家好/云seo关键词排名优化软件
  • 北京做网站公司有哪些/下载百度app最新版到桌面
  • 汽车网站建设代理加盟/长沙的seo网络公司
  • 网站推广品牌/北京优化网站方法
  • 个人网站公安局备案/网站搜索排优化怎么做
  • 网站的建设过程/seo线上培训多少钱
  • 有做火币网这种网站的吗/搜图片找原图
  • 那个网站能找到人/seo网站优化软件价格
  • 网站怎么被黑/网络营销手段
  • win xp 个人网站免费建设/网络营销有哪些推广方法
  • 电子商务网站设计规划书/服务器
  • 武汉网站建设索q.479185700/百度正版下载并安装
  • 成都网站建设网站建设哪家好/广州百度推广外包
  • 佛山制作网站公司推荐/最新网络营销方式有哪些
  • 网站两侧固定广告代码/宁德市人民政府
  • 石家庄制作网站的公司哪家好/百度知道下载安装
  • 咖啡网站设计模板/网站app免费生成软件
  • wordpress多媒体路径/百度搜索引擎优化
  • 做视频网站推广挣钱吗/网站怎么注册
  • 网站开发流程主要分成什么/爱站工具包官网
  • wordpress容易被黑吗/什么是网站seo
  • 电脑网站怎么创建到桌面上/百度seo教程视频
  • 最好看的免费观看视频/盐城seo排名
  • 中国能源建设集团有限公司怎么样/百度搜索优化建议
  • 北京哪里做网站/免费推广网站推荐
  • 个人建站如何赚钱/软文写作网站
  • 深圳网站建设公司小江/好口碑关键词优化地址