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 dump渲染和合成图层GraphicBuffer指南 -> 正文阅读

[移动开发]Android dump渲染和合成图层GraphicBuffer指南

??????Android dump渲染和合成图层GraphicBuffer指南



引言

??博客停更很久了,提起笔来渐感生疏啊!看来,还是得抽出时间来更新更新啊!好了,感慨也发完了,是时候切入正题了。本篇博客主要以本人在实际项目的开发中,为了定位Android显示异常究竟是GPU渲染,合成的问题,还是DRM端送显异常的问题而来。这里为了后续的复盘,也为可能有共同需求的朋友所以记录下来,最终发展成为如何dump Android渲染和合成图层GraphicBuffer,并通过YUV软件查看流程(主要是教大家如何把锅甩给队友)!通过本篇博客,读者将会至少学会如下两点:

  • 通过dump Android渲染图层GraphicBuffer,查看Android渲染结果是否正确
  • 通过dump Android合成图层GraphicBuffer,查看Android合成结果是否正确

    这里需要注意一点的是,这里的合成指的是GPU(Client)的合成方式!

好了不多说了,直接开干!

能搜寻到这篇博客的,肯定是对Android graphci有一定掌握的朋友,所以这里就不会过多解释一些名词和代码逻辑了。总之这是一篇专业性比较强的文章(因为一般的朋友也不会搜这个)!

注意:本篇的介绍是基于Android 11?平台为基础的(其中Q的版本差异也不是很大),其中涉及的代码路径如下:

frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp
frameworks/native/libs/renderengine/gl/GLESRenderEngine.cpp
frameworks/native/services/surfaceflinger/DisplayHardware/FramebufferSurface.cpp
frameworks/base/cmds/screencap
frameworks/av/cmds/screenrecord



一.通过Android内置命令dump Android合成图层GraphicBuffer

??在正式开始分析我们如何自行添加dump相关逻辑代码,进而dump渲染和合成图层GraphicBuffer之前,这里我简单介绍下如何使用Android内置的cmd命令,进行相关的dump逻辑,这里的dump逻辑仅仅只能dump GPU合成图层GraphicBuffer。

上述源码的逻辑,强烈建议读者参阅借鉴一下,这个很有必要,因为我的最终代码实现就是参考Android提供的这两个cmd命令而来!


1.1 Screencap dump一帧GPU合成图层GraphicBuffer

??Screencap的命令格式如下,它将当前当前Android显示的图层以GPU合成的模式,并通过png格式保存下来。该命令的使用方法如下:

130|XXX:/ # screencap  --help                                                                                                                       
screencap: invalid option -- -
usage: screencap [-hp] [-d display-id] [FILENAME]
   -h: this message
   -p: save the file as a png.
   -d: specify the physical display ID to capture (default: 0)
       see "dumpsys SurfaceFlinger --display-id" for valid display IDs.
If FILENAME ends with .png it will be saved as a png.
If FILENAME is not given, the results will be printed to stdout.

130|XXX:/ #screencap  -p /sdcard/screencap.png

关于screencap的具体实现逻辑就不过多介绍,感兴趣的可以frameworks/base/cmds/screencap查看相关的源码逻辑!


1.2 Screenrecord dump一帧或多帧GPU合成图层GraphicBuffer

??Screenrecord的命令格式如下,它将当前当前Android显示的图层以GPU合成的模式,并通过多种格式保存下来。该命令的使用方法如下:

1|XXX:/ # screenrecord --help                                                                                                                       
Usage: screenrecord [options] <filename>

Android screenrecord v1.3.  Records the device's display to a .mp4 file.

Options:
--size WIDTHxHEIGHT
    Set the video size, e.g. "1280x720".  Default is the device's main
    display resolution (if supported), 1280x720 if not.  For best results,
    use a size supported by the AVC encoder.
--bit-rate RATE
    Set the video bit rate, in bits per second.  Value may be specified as
    bits or megabits, e.g. '4000000' is equivalent to '4M'.  Default 20Mbps.
--bugreport
    Add additional information, such as a timestamp overlay, that is helpful
    in videos captured to illustrate bugs.
--time-limit TIME
    Set the maximum recording time, in seconds.  Default / maximum is 180.
--display-id ID
    specify the physical display ID to record. Default is the primary display.
    see "dumpsys SurfaceFlinger --display-id" for valid display IDs.
--verbose
    Display interesting information on stdout.
--help
    Show this message.

Recording continues until Ctrl-C is hit or the time limit is reached.

1|XXX:/ #screenrecord --verbose --time-limit 10 --output-format raw-frames /sdcard/raw-frames.frames
//说明:录制屏幕,录制时间为10s,格式为裸BGR(FORMAT_FRAMES),不添加任何信息
1|XXX:/ #screenrecord --verbose --time-limit 10 --output-format frames /sdcard/frames.frames
//说明:录制屏幕,录制时间为10s,格式为裸BGR(FORMAT_FRAMES),只是每帧前面会加上用于描述帧信息的20字节头。
1|XXX:/ #screenrecord --verbose --time-limit 30 --output-format h264 /sdcard/demo.h264
//说明:录制屏幕,录制时间为30s,格式为h264(FORMAT_H264)1|XXX:/ #screenrecord /sdcard/demo.mp4
//说明:录制屏幕,录制时间为默认的180s,格式为MP4(FORMAT_MP4)

关于screenrecord的具体实现逻辑就不过多介绍,感兴趣的可以frameworks/base/cmds/screencap查看相关的源码逻辑!




二.自定义逻辑dump Android渲染和合成图层GraphicBuffer指南

??通过前面的章节,我们简单介绍了如何使用Android内置的cmds命令dump GPU合成图层的GraphicBuffer,本章节我们重点介绍如何自定义dump Android渲染图层和GPU合成图层。


2.1dump Android渲染图层GraphicBuffer

??这块我们可以在GLESRenderEngine.cpp的如下方法中添加相关的逻辑,如下:

//frameworks/native/libs/renderengine/gl/GLESRenderEngine.cpp
static void dump_content_of_layers_to_file(const sp<GraphicBuffer>& target)
{
    ALOGE("dump_content_of_layers_to_file");
    // ALOGE("dump_content_of_layers_to_file");
    // ALOGE("dump_content_of_layers_to_file");
    int result = -1;
    void *addr = NULL;
    static int DumpSurfaceCount = 0;
	int32_t bufStride;
	FILE * pfile = NULL;
	char layername[100] ;
	memset(layername,0,sizeof(layername));

    uint32_t w, s, h, f;
    w = target->getWidth();
    h = target->getHeight();
    s = target->getStride();
    f = target->getPixelFormat();
    android_dataspace d;
    uint32_t buffer_size = 0;
    d = HAL_DATASPACE_UNKNOWN;
    buffer_size = s * h * bytesPerPixel(f);
    bufStride = bytesPerPixel(f);
    ALOGE("buffer_layer info w:%d h:%d s:%d f:%d d:%d size:%d", w, h, s, f, d, buffer_size);

	sprintf(layername,
		"/data/dump/buffer_layer_%d_frame_%d_%d_%d.bin",
		DumpSurfaceCount,
		w,
		h,
		bufStride);

	ALOGD("The dump  file info : %s", layername);
	DumpSurfaceCount ++;
	pfile = fopen(layername,"w+");
	if(pfile)
	{
        //获取FrameBufferSurface对应的ion地址
        result = target->lock(GraphicBuffer::USAGE_SW_READ_OFTEN, &addr);
        
        if(addr != NULL){
            ALOGE("The addr : %p", addr);
            int result = -1;
            // system("mkdir /data/dump && chmod 777 /data/dump");
            result = fwrite( (const void *)addr,
                    (size_t)( (buffer_size)),
                    1,
                    pfile);
            if(result >0){
                ALOGD("fwrite success!");
            }else{
                ALOGE("fwrite failed error %d", result);
            }
        }else{
            ALOGE("lock buffer error!");
        }
        fclose(pfile);
        target->unlock();
	}
}

status_t GLESRenderEngine::drawLayers(const DisplaySettings& display,
                                      const std::vector<const LayerSettings*>& layers,
                                      ANativeWindowBuffer* const buffer,
                                      const bool useFramebufferCache, base::unique_fd&& bufferFence,
                                      base::unique_fd* drawFence) {
    ATRACE_CALL();
    if (layers.empty()) {
        ALOGV("Drawing empty layer stack");
        return NO_ERROR;
    }

    if (bufferFence.get() >= 0) {
        // Duplicate the fence for passing to waitFence.
        base::unique_fd bufferFenceDup(dup(bufferFence.get()));
        if (bufferFenceDup < 0 || !waitFence(std::move(bufferFenceDup))) {
            ATRACE_NAME("Waiting before draw");
            sync_wait(bufferFence.get(), -1);
        }
    }

    if (buffer == nullptr) {
        ALOGE("No output buffer provided. Aborting GPU composition.");
        return BAD_VALUE;
    }

    //dump layers
    char pro_value[PROPERTY_VALUE_MAX];
    property_get("buffer.dump",pro_value,0);
    if(!strcmp(pro_value,"true"))
    {
        ALOGD("dump_content_of_layers_to_file!");
        //dump_content_of_layers_to_file(mCurrentBuffer);
        //int layer_order = 0;
        for (auto const layer : layers) {
            if (layer->source.buffer.buffer != nullptr) {
                sp<GraphicBuffer> gBuf = layer->source.buffer.buffer;
                dump_content_of_layers_to_file(gBuf);
                //layer_order++;
            }
        }
    }
    ...
}

当然上述仅仅是提供了一种思路,具体的上述源码逻辑用在什么地方,读者可以根据自己的需要自行调整。这里我们简单测试一下,看看生成的dump文件如下:

XXX:/data/dump # setprop buffer.dump true
XXX:/data/dump # ls
buffer_layer_0_frame_2880_2560_4.bin  buffer_layer_3_frame_1920_56_4.bin    buffer_layer_6_frame_1920_24_4.bin
buffer_layer_1_frame_1920_1080_4.bin  buffer_layer_4_frame_2880_2560_4.bin  buffer_layer_7_frame_1920_56_4.bin
buffer_layer_2_frame_1920_24_4.bin    buffer_layer_5_frame_1920_1080_4.bin  buffer_layer_8_frame_22_28_4.bin

2.2 dump Android GPU合成图层GraphicBuffer

??这块我们可以在FramebufferSurface.cpp的如下方法中添加相关的逻辑,如下:

//frameworks/native/services/surfaceflinger/DisplayHardware/FramebufferSurface.cpp


void dump_content_of_layers_to_file(const sp<GraphicBuffer>& target)
{
    ALOGE("dump_content_of_layers_to_file");
    ALOGE("dump_content_of_layers_to_file");
    ALOGE("dump_content_of_layers_to_file");
    int result = -1;
    void *addr = NULL;
    static int DumpSurfaceCount = 0;
	int32_t bufStride;
	FILE * pfile = NULL;
	char layername[100] ;
	memset(layername,0,sizeof(layername));

    uint32_t w, s, h, f;
    w = target->getWidth();
    h = target->getHeight();
    s = target->getStride();
    f = target->getPixelFormat();
    android_dataspace d;
    uint32_t buffer_size = 0;
    d = HAL_DATASPACE_UNKNOWN;
    buffer_size = s * h * bytesPerPixel(f);
    bufStride = bytesPerPixel(f);
    ALOGE("FrameBufferSurface info w:%d h:%d s:%d f:%d d:%d size:%d", w, h, s, f, d, buffer_size);

	sprintf(layername,
		"/data/dump/hwc_layer_%d_frame_%d_%d_%d.bin",
		DumpSurfaceCount,
		w,
		h,
		bufStride);

	ALOGD("The dump  file info : %s", layername);
	DumpSurfaceCount ++;
	pfile = fopen(layername,"w+");
	if(pfile)
	{
        //获取FrameBufferSurface对应的ion地址
        result = target->lock(GraphicBuffer::USAGE_SW_READ_OFTEN, &addr);
        
        if(addr != NULL){
            ALOGE("The addr : %p", addr);
            int result = -1;
            system("mkdir /data/dump && chmod 777 /data/dump");
            result = fwrite( (const void *)addr,
                    (size_t)( (buffer_size)),
                    1,
                    pfile);
            if(result >0){
                ALOGD("fwrite success!");
            }else{
                ALOGE("fwrite failed error %d", result);
            }
        }else{
            ALOGE("lock buffer error!");
        }
        fclose(pfile);
        target->unlock();
        usleep(1000 * 5);//延时5毫秒
	}
}

#define HWC_DUMP_LAYER 1
status_t FramebufferSurface::nextBuffer(uint32_t& outSlot,
        sp<GraphicBuffer>& outBuffer, sp<Fence>& outFence,
        Dataspace& outDataspace) {
	...
	  #if HWC_DUMP_LAYER
    char pro_value[PROPERTY_VALUE_MAX];
    property_get("hwc.dump",pro_value,0);
    if(!strcmp(pro_value,"true"))
    {
        dump_content_of_layers_to_file(mCurrentBuffer);
    }
    #endif

    status_t result = mHwc.setClientTarget(mDisplayId, outSlot, outFence, outBuffer, outDataspace);
    ...
}

当然上述仅仅是提供了一种思路,具体的上述源码逻辑用在什么地方,读者可以根据自己的需要自行调整。这里我们简单测试一下,看看生成的dump文件如下:

XXX:/data/dump #setprop hwc.dump true
XXX:/data/dump # ls
hwc_layer_0_frame_1920_1080_4.bin  hwc_layer_2_frame_1920_1080_4.bin  hwc_layer_4_frame_1920_1080_4.bin  hwc_layer_6_frame_1920_1080_4.bin
hwc_layer_1_frame_1920_1080_4.bin  hwc_layer_3_frame_1920_1080_4.bin  hwc_layer_5_frame_1920_1080_4.bin  hwc_layer_7_frame_1920_1080_4.bin

2.3.dump渲染和合成图层GraphicBuffer指南

??到这里dump的相关逻辑就告一段落了,细心的朋友也许会发现dump渲染和合成的图层代码逻辑基本完全一致。是的,这是因为无论是渲染的图层还是合成之后的图层它们都是通过GraphicBuffer来进行存储指向的。这里我们简单总结下dump SurfaceFlinger中各种图层GraphicBuffer内容的思路:

  • 首先要获取各种图层所指向的GraphicBuffer
  • 获取GraphicBuffer的存储地址(通常是通过内存映射过来的)
  • 然后将获取到的地址中的内容,以一定的格式进行存储,通常是GRB888格式(也可以进行相关的封装)

这里重点补充一下,关于dump GraphicBuffer获取的信息大小,格式,以及存储计算规则是否正确可以通过dumpsys SurfaceFlinger进行查看,如下:

GraphicBufferAllocator buffers:
0xb400edc6f16e8500:  420.00 KiB | 1920 (1920) x   56 |    1 |        1 | 0xb00 | NavigationBar0#0
0xb400edc6f16e8780:  420.00 KiB | 1920 (1920) x   56 |    1 |        1 | 0xb00 | NavigationBar0#0
0xb400edc6f16e8a00:  180.00 KiB | 1920 (1920) x   24 |    1 |        1 | 0xb00 | StatusBar#0
0xb400edc6f16e8aa0:  420.00 KiB | 1920 (1920) x   56 |    1 |        1 | 0xb00 | NavigationBar0#0
0xb400edc6f16e8be0:  420.00 KiB | 1920 (1920) x   56 |    1 |        1 | 0xb00 | NavigationBar0#0
0xb400edc6f16e9040:  180.00 KiB | 1920 (1920) x   24 |    1 |        1 | 0xb00 | StatusBar#0
0xb400edc6f16e9400:  180.00 KiB | 1920 (1920) x   24 |    1 |        1 | 0xb00 | StatusBar#0
0xb400edc6f16e95e0:  180.00 KiB | 1920 (1920) x   24 |    1 |        1 | 0xb00 | StatusBar#0
0xb400edc6f16e9900: 8100.00 KiB | 1920 (1920) x 1080 |    1 |        1 | 0xb00 | com.android.launcher3/com.android.launcher3.uioverrides.QuickstepLauncher#0
0xb400edc6f16e9e00: 28800.00 KiB | 2880 (2880) x 2560 |    1 |        2 | 0xb00 | com.android.systemui.ImageWallpaper#0
0xb400edc6f16ea620: 8100.00 KiB | 1920 (1920) x 1080 |    1 |        1 | 0xb00 | com.android.launcher3/com.android.launcher3.uioverrides.QuickstepLauncher#0
0xb400edc6f16eaf80: 8100.00 KiB | 1920 (1920) x 1080 |    1 |        1 | 0x1a00 | FramebufferSurface
0xb400edc6f16eb660: 8100.00 KiB | 1920 (1920) x 1080 |    1 |        1 | 0xb00 | com.android.launcher3/com.android.launcher3.uioverrides.QuickstepLauncher#0
0xb400edc6f16eb8e0: 8100.00 KiB | 1920 (1920) x 1080 |    1 |        1 | 0xb00 | com.android.launcher3/com.android.launcher3.uioverrides.QuickstepLauncher#0
0xb400edc6f2126240: 8100.00 KiB | 1920 (1920) x 1080 |    1 |        1 | 0x1a00 | FramebufferSurface
0xb400edc777c22740:    0.12 KiB |    1 (  32) x    1 |    1 |        1 | 0x300 | placeholder
Total allocated by GraphicBufferAllocator (estimate): 79800.12 KB
Imported gralloc buffers:
+ name:com.android.launcher3/com.android.launcher3.uioverrides.Quickst, id:849, size:8.1e+03KiB, w/h:1920x1080, usage: 0xb00, req fmt:1, fourcc/mod:875708993/0, compressed: false
	planes: R/G/B/A:	 w/h:1920x1080, stride:7680 bytes, size:8294400
+ name:com.android.launcher3/com.android.launcher3.uioverrides.Quickst, id:844, size:8.1e+03KiB, w/h:1920x1080, usage: 0xb00, req fmt:1, fourcc/mod:875708993/0, compressed: false
	planes: R/G/B/A:	 w/h:1920x1080, stride:7680 bytes, size:8294400
+ name:com.android.launcher3/com.android.launcher3.uioverrides.Quickst, id:840, size:8.1e+03KiB, w/h:1920x1080, usage: 0xb00, req fmt:1, fourcc/mod:875708993/0, compressed: false
	planes: R/G/B/A:	 w/h:1920x1080, stride:7680 bytes, size:8294400
+ name:com.android.launcher3/com.android.launcher3.uioverrides.Quickst, id:747, size:8.1e+03KiB, w/h:1920x1080, usage: 0xb00, req fmt:1, fourcc/mod:875708993/0, compressed: false
	planes: R/G/B/A:	 w/h:1920x1080, stride:7680 bytes, size:8294400
+ name:FramebufferSurface, id:443, size:8.1e+03KiB, w/h:1920x1080, usage: 0x1a00, req fmt:1, fourcc/mod:875708993/0, compressed: false
	planes: R/G/B/A:	 w/h:1920x1080, stride:7680 bytes, size:8294400
+ name:FramebufferSurface, id:418, size:8.1e+03KiB, w/h:1920x1080, usage: 0x1a00, req fmt:1, fourcc/mod:875708993/0, compressed: false
	planes: R/G/B/A:	 w/h:1920x1080, stride:7680 bytes, size:8294400
+ name:com.android.systemui.ImageWallpaper#0, id:332, size:2.9e+04KiB, w/h:2880x2560, usage: 0xb00, req fmt:263, fourcc/mod:875713112/0, compressed: false
	planes: R/G/B:	 w/h:2880x2560, stride:11520 bytes, size:29491200
+ name:StatusBar#0, id:321, size:1.9e+02KiB, w/h:1920x24, usage: 0xb00, req fmt:1, fourcc/mod:875708993/0, compressed: false
	planes: R/G/B/A:	 w/h:1920x24, stride:7680 bytes, size:184320
+ name:StatusBar#0, id:317, size:1.9e+02KiB, w/h:1920x24, usage: 0xb00, req fmt:1, fourcc/mod:875708993/0, compressed: false
	planes: R/G/B/A:	 w/h:1920x24, stride:7680 bytes, size:184320
+ name:NavigationBar0#0, id:315, size:4.3e+02KiB, w/h:1920x56, usage: 0xb00, req fmt:1, fourcc/mod:875708993/0, compressed: false
	planes: R/G/B/A:	 w/h:1920x56, stride:7680 bytes, size:430080
+ name:StatusBar#0, id:290, size:1.9e+02KiB, w/h:1920x24, usage: 0xb00, req fmt:1, fourcc/mod:875708993/0, compressed: false
	planes: R/G/B/A:	 w/h:1920x24, stride:7680 bytes, size:184320
+ name:NavigationBar0#0, id:273, size:4.3e+02KiB, w/h:1920x56, usage: 0xb00, req fmt:1, fourcc/mod:875708993/0, compressed: false
	planes: R/G/B/A:	 w/h:1920x56, stride:7680 bytes, size:430080
+ name:NavigationBar0#0, id:265, size:4.3e+02KiB, w/h:1920x56, usage: 0xb00, req fmt:1, fourcc/mod:875708993/0, compressed: false
	planes: R/G/B/A:	 w/h:1920x56, stride:7680 bytes, size:430080
+ name:StatusBar#0, id:204, size:1.9e+02KiB, w/h:1920x24, usage: 0xb00, req fmt:1, fourcc/mod:875708993/0, compressed: false
	planes: R/G/B/A:	 w/h:1920x24, stride:7680 bytes, size:184320
+ name:NavigationBar0#0, id:175, size:4.3e+02KiB, w/h:1920x56, usage: 0xb00, req fmt:1, fourcc/mod:875708993/0, compressed: false
	planes: R/G/B/A:	 w/h:1920x56, stride:7680 bytes, size:430080
+ name:placeholder, id:72, size:12KiB, w/h:1x1, usage: 0x300, req fmt:1, fourcc/mod:875708993/0, compressed: false
	planes: R/G/B/A:	 w/h:1x1, stride:128 bytes, size:128
Total imported by gralloc: 8e+04KiB




三.写在最后

??至此Android dump渲染和合成图层GraphicBuffer阶段整个就完成了,读者是感到意犹未尽呢,还是想说一句尼玛,瞎扯淡呢!

好了,Android dump渲染和合成图层GraphicBuffer分析就告一段落了,各位青山不改绿水长流,各位江湖见!当然各位读者的点赞和关注是我写作路上前进的最大动力了,如果有啥不对或者不爽的也可以踩一踩也无妨!你们的鼓励和批评是博主前进路上最大的动力。

各位读友,千万不要喷我,因为我这也是第一次深入到Android显示这块的源码逻辑,为啥我深入到了这块,因为入职了一家原厂。所以我现在是菜鸟一杯,如果有对Android graphic刚兴趣的朋友,也可以联系我,一起学习进步!

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-11-09 19:40:29  更:2021-11-09 19:40:59 
 
开发: 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:15:56-

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