Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all articles
Browse latest Browse all 5930

如何实现ButterKnife (二) —— BindResource

$
0
0

相关文章:

如何实现ButterKnife (一) —— 搭建开发框架

周末两天早起看TI,看中国夺冠还是很激动的,周末时间一晃也就过去了。不说废话了,接着上一篇现在从最简单的Resource资源绑定来说明,大体了解整个开发基本流程。

@BindString

先定义个用来注入字符串资源的注解:

/**
 * 字符串资源注解
 */
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindString {
    @StringRes int value();
}

可以看到这是一个编译时注解(RetentionPolicy.CLASS),并且注解指定为Field注解(ElementType.FIELD),注解有个int型的属性值用来标注字符串资源ID(R.string.xxx)。注意这里对这个属性使用了 @StringRes 注解来限定取值范围只能是字符串资源,这个注解是在com.android.support:support-annotations 库里的,也就是之前为什么要加这个库。

再来看下注解处理器怎么处理这个注解:

@AutoService(Processor.class)
public class ButterKnifeProcessor extends AbstractProcessor {

    private Types typeUtils;
    private Elements elementUtils;
    private Filer filer;
    private Messager messager;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        typeUtils = processingEnv.getTypeUtils();
        elementUtils = processingEnv.getElementUtils();
        filer = processingEnv.getFiler();
        messager = processingEnv.getMessager();
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // 保存包含注解元素的目标类,注意是使用注解的外围类,主要用来处理父类继承,例:MainActivity
        Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();
        // TypeElement 使用注解的外围类,BindingClass 对应一个要生成的类
        Map<TypeElement, BindingClass> targetClassMap = new LinkedHashMap<>();

        // 处理BindString
        for (Element element : roundEnv.getElementsAnnotatedWith(BindString.class)) {
            if (VerifyHelper.verifyResString(element, messager)) {
                ParseHelper.parseResString(element, targetClassMap, erasedTargetNames, elementUtils);
            }
        }
        // 略...

        for (Map.Entry<TypeElement, BindingClass> entry : targetClassMap.entrySet()) {
            TypeElement typeElement = entry.getKey();
            BindingClass bindingClass = entry.getValue();

            // 查看是否父类也进行注解绑定,有则添加到BindingClass
            TypeElement parentType = _findParentType(typeElement, erasedTargetNames);
            if (parentType != null) {
                BindingClass parentBinding = targetClassMap.get(parentType);
                bindingClass.setParentBinding(parentBinding);
            }

            try {
                // 生成Java文件
                bindingClass.brewJava().writeTo(filer);
            } catch (IOException e) {
                _error(typeElement, "Unable to write view binder for type %s: %s", typeElement,
                        e.getMessage());
            }
        }
        return true;
    }

    /**
     * 查找父类型
     * @param typeElement   类元素
     * @param erasedTargetNames 存在的类元素
     * @return
     */
    private TypeElement _findParentType(TypeElement typeElement, Set<TypeElement> erasedTargetNames) {
        TypeMirror typeMirror;
        while (true) {
            // 父类型要通过 TypeMirror 来获取
            typeMirror = typeElement.getSuperclass();
            if (typeMirror.getKind() == TypeKind.NONE) {
                return null;
            }
            // 获取父类元素
            typeElement = (TypeElement) ((DeclaredType)typeMirror).asElement();
            if (erasedTargetNames.contains(typeElement)) {
                // 如果父类元素存在则返回
                return typeElement;
            }
        }
    }
}
代码上的注释都大概说明了所做的事,现在只看关键的几个地方:

1、erasedTargetNames 保存的是使用注解的外围类元素,如 MainActivity 里有个变量 String mBindString 使用了 @BindString,那这里就会保存 MainActivity所对应的元素。这个主要在后面处理父类绑定继承的问题上需要用到,暂时不介绍这个可以先了解下即可;

2、targetClassMap 的键值对存储的是键就是上面的外围类元素,而值是一个 BindingClass,它保存了我们要生成的Java代码所有必要的数据信息,一个 BindingClass对应一个生成的Java类;

3、VerifyHelper 和 ParseHelper是我另外封装的两个帮助类,主要是把注解处理的过程进行拆分,这样看逻辑思路的时候会清晰点,我们所做的处理工作大部分都在这里面进行,注意这样拆分不是必须的;

4、最后的代码就是生产Java文件的处理了,这个也放后面说明;

先来看下 VerifyHelper.verifyResString() 做了什么:

/**
 * 检验元素有效性帮助类
 */
public final class VerifyHelper {

    private static final String STRING_TYPE = "java.lang.String";

    private VerifyHelper() {
        throw new AssertionError("No instances.");
    }

    /**
     * 验证 String Resource
     */
    public static boolean verifyResString(Element element, Messager messager) {
        return _verifyElement(element, BindString.class, messager);
    }

    /**
     * 验证元素的有效性
     * @param element   注解元素
     * @param annotationClass   注解类
     * @param messager  提供注解处理器用来报告错误消息、警告和其他通知
     * @return  有效则返回true,否则false
     */
    private static boolean _verifyElement(Element element, Class<? extends Annotation> annotationClass,
                                        Messager messager) {
        // 检测元素的有效性
        if (!SuperficialValidation.validateElement(element)) {
            return false;
        }
        // 获取最里层的外围元素
        TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

        if (!_verifyElementType(element, annotationClass, messager)) {
            return false;
        }
        // 使用该注解的字段访问权限不能为 private 和 static
        Set<Modifier> modifiers = element.getModifiers();
        if (modifiers.contains(Modifier.PRIVATE) || modifiers.contains(Modifier.STATIC)) {
            _error(messager, element, "@%s %s must not be private or static. (%s.%s)",
                    annotationClass.getSimpleName(), "fields", enclosingElement.getQualifiedName(),
                    element.getSimpleName());
            return false;
        }
        // 包含该注解的外围元素种类必须为 Class
        if (enclosingElement.getKind() != ElementKind.CLASS) {
            _error(messager, enclosingElement, "@%s %s may only be contained in classes. (%s.%s)",
                    annotationClass.getSimpleName(), "fields", enclosingElement.getQualifiedName(),
                    element.getSimpleName());
            return false;
        }
        // 包含该注解的外围元素访问权限不能为 private
        if (enclosingElement.getModifiers().contains(Modifier.PRIVATE)) {
            _error(messager, enclosingElement, "@%s %s may not be contained in private classes. (%s.%s)",
                    annotationClass.getSimpleName(), "fields", enclosingElement.getQualifiedName(),
                    element.getSimpleName());
            return false;
        }
        // 判断是否处于错误的包中
        String qualifiedName = enclosingElement.getQualifiedName().toString();
        if (qualifiedName.startsWith("android.")) {
            _error(messager, element, "@%s-annotated class incorrectly in Android framework package. (%s)",
                    annotationClass.getSimpleName(), qualifiedName);
            return false;
        }
        if (qualifiedName.startsWith("java.")) {
            _error(messager, element, "@%s-annotated class incorrectly in Java framework package. (%s)",
                    annotationClass.getSimpleName(), qualifiedName);
            return false;
        }

        return true;
    }

    /**
     * 验证元素类型的有效性
     * @param element   元素
     * @param annotationClass   注解类
     * @param messager  提供注解处理器用来报告错误消息、警告和其他通知
     * @return  有效则返回true,否则false
     */
    private static boolean _verifyElementType(Element element, Class<? extends Annotation> annotationClass,
                                              Messager messager) {
        // 获取最里层的外围元素
        TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();

        // 检测使用该注解的元素类型是否正确
        if (annotationClass == BindString.class) {
            if (!STRING_TYPE.equals(element.asType().toString())) {
                _error(messager, element, "@%s field type must be 'String'. (%s.%s)",
                        annotationClass.getSimpleName(), enclosingElement.getQualifiedName(),
                        element.getSimpleName());
                return false;
            }
        } 
        // 略...

        return true;
    }

    /**
     * 输出错误信息
     * @param element
     * @param message
     * @param args
     */
    private static void _error(Messager messager, Element element, String message, Object... args) {
        if (args.length > 0) {
            message = String.format(message, args);
        }
        messager.printMessage(Diagnostic.Kind.ERROR, message, element);
    }
}

我把验证过程也分为几个方法处理,因为其它注解的处理过程也基本类似,只是在 _verifyElementType() 判断类型的时候会稍微有点不同,这个后面会看到。来整理下检测的流程,我也分步骤来说吧:

1、首先调用了 SuperficialValidation.validateElement(element) 来检测使用注解的元素的有效性,这个是 auto-common 提供的方法,可以看下官方说明;

2、_verifyElementType() 检测元素类型是否符合要求,因为我们在处理字符串资源的绑定,所以元素类型必须为 STRING_TYPE("java.lang.String")

3、然后就是判断外围元素种类必须为 Class 和一些访问权限的判断,因为我们后面肯定要对这些字段的值进行注入,所以需要有访问的权限;

4、最后就是判断外围元素不能处在 android. 和 java. 开头的系统包中,getQualifiedName() 取得是它完全限定名,即包含包路径的完整名称;

检测大体就这样,下面来看怎么解析注解:

/**
 * 注解解析绑定帮助类
 */
public final class ParseHelper {

    private ParseHelper() {
        throw new AssertionError("No instances.");
    }

    /**
     * 解析 String 资源
     *
     * @param element        使用注解的元素
     * @param targetClassMap 映射表
     * @param elementUtils   元素工具类
     */
    public static void parseResString(Element element, Map<TypeElement, BindingClass> targetClassMap,
                                      Set<TypeElement> erasedTargetNames, Elements elementUtils) {
        // 获取字段名和注解的资源ID
        String name = element.getSimpleName().toString();
        int resId = element.getAnnotation(BindString.class).value();

        BindingClass bindingClass = _getOrCreateTargetClass(element, targetClassMap, elementUtils);
        // 生成资源信息
        FieldResourceBinding binding = new FieldResourceBinding(resId, name, "getString");
        // 给BindingClass添加资源信息
        bindingClass.addResourceBinding(binding);

        erasedTargetNames.add((TypeElement) element.getEnclosingElement());
    }

    /**
     * 获取存在的 BindingClass,没有则重新生成
     *
     * @param element        使用注解的元素
     * @param targetClassMap 映射表
     * @param elementUtils   元素工具类
     * @return BindingClass
     */
    private static BindingClass _getOrCreateTargetClass(Element element, Map<TypeElement, BindingClass> targetClassMap,
                                                        Elements elementUtils) {
        TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
        BindingClass bindingClass = targetClassMap.get(enclosingElement);
        // 以下以 com.butterknife.MainActivity 这个类为例
        if (bindingClass == null) {
            // 获取元素的完全限定名称:com.butterknife.MainActivity
            String targetType = enclosingElement.getQualifiedName().toString();
            // 获取元素所在包名:com.butterknife
            String classPackage = elementUtils.getPackageOf(enclosingElement).getQualifiedName().toString();
            // 获取要生成的Class的名称:MainActivity$$ViewBinder
            int packageLen = classPackage.length() + 1;
            String className = targetType.substring(packageLen).replace('.', '$') + BINDING_CLASS_SUFFIX;
            // 生成Class的完全限定名称:com.butterknife.MainActivity$$ViewBinder
            String classFqcn = classPackage + "." + className;

            /* 不要用下面这个来生成Class名称,内部类会出错,比如ViewHolder */
//            String className = enclosingElement.getSimpleName() + BINDING_CLASS_SUFFIX;

            bindingClass = new BindingClass(classPackage, className, targetType, classFqcn);
            targetClassMap.put(enclosingElement, bindingClass);
        }
        return bindingClass;
    }
}

可以看到这里主要就是获取对应的注解字段和它的注解属性值,然后生成一个 FieldResourceBinding 对象并添加到BindingClass中。我们前面介绍了一个外围类对应一个 BindingClass,而外围类可能同时包含多个注解的,所以 BindingClass可能是存在并保存在 targetClassMap中,这时我们直接去获取就行了。现在只要了解 FieldResourceBinding和 BindingClass的用法整个流程就通了,先来看下 FieldResourceBinding:

/**
 * 资源信息
 */
public final class FieldResourceBinding {

    // 资源ID
    private final int id;
    // 字段变量名称
    private final String name;  
    // 获取资源数据的方法
    private final String method;    

    public FieldResourceBinding(int id, String name, String method) {
        this.id = id;
        this.name = name;
        this.method = method;
    }

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getMethod() {
        return method;
    }
}

这个还是好理解,id name 都好理解,前面我们也通过注解获取了,至于 method我们现在取的是String资源所以传入"getString",这个等下在 BindingClass中就会用到。下面来看 BindingClass的实现:

/**
 * 绑定处理类,一个 BindingClass 对应一个要生成的类
 */
public final class BindingClass {

    private static final ClassName FINDER = ClassName.get("com.dl7.butterknifelib", "Finder");
    private static final ClassName VIEW_BINDER = ClassName.get("com.dl7.butterknifelib", "ViewBinder");
    private static final ClassName CONTEXT = ClassName.get("android.content", "Context");
    private static final ClassName RESOURCES = ClassName.get("android.content.res", "Resources");

    private final List<FieldResourceBinding> resourceBindings = new ArrayList<>();
    private final String classPackage;
    private final String className;
    private final String targetClass;
    private final String classFqcn;


    /**
     * 绑定处理类
     *
     * @param classPackage 包名:com.butterknife
     * @param className    生成的类:MainActivity$$ViewBinder
     * @param targetClass  目标类:com.butterknife.MainActivity
     * @param classFqcn    生成Class的完全限定名称:com.butterknife.MainActivity$$ViewBinder
     */
    public BindingClass(String classPackage, String className, String targetClass, String classFqcn) {
        this.classPackage = classPackage;
        this.className = className;
        this.targetClass = targetClass;
        this.classFqcn = classFqcn;
    }

    /**
     * 生成Java类
     *
     * @return JavaFile
     */
    public JavaFile brewJava() {
        // 构建一个类
        TypeSpec.Builder result = TypeSpec.classBuilder(className)
                .addModifiers(Modifier.PUBLIC)
                .addTypeVariable(TypeVariableName.get("T", ClassName.bestGuess(targetClass)));

        if (_hasParentBinding()) {
            // 有父类则继承父类
            result.superclass(ParameterizedTypeName.get(ClassName.bestGuess(parentBinding.classFqcn),
                    TypeVariableName.get("T")));
        } else {
            // 实现 ViewBinder 接口
            result.addSuperinterface(ParameterizedTypeName.get(VIEW_BINDER, TypeVariableName.get("T")));
        }

        // 添加方法
        result.addMethod(_createBindMethod());
        // 构建Java文件
        return JavaFile.builder(classPackage, result.build())
                .addFileComment("Generated code from Butter Knife. Do not modify!")
                .build();
    }

    /**
     * 创建方法
     *
     * @return MethodSpec
     */
    private MethodSpec _createBindMethod() {
        // 定义一个方法,其实就是实现 ViewBinder 的 bind 方法
        MethodSpec.Builder result = MethodSpec.methodBuilder("bind")
                .addAnnotation(Override.class)
                .addModifiers(Modifier.PUBLIC)
                .addParameter(FINDER, "finder", Modifier.FINAL)
                .addParameter(TypeVariableName.get("T"), "target", Modifier.FINAL)
                .addParameter(Object.class, "source");

        if (_hasParentBinding()) {
            // 调用父类的bind()方法
            result.addStatement("super.bind(finder, target, source)");
        }
        
        if (_hasResourceBinding()) {
            // 过滤警告
            result.addAnnotation(AnnotationSpec.builder(SuppressWarnings.class)
                    .addMember("value", "$S", "ResourceType")
                    .build());

            result.addStatement("$T context = finder.getContext(source)", CONTEXT);
            result.addStatement("$T res = context.getResources()", RESOURCES);
            // Resource
            for (FieldResourceBinding binding : resourceBindings) {
                result.addStatement("target.$L = res.$L($L)", binding.getName(), binding.getMethod(),
                        binding.getId());
            }
        }

        return result.build();
    }

    /**
     * 添加资源
     *
     * @param binding 资源信息
     */
    public void addResourceBinding(FieldResourceBinding binding) {
        resourceBindings.add(binding);
    }

    private boolean _hasResourceBinding() {
        return !(resourceBindings.isEmpty() && colorBindings.isEmpty());
    }
}
这里只列了必要的步骤,其实整个过程就是用 javapoet 生成Java类,和我们注解相关的最主要是这句话:

result.addStatement("target.$L = res.$L($L)", binding.getName(), binding.getMethod(), binding.getId());

我们调用对应的方法(getMethod)并传入对应的ID(getId)来设置对应的字段(getName),和我们平常写句子的逻辑是一样的。关于javapoet的详细用法可以参考官方示例。

在处理的过程中我们还进行了父类继承的处理,为什么要进行这个处理呢?举个简单的例子,我们有一个类B,它有直接父类A,它们都有使用我们定义的注解,正常我们类B是可以调用类A中的非私有域,所以我们在执行类B的bind()方法时要先执行父类的bind()方法,即super.bind()

到这里整个流程就完成了,你再回头看看注解处理器最后生成Java文件的代码应该能理解是怎么回事了,现在在代码中应该也能正常使用了,使用代码我就不贴了,看下文章最后给的例子源码就行了。

@BindColor

接下来看下 @BindColor 注解怎么处理,其实大体流程都是一样的,先定义个注解:

/**
 * 绑定颜色资源
 */
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface BindColor {
    @ColorRes int value();
}

同样的,这边用了 @ColorRes 来限定注解的属性值只能为 R.color.xxx

再看下注解处理器:

@AutoService(Processor.class)
public class ButterKnifeProcessor extends AbstractProcessor {
    
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
    	// 略...	

		// 处理BindColor
	    for (Element element : roundEnv.getElementsAnnotatedWith(BindColor.class)) {
	        if (VerifyHelper.verifyResColor(element, messager)) {
	            ParseHelper.parseResColor(element, targetClassMap, erasedTargetNames, elementUtils);
	        }
	    }
	    // 略...
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotations = new LinkedHashSet<>();
        annotations.add(BindString.class.getCanonicalName());
        annotations.add(BindColor.class.getCanonicalName());
        return annotations;
    }
}

和刚才的注解处理基本一致,主要来看下怎么检查和解析注解,注意要在 getSupportedAnnotationTypes() 中指明要处理这个注解。检测如下:

public final class VerifyHelper {

    private static final String COLOR_STATE_LIST_TYPE = "android.content.res.ColorStateList";
    
    /**
     * 验证 Color Resource
     */
    public static boolean verifyResColor(Element element, Messager messager) {
        return _verifyElement(element, BindColor.class, messager);
    }

    private static boolean _verifyElement(Element element, Class<? extends Annotation> annotationClass,
                                        Messager messager) {
    	// 和 @BindString 一致
    }

    private static boolean _verifyElementType(Element element, Class<? extends Annotation> annotationClass,
                                              Messager messager) {
        // 获取最里层的外围元素
        TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
		if (annotationClass == BindColor.class) {
            if (COLOR_STATE_LIST_TYPE.equals(element.asType().toString())) {
                return true;
            } else if (element.asType().getKind() != TypeKind.INT) {
                _error(messager, element, "@%s field type must be 'int' or 'ColorStateList'. (%s.%s)",
                        BindColor.class.getSimpleName(), enclosingElement.getQualifiedName(),
                        element.getSimpleName());
                return false;
            }
        }

        return true;
    }
}

检测主要看元素类型的检测,其它的和上一个注解一致,在这里我们定义的注解可能会返回两种情况:一种是我们正常使用的颜色资源,返回 int 类型;还有一种是根据状态变化的颜色选择器,返回 COLOR_STATE_LIST_TYPE = "android.content.res.ColorStateList",由 <selector> 标签定义的一组颜色值。

下面看对注解的解析:

public final class ParseHelper {

    /**
     * 解析 String 资源
     *
     * @param element        使用注解的元素
     * @param targetClassMap 映射表
     * @param elementUtils   元素工具类
     */
    public static void parseResColor(Element element, Map<TypeElement, BindingClass> targetClassMap,
                                     Set<TypeElement> erasedTargetNames, Elements elementUtils) {
        // 获取字段名和注解的资源ID
        String name = element.getSimpleName().toString();
        int resId = element.getAnnotation(BindColor.class).value();

        BindingClass bindingClass = _getOrCreateTargetClass(element, targetClassMap, elementUtils);
        // 生成资源信息
        FieldColorBinding binding;
        if (COLOR_STATE_LIST_TYPE.equals(element.asType().toString())) {
            binding = new FieldColorBinding(resId, name, "getColorStateList");
        } else {
            binding = new FieldColorBinding(resId, name, "getColor");
        }
        // 给BindingClass添加资源信息
        bindingClass.addColorBinding(binding);

        erasedTargetNames.add((TypeElement) element.getEnclosingElement());
    }
}

还是和 @BindString 基本一致,主要多了对 COLOR_STATE_LIST_TYPE 的处理,这里分别设置不同的 Method 来获取数据。这边的 FieldColorBinding 和 FieldResourceBinding 的字段其实都是一样的,我把它们分开是后面生成Java文件的时候好判断,定义如下:

/**
 * 资源 Color 绑定信息
 */
public final class FieldColorBinding {

    private final int id;
    private final String name;
    private final String method;

    public FieldColorBinding(int id, String name, String method) {
        this.id = id;
        this.name = name;
        this.method = method;
    }

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public String getMethod() {
        return method;
    }
}

最后看下 BindingClass 的处理,只给出变化的部分:

public final class BindingClass {

    private static final ClassName CONTEXT = ClassName.get("android.content", "Context");
    private static final ClassName RESOURCES = ClassName.get("android.content.res", "Resources");
    private static final ClassName CONTEXT_COMPAT = ClassName.get("android.support.v4.content", "ContextCompat");

    private final List<FieldColorBinding> colorBindings = new ArrayList<>();

    private MethodSpec _createBindMethod() {
        // 略...
        if (_hasResourceBinding()) {
        	// 略...

            // ClassResource
            for (FieldColorBinding binding : colorBindings) {
                result.addStatement("target.$L = $T.$L(context, $L)", binding.getName(), CONTEXT_COMPAT,
                        binding.getMethod(), binding.getId());
            }
        }
    }

    public void addColorBinding(FieldColorBinding binding) {
        colorBindings.add(binding);
    }
}
可以看到在获取颜色的时候用了 v4 包中的 ContextCompat 类来处理,这是一个兼容类,因为在 SDK≥23 的时候获取颜色会需要传入一个 Resources.Theme,用兼容包的话会统一帮我们处理,这个和源码处理不一样,但效果是相似的。

到这里 @BindColor 也可以使用了,最后在代码中使用如下:

public class MainActivity extends AppCompatActivity {

    @BindString(R.string.activity_string)
    String mBindString;
    @BindColor(R.color.colorAccent)
    int mBindColor;
    @BindColor(R.color.sel_btn_text)
    ColorStateList mBtnTextColor;

    // 略...
}

看下注解处理器帮我们生成的代码:

public class MainActivity$$ViewBinder<T extends MainActivity> implements ViewBinder<T> {
    @Override
    @SuppressWarnings("ResourceType")
    public void bind(final Finder finder, final T target, Object source) {
        Context context = finder.getContext(source);
        Resources res = context.getResources();
        target.mBindString = res.getString(2131099669);
        target.mBindColor = ContextCompat.getColor(context, 2131427346);
        target.mBtnTextColor = ContextCompat.getColorStateList(context, 2131427399);
    }
}

可以看到应该是正常的,根据这些代码你再回想一下关于检测的权限、代码的生成过程应该有一个更好的认识。

关于其它资源绑定注解其实实现都和这两个类似,这方面我就不再说明了,下一个就到了最常用的 @Bind 绑定 View 视图的处理。

例子代码:ButterKnifeStudy


作者:github_35180164 发表于2016/8/15 9:33:53 原文链接
阅读:263 评论:0 查看评论

Viewing all articles
Browse latest Browse all 5930

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>