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 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> Springloaded源码解析 -> 正文阅读

[Java知识库]Springloaded源码解析

spring-loaded Spring官方的热更新agent
Spring Loaded allows you to add/modify/delete methods/fields/constructors

评价

  • 使用asm进行字节增强,速度较快,但是字节增强的代码比较难懂,可以将增强后的类保存下来反编译查看增强的结果
  • 不依赖于DCEVM,可以直接在开发的jdk中使用
  • 自己实现的类更新监控,一个类只能监控一个文件,不能监控自定义目录,没有使用nio系统文件监控,自己启动一个线程循环遍历比较监控的文件更新时间,效率感觉有点低
  • 只能在IDE中实现热更新,不能给Spring boot jar包的类资源创建监听

使用

IDE中配置vm启动参数

-noverify -javaagent:/tmp/springloaded-1.3.0.RELEASE.jar -Dspringloaded="verbose=true;logging=true;watchJars=dependency.jar"

springloaded支持配置多个参数,用封号拼接
参数说明

  • logging=true 打印日志
  • verbose=true 打印更多的细节
  • watchJars=dependency.jar 监听依赖jar包的变更热更新
  • dump=a.b,c.d,可以将类的.class文件dump下来,然后用jad反编译查看增强类的字节, 例如dump=com.study.ServiceTest2,com.study.ServiceTest
  • dumpFolder=/tmp/dump 指定dump字节文件保存的目录
  • cleanCache
  • caching
  • allowSplitPackages
  • debugplugins 是否打印插件的信息
  • enumlimit
  • profile
  • cacheDir
  • callsideRewritingOn
  • verifyReloads
  • maxClassDefinitions 默认是100
  • asserts 启用强校验
  • rebasePaths 可以更换监控目录
    • 例如"a.b=c.d,e.f=g.h",监控资源的时候会将一个类监控的前缀改写,加入原来类的前缀是a.b.myClass,监控的是c.d.myClass文件变更
  • inclusions 需要增强监控的包名
  • exclusions 不需要增强监控的包名
  • plugins 加载自定义插件,多个用逗号拼接
  • investigateSystemClassReflection
  • rewriteAllSystemClasses
  • explain

编译学习

  • 打开工程用gradle编译即可,没有jdk限制,打包比较简单
  • 有很多单元测试用例可以用来学习
  • 配置dump可以保存增强类,查看增强后的结果

核心代码

  • org.springsource.loaded.agent.SpringLoadedAgent agent启动类
  • org.springsource.loaded.agent.ClassPreProcessorAgentAdapter transform字节
    • 如果是重新加载类,返回ReloadableType定义的字节
    • 类第一次加载的时候调用org.springsource.loaded.agent.SpringLoadedPreProcessor#preProcess进行增强
  • org.springsource.loaded.agent.SpringLoadedPreProcessor 字节增强
public byte[] preProcess(ClassLoader classLoader, String slashedClassName, ProtectionDomain protectionDomain,
			byte[] bytes) {
		// 如果禁用则不增强
		if (disabled) {
			return bytes;
		}
		// 遍历插件进行字节增强
		for (Plugin plugin : getGlobalPlugins()) {
			if (plugin instanceof LoadtimeInstrumentationPlugin) {
				LoadtimeInstrumentationPlugin loadtimeInstrumentationPlugin = (LoadtimeInstrumentationPlugin) plugin;
				if (loadtimeInstrumentationPlugin.accept(slashedClassName, classLoader, protectionDomain, bytes)) {
					bytes = loadtimeInstrumentationPlugin.modify(slashedClassName, classLoader, bytes);
				}
			}
		}
		// 确保系统类被加载
		tryToEnsureSystemClassesInitialized(slashedClassName);
		// 得到类注册器TypeRegistry
		TypeRegistry typeRegistry = TypeRegistry.getTypeRegistryFor(classLoader);
        ...
		if (typeRegistry == null) { // A null type registry indicates nothing is being made reloadable for the classloader
		    ......此处省略,可以通过配置决定是否对系统类进行增强
			return bytes;
		}
		// 决策类是否可以重新加载What happens here? The aim is to determine if the type should be made reloadable.
		// 1. If NO, but something in this classloader might be, then rewrite the call sites.
		// 2. If NO, and nothing in this classloader might be, return the original bytes.
		// 3. If YES, make the type reloadable (including rewriting call sites)
		ReloadableTypeNameDecision isReloadableTypeName = typeRegistry.isReloadableTypeName(slashedClassName,
				protectionDomain, bytes);
		if (isReloadableTypeName.isReloadable) {
			..... 一大堆逻辑,字节增强,核心就是创建ReloadableType对象和资源监控
			try {
                String dottedClassName = slashedClassName.replace('/', '.');
                String watchPath = getWatchPathFromProtectionDomain(protectionDomain, slashedClassName);
                if (watchPath == null) {
                    // 如果是jar包运行的时候会执行到这里来
                    .... For a CGLIB generated type, we may still need to make the type reloadable
                    if(....) {
                       return bytes;
                    } else {
                        ...
                        return rtype.bytesLoaded;
                    }
                }
                // 创建ReloadableType
                ReloadableType rtype = typeRegistry.addType(dottedClassName, bytes);
                if (rtype == null && GlobalConfiguration.callsideRewritingOn) {
                    // it is not a candidate for being made reloadable (maybe it is an annotation type)
                    // but we still need to rewrite call sites.
                    bytes = typeRegistry.methodCallRewrite(bytes);
                }
                else {
                    if (GlobalConfiguration.fileSystemMonitoring && watchPath != null) {
                        // 添加资源更新监控
                        typeRegistry.monitorForUpdates(rtype, watchPath);
                    }
                    return rtype.bytesLoaded;
                }
            }
            ....
		}
		else {
			try {
				// Skipping the CallSiteClassLoader here because types from there will already have been dealt
				// with due to GroovyPlugin class that intercepts define in that infrastructure
				if (needsClientSideRewriting(slashedClassName) &&
						(classLoader == null || !classLoader.getClass().getName().equals(
								"org.codehaus.groovy.runtime.callsite.CallSiteClassLoader"))) {
					// 改写类的调用方式,改写第一次写入缓存,后续从缓存中加载改写的类字节
					bytes = typeRegistry.methodCallRewriteUseCacheIfAvailable(slashedClassName, bytes);
				}
			}
			....
		}
		return bytes;
	}
  • org.springsource.loaded.GlobalConfiguration 配置类,静态代码块中执行配置解析,配置变量都保存为静态变量
  • org.springsource.loaded.TypeRegistry 所有可重载类的生产和注册中心 The type registry tracks all reloadable types loaded by a specific class loader
  • org.springsource.loaded.ReloadableType 可重载类的实现核心,如果一个类是需要可重载的,会这个类会进行增强生成一个ReloadableType的静态变量r$type,方法调用和加载新版本字节都是通过这个r$type实现的
  • org.springsource.loaded.agent.FileSystemWatcher#FileSystemWatcher 自己实现的简单的单线程监控文件变更,不断循环遍历扫描文件的更新时间,不能监控spring boot jar包中的类
  • org.springsource.loaded.ReloadableType.MergedRewrite 内部类,给需要可重载的类进行增强,通过ChainAdaptor嵌套改写字节增强类方法(有点难懂)
    • org.springsource.loaded.MethodInvokerRewriter.RewriteClassAdaptor 一大堆代码,惭愧没看懂增强为什还要这个RewriteClassAdaptor
    • org.springsource.loaded.TypeRewriter.RewriteClassAdaptor 增强都是在这里做的
      • In every method, introduce logic to check it it the latest version of that method
      • Creates additional methods to aid with field setting/getting
      • Creates additional fields to help reloading (reloadable type instance, new field value holders)
      • Creates catchers for inherited methods. Catchers are simply passed through unless a new version of the class
  • org.springsource.loaded.ChildClassLoader 自定义ClassLoader防止内存泄露
    • 通过WeakReference引用,防止 ChildClassLoader初始化就被gc
    • 增加了一个definedCount计数,用来统计加载类的个数,TypeRegistry#checkChildClassLoader会判断加载类的个数是否超过maxClassDefinitions,如果超过以后重新创建一个ChildClassLoader,老的那个ChildClassLoader及其加载的所有类会等待系统自动gc
    • 一个可重载类的新版本字节才是通过ChildClassLoader加载的,类第一次加载都是用的默认classLoader
    public class ChildClassLoader extends URLClassLoader {
    	private static URL[] NO_URLS = new URL[0];
    	private int definedCount = 0;
    	public ChildClassLoader(ClassLoader classloader) {
    	    // 放一个空的URL,这样定义的类字节就没有URL引用
    		super(NO_URLS, classloader);
    	}
    	public Class<?> defineClass(String name, byte[] bytes) {
    	    // 超过maxClassDefinitions以后,会重新创建一个ChildClassLoader,防止内存泄露
    		definedCount++;
    		return super.defineClass(name, bytes, 0, bytes.length);
    	}
    	public int getDefinedCount() {
    		return definedCount;
    	}
    }
    

实用工具类

  • Utils.loadDottedClassAsBytes 加载.class文件字节
  • org.springsource.loaded.ClassRenamer 可以修改类的名字,方法的名字
  • org.springsource.loaded.test.infra.ClassPrinter 将byte数组打印成字节码
    - org.springsource.loaded.TypeDiffComputer#computeDifferences 比较两个类的字节byte[]的差异

类增强

使用demo

// 得到一个TypeRegistry
TypeRegistry typeRegistry = getTypeRegistry("data.HelloWorld");
// 得到一个类的ReloadableType
ReloadableType rtype = typeRegistry.addType("data.HelloWorld", loadBytesForClass("data.HelloWorld"));
// 运行方法
runUnguarded(rtype.getClazz(), "greet");
// 给类加载新版本字节
rtype.loadNewVersion("000", bytes);
// 保留了原始类字节bytesInitial,可以重新加载回最原始的类
rtype.loadNewVersion("000", rtype.bytesInitial);

可重载原理解析

可重载的类进行增强,类增强代码过于复杂,可以直接dump增强后的类来理解原理。

  • 生成了一个ReloadableType对象,赋值给类的静态变量的r$typeReloadableType会监控类资源是否变更,如果变更以后会调用loadNewVersion方法加载最新版本的字节类
  • 在所有的方法前面注入了一段代码,如果类变更以后调用新版本字节类的方法,否则执行原来的代码, 对非静态变量进行了拦截判断

原来的类

package com.study;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class ServiceTest {
    private static final String NAME = "staticName";
    private String name = "name";
    @Autowired
    private ServiceTest2 serviceTest2;
    public String hello(){
        String staticName = NAME;
        String nonStaticName = this.name;
        return "hello," + staticName + nonStaticName;
    }
    public String hello2() {
        String hello = serviceTest2.hello2();
        return hello;
    }
}

增强后的类

package com.study;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springsource.loaded.C;
import org.springsource.loaded.ISMgr;
import org.springsource.loaded.ReloadableType;
import org.springsource.loaded.SSMgr;
import org.springsource.loaded.TypeRegistry;

@Service
public class ServiceTest {
  public static ReloadableType r$type = TypeRegistry.getReloadableType(0, 516);
  
  public transient ISMgr r$fields;
  
  public static final SSMgr r$sfields;
  
  public static String NAME = "staticName";
  
  public String name;
  
  @Autowired
  public ServiceTest2 serviceTest2;
  
  public Object r$get(Object paramObject, String paramString) {
    if (this.r$fields == null)
      this.r$fields = new ISMgr(this, r$type); 
    return this.r$fields.getValue(r$type, paramObject, paramString);
  }
  
  public void r$set(Object paramObject1, Object paramObject2, String paramString) {
    if (this.r$fields == null)
      this.r$fields = new ISMgr(this, r$type); 
    this.r$fields.setValue(r$type, paramObject2, paramObject1, paramString);
  }
  
  public static void r$sets(Object paramObject, String paramString) {
    if (r$sfields == null)
      r$sfields = new SSMgr(); 
    r$sfields.setValue(r$type, paramObject, paramString);
  }
  
  public static Object r$gets(String paramString) {
    if (r$sfields == null)
      r$sfields = new SSMgr(); 
    return r$sfields.getValue(r$type, paramString);
  }
  
  public static void ___clinit___() {
    ((ServiceTest__I)r$type.fetchLatest()).___clinit___();
  }
  
  public ServiceTest(C paramC) {}
  
  public void ___init___() {
    ((ServiceTest__I)r$type.getLatestDispatcherInstance(true)).___init___(this);
  }
  
  public ServiceTest() {
    if (this.r$fields == null)
      this.r$fields = new ISMgr(this, r$type); 
    if (this.r$fields == null)
      this.r$fields = new ISMgr(this, r$type); 
    if (TypeRegistry.instanceFieldInterceptionRequired(516, "name")) {
      r$set("name", this, "name");
    } else {
      this.name = "name";
    } 
  }
  
  public String hello() {
    // 给方法插入了代码,如果类变更了,调用r$type最新的字节类中的hello方法
    if (r$type.changed(0) != 0) {
      if (r$type.changed(0) != 1)
        throw new NoSuchMethodError("com.red.study.ServiceTest.hello()Ljava/lang/String;"); 
      return ((ServiceTest__I)r$type.fetchLatest()).hello(this);
    } 
    r$type.changed(0);
    String staticName = "staticName";
    // 判断本地变量是否需要拦截,如果用增强的变量用r$get获取非静态变量name,否则就用对象本身的name
    String nonStaticName = TypeRegistry.instanceFieldInterceptionRequired(516, "name") ? (String)r$get(this, "name") : this.name;
    return "hello," + staticName + nonStaticName;
  }
  
  public String hello2() {
    // 给方法插入了代码,如果类变更了,调用r$type最新的字节类中的hello方法
    if (r$type.changed(1) != 0) {
      if (r$type.changed(1) != 1)
        throw new NoSuchMethodError("com.red.study.ServiceTest.hello2()Ljava/lang/String;"); 
      return ((ServiceTest__I)r$type.fetchLatest()).hello2(this);
    } 
    r$type.changed(1);
    // 判断serviceTest2是否需要拦截,如果用增强的变量用r$get方法获取serviceTest2,否则用自身的变量serviceTest2
    ServiceTest2 var2 = TypeRegistry.instanceFieldInterceptionRequired(516, "serviceTest2") ? (ServiceTest2)this.r$get(this, "serviceTest2") : this.serviceTest2;
    // 判断是通过__execute调用还是直接调用
    String hello = TypeRegistry.ivicheck(524, "hello2()Ljava/lang/String;") ? (String)var2.__execute((Object[])null, var2, "hello2()Ljava/lang/String;") : var2.hello2();
    return hello;
  }
  
  static {
    if (r$sfields == null)
      r$sfields = new SSMgr(); 
    if (r$type.clinitchanged() != 0) {
      ((ServiceTest__I)r$type.fetchLatest()).___clinit___();
      return;
    } 
  }
  
  public int hashCode() {
    if (r$type.fetchLatestIfExists(2) != null)
      return ((ServiceTest__I)r$type.fetchLatestIfExists(2)).hashCode(this); 
    r$type.fetchLatestIfExists(2);
    return super.hashCode();
  }
  
  public boolean equals(Object paramObject) {
    if (r$type.fetchLatestIfExists(3) != null)
      return ((ServiceTest__I)r$type.fetchLatestIfExists(3)).equals(this, paramObject); 
    r$type.fetchLatestIfExists(3);
    return super.equals(paramObject);
  }
  
  public Object clone() throws CloneNotSupportedException {
    if (r$type.fetchLatestIfExists(4) != null)
      return ((ServiceTest__I)r$type.fetchLatestIfExists(4)).clone(this); 
    r$type.fetchLatestIfExists(4);
    return super.clone();
  }
  
  public String toString() {
    if (r$type.fetchLatestIfExists(5) != null)
      return ((ServiceTest__I)r$type.fetchLatestIfExists(5)).toString(this); 
    r$type.fetchLatestIfExists(5);
    return super.toString();
  }
  
  public Object __execute(Object[] paramArrayOfObject, Object paramObject, String paramString) {
    if (r$type.determineDispatcher(this, paramString) != null)
      return r$type.determineDispatcher(this, paramString).__execute(paramArrayOfObject, this, paramString); 
    r$type.determineDispatcher(this, paramString);
    throw new NoSuchMethodError(paramString);
  }
}

插件

有两种插件LoadtimeInstrumentationPluginReloadEventProcessorPlugin

  • LoadtimeInstrumentationPlugin 在类第一次加载的时候插桩增强,有accept和modify两个方法,一个用来判断是否需要修改类,一个用来修改类
  • ReloadEventProcessorPlugin 在类重新加载的时候处理,有shouldRerunStaticInitializer和reloadEvent方法,一个用来判断是否需要重新初始化类的静态变量和静态方法,一个用来处理重新加载的事件

JVMPlugin插件

如果加载了java/beans/Introspector,introspectorLoaded = true
如果加载了java/beans/ThreadGroupContext类,threadGroupContextLoaded = true
当类被重新加载的时候

  • 如果threadGroupContextLoaded=true, clearThreadGroupContext
  • 如果introspectorLoaded = true会清理Introspector的缓存

SpringPlugin插件

  1. 插桩增强
    AnnotationMethodHandlerAdapterRequestMappingHandlerMappingLocalVariableTableParameterNameDiscoverer等构造函数初始化的时候将bean实例对象注册到SpringPlugin的静态变量List中
    bytesWithInstanceCreationCaptured 方法可以在类字节的构造函数后面追加调用另外一个类的一个方法的代码
	private byte[] bytesWithInstanceCreationCaptured(byte[] bytes, String classToCall, String methodToCall) {
		ClassReader cr = new ClassReader(bytes);
		ClassVisitingConstructorAppender ca = new ClassVisitingConstructorAppender(classToCall, methodToCall);
		cr.accept(ca, 0);
		byte[] newbytes = ca.getBytes();
		return newbytes;
	}
  1. 更新reloadEvent,清理所有的缓存, 重新构建HandlerMapping
	public void reloadEvent(String typename, Class<?> clazz, String versionsuffix) {
	    // 清理methodResolverCache
		removeClazzFromMethodResolverCache(clazz);
		// 清理ReflectionUtils.declaredMethodsCache
		removeClazzFromDeclaredMethodsCache(clazz);
		// 清理org.springframework.beans.CachedIntrospectionResults的classCache、strongClassCache、softClassCache缓存
		clearCachedIntrospectionResults(clazz);
		// 调用DefaultAnnotationHandlerMapping.detectHandlers
		reinvokeDetectHandlers(); // Spring 3.0
		// 清空requestMappingHandlerMappingInstances中的handlerMethods和urlMap, clearMappingRegistry,然后调用initHandlerMethods重新初始化HandlerMapping
		reinvokeInitHandlerMethods(); // Spring 3.1
		// 清理localVariableTableParameterNameDiscovererInstances.parameterNamesCache
		clearLocalVariableTableParameterNameDiscovererCache(clazz);
	}

CglibPlugin插件

  • 对以/cglib/core/AbstractClassGenerator结尾的类才进行增强修改,包括net/sf/cglib/core/AbstractClassGeneratororg/springframework/cglib/core/AbstractClassGenerator
  • 调用CglibPluginCapturing.catchGenerate(bytes)进行字节修改

如何使用监听类变更

在FileSystemWatcher中启动了一个单线程,循环遍历监听类资源,通过修改时间判断类是否有变更,每个可重载的类都会添加一个监听文件到这个类中

为什么Spring不需要重新初始化bean

因为所有可重载的类都被增强了,实例对象的方法都进行了改写,如果新新版本类产生则执行新版本的类方法,入口还是原来实例对象,所以重载事件中只做了清理缓存的操作

版本不断更新后如何防止内存泄露

核心就是前面讲到的ChildClassLoaderTypeRegistry#checkChildClassLoader

  • ChildClassLoader用的是一个空URL数组,没有任何URL引用
  • ChildClassLoader用的是WeakReference弱引用,可以被gc回收
  • ChildClassLoader有一个类加载计数变量definedCount统计所有被该累加载器加载的类个数,只有一个类产生了新版本的时候才会使用ChildClassLoader进行加载,typeRegistry.defineClass方法的第三个传参permanent如果为true使用默认classLoader加载,如果为false才使用ChildClassLoader加载,可重载类在第一次加载的时候用传的是true,加载新版本的时候传的是false
  • TypeRegistry#checkChildClassLoader 在每次加载新版本类的时候会判断加载类的个数是否超过maxClassDefinitions(默认500,可以通过参数修改),如果超过以后重新创建一个ChildClassLoader,原来的弱引用指向新建的这个ChildClassLoader,同时将已经加载的每个类引用清空,这样老的那个ChildClassLoader及其加载的所有类会等待系统gc
public void checkChildClassLoader(ReloadableType currentlyDefining) {
		ChildClassLoader ccl = childClassLoader == null ? null : childClassLoader.get();
		int definedCount = (ccl == null ? 0 : ccl.getDefinedCount());
		long time = System.currentTimeMillis();
		// 至少要隔5s才执行一次重新加载清理逻辑
		if (definedCount > maxClassDefinitions && ((time - lastTidyup) > 5000)) {
			lastTidyup = time;
			ccl = new ChildClassLoader(classLoader.get());
			this.childClassLoader = new WeakReference<ChildClassLoader>(ccl);
			// 遍历所有的ReloadableTypes, 将老的classloader加载的类引用link全部清理
			for (int i = 0; i < reloadableTypesSize; i++) {
				ReloadableType rtype = reloadableTypes[i];
				if (rtype != null && rtype != currentlyDefining) {
					rtype.clearClassloaderLinks();
					// 重新加载最新版本类
					rtype.reloadMostRecentDispatcherAndExecutor();
				}
			}
			for (int i = 0; i < reloadableTypesSize; i++) {
				ReloadableType rtype = reloadableTypes[i];
				if (rtype != null && rtype != currentlyDefining && rtype.hasBeenReloaded()) {
					if (rtype.getLiveVersion().staticInitializedNeedsRerunningOnDefine) {
					    // 如果类重新定义后需要运行静态代码的话则重新执行
						rtype.runStaticInitializer();
					}
				}
			}
			// 防止因为maxClassDefinitions设置的不合理,频繁的重新加载浪费时间,每执行一次reload就把maxClassDefinitions+3
			int count = ccl.getDefinedCount() + 3;
			if (count > maxClassDefinitions) {
				maxClassDefinitions = count;
			}
		}
	}
  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2022-05-09 12:26:14  更:2022-05-09 12:28:20 
 
开发: 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 0:35:52-

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