proguard源码分析四 Shrinker

上一节我们分析了proguard是如何把项目里面代码的依赖关系给检索出来,有了依赖关系链之后就可以知道哪些代码是有用的,哪些是无用的,proguard会根据配置文件里的keep规则,配合上前面检索出来的代码依赖关系,就可以把部分无用的代码给裁剪掉了。

proguard的代码压缩过程主要包括了两个步骤:标记压缩,这些事情都是由 Shrinker接口来完成。

标记

Shrinker 首选会根据keep配置规则以及前面检索出来的代码依赖关系,把有用代码跟无用代码都标记出来。标记的过程又可以分为两个步骤,第一个是根据keep配置规则,生成与之相对应的classPool visitor,第二步是classPool visitor会遍历ClassPool,找到需要keep的类,标记为USED,并且会把此类的依赖也标记为USED

  • 创建classPool visitor
     /**
     * Performs shrinking of the given program class pool.
     */
    public ClassPool execute(ClassPool programClassPool,
                             ClassPool libraryClassPool) throws IOException {

        //1 创建classPool visitor
        // Create a visitor for marking the seeds.
        UsageMarker usageMarker = configuration.whyAreYouKeeping == null ?
                new UsageMarker() :
                new ShortestUsageMarker();
        
        ClassPoolVisitor classPoolvisitor =
                ClassSpecificationVisitorFactory.createClassPoolVisitor(configuration.keep,
                        classUsageMarker,
                        usageMarker,
                        true,
                        false,
                        false);
        //2 开始标记类
        // Mark the seeds.
        programClassPool.accept(classPoolvisitor);
        libraryClassPool.accept(classPoolvisitor);
    }

首先是要创建classPool visitor,prougard为每一条keep规则都创建一个与其相对应的classPool visitor,

    public static ClassPoolVisitor createClassPoolVisitor(List keepClassSpecifications,
                                                          ClassVisitor  classVisitor,
                                                          MemberVisitor memberVisitor,
                                                          boolean       shrinking,
                                                          boolean       optimizing,
                                                          boolean       obfuscating)
    {
        MultiClassPoolVisitor multiClassPoolVisitor = new MultiClassPoolVisitor();

        if (keepClassSpecifications != null)
        {
            for (int index = 0; index < keepClassSpecifications.size(); index++)
            {
                KeepClassSpecification keepClassSpecification =
                        (KeepClassSpecification)keepClassSpecifications.get(index);

                //keepClassSpecification.printInfo();

                if ((shrinking   && !keepClassSpecification.allowShrinking)    ||
                        (optimizing  && !keepClassSpecification.allowOptimization) ||
                        (obfuscating && !keepClassSpecification.allowObfuscation))
                {
                    multiClassPoolVisitor.addClassPoolVisitor(
                            createClassPoolVisitor(keepClassSpecification,
                                    classVisitor,
                                    memberVisitor));
                }
            }
        }

        return multiClassPoolVisitor;
    }

createClassPoolVisitor内部会根据我们的keep配置,如keep class keepclassmembers keepclasseswithmembernames等等不同的配置生成不同的classPool visitor,不过是怎么样的配置,最终生成的classPool visitor要么就是NamedClassVisitor要么就是AllClassVisitor。由于proguard支持的keep规则比较多,这里我们不进行一一分析了,我们只分析最常见的keep class规则。

假如有这样一条keep规则:
keep 'public class com.nls.lib.MyClass { *; }
我们通过这条keep规则告诉proguard要保留MyClass类以及类的所有成员,

    public static ClassPoolVisitor createClassPoolVisitor(ClassSpecification classSpecification,
                                                          ClassVisitor       classVisitor,
                                                          MemberVisitor      memberVisitor)
    {
        //1. 创建composedClassVisitor
        // Combine both visitors.
        ClassVisitor composedClassVisitor =
            createCombinedClassVisitor(classSpecification,
                                       classVisitor,
                                       memberVisitor);

        //2. 这里的className就是class com.example.lib.MyClass
        // By default, start visiting from the named class name, if specified.
        String className = classSpecification.className;

        //3. 由于指定了public访问类型所以满足了这里的条件
        // If specified, only visit classes with the right access flags.
        if (classSpecification.requiredSetAccessFlags   != 0 ||
                classSpecification.requiredUnsetAccessFlags != 0)
        {
            composedClassVisitor =
                    new ClassAccessFilter(classSpecification.requiredSetAccessFlags,
                            classSpecification.requiredUnsetAccessFlags,
                            composedClassVisitor);
        }

        //4. 最后创建了NamedClassVisitor.
        // If specified, visit a single named class, otherwise visit all classes.
        return className != null ?
                (ClassPoolVisitor)new NamedClassVisitor(composedClassVisitor, className) :
                (ClassPoolVisitor)new AllClassVisitor(composedClassVisitor);
    }

createClassPoolVisitor方法比较复杂,为了方便理解,我把这条keep规则不会命中到的条件都剔除掉了,最终可以看见生成的classPool visitor就是NamedClassVisitor了(其实绝大多数的keep规则最终都是生成了NamedClassVisitor)

其中标记了1createCombinedClassVisitor方法很重要,它内部创建了类的成员访问器,用作为标记哪些类成员以及它们的依赖为USED

private static ClassVisitor createCombinedClassVisitor(ClassSpecification classSpecification,
                                                       ClassVisitor       classVisitor,
                                                       MemberVisitor      memberVisitor)
{
    //一些无用代码被注释掉了..
    // If specified, let the member info visitor visit the class members.
    if (memberVisitor != null)
    {
        ClassVisitor memberClassVisitor =
                createClassVisitor(classSpecification, memberVisitor);

        // This class visitor may be the only one.
        if (classVisitor == null)
        {
            return memberClassVisitor;
        }

        multiClassVisitor.addClassVisitor(memberClassVisitor);
    }

    return multiClassVisitor;
}

private static ClassVisitor createClassVisitor(ClassSpecification classSpecification,
                                               MemberVisitor      memberVisitor)
{
    MultiClassVisitor multiClassVisitor = new MultiClassVisitor();

    addMemberVisitors(classSpecification.fieldSpecifications,  true,  multiClassVisitor, memberVisitor);
    addMemberVisitors(classSpecification.methodSpecifications, false, multiClassVisitor, memberVisitor);

    // Mark the class member in this class and in super classes.
    return new ClassHierarchyTraveler(true, true, false, false,
            multiClassVisitor);
}

可以看到这个composedClassVisitor其实最终就是ClassHierarchyTraveler,其内部包含了两个类成员访问器,它们都是通过addMemberVisitors方法创建的

private static ClassVisitor createClassVisitor(MemberSpecification memberSpecification,
                                               boolean             isField,
                                               MemberVisitor       memberVisitor)
{
  
   //一些无用代码被注释掉了..
    // Depending on what's specified, visit a single named class member,
    // or all class members, filtering the matching ones.
    return isField ?
            fullySpecified ?
                    (ClassVisitor)new NamedFieldVisitor(name, descriptor, memberVisitor) :
                    (ClassVisitor)new AllFieldVisitor(memberVisitor) :
            fullySpecified ?
                    (ClassVisitor)new NamedMethodVisitor(name, descriptor, memberVisitor) :
                    (ClassVisitor)new AllMethodVisitor(memberVisitor);
}

由于我们这里的keep配置是用了*号通配符,所以最终的成员访问器就是AllFieldVisitorAllMethodVisitor

这里一层套一层的ClassVisitor理解起来会比较复杂,为了快速记忆跟方便理解,我这里画了个图来整理ClassVisitor的嵌套关系


最外层的是NamedClassVisitor,它的作用是根据名字来检索出对应的Clazz对象,NamedClassVisitor里面有两个平级关系的ClassVisitor,一个是MultiClassVisitor,另外一个是ClassHierarchyTraveler,其中MultiClassVisitor里面的NamedMethodVisitor是负责来检索出类对象的<init>方法的,而ClassHierarchyTraveler内部的两个All*Visitor则是负责检索所有的类成员变量跟类成员方法
其实不管套了多少层ClassVisitor,每一层的ClassVisitor仅仅是多加了个条件而已,最终标记都是由UsageMarker来完成

  • 遍历ClassPool标记类
    回来Shrinker的execute方法,
     programClassPool.accept(classPoolvisitor);
     libraryClassPool.accept(classPoolvisitor);
     libraryClassPool.classesAccept(usageMarker);

accept就开始遍历ClassPool,这里传递的classPoolvisitor参数就是上一步创建的NamedClassVisitor,NamedClassVisitor其实只是保存了keep规则里面的类名称而已,代码如下:

public class NamedClassVisitor implements ClassPoolVisitor
{
    private final ClassVisitor classVisitor;
    private final String       name;
    public NamedClassVisitor(ClassVisitor classVisitor,
                             String       name)
    {
        this.classVisitor = classVisitor;
        this.name         = name;
    }
    public void visitClassPool(ClassPool classPool)
    {
        classPool.classAccept(name, classVisitor);
    }
}

classAccept方法会根据类名从ClassPool里面找到对应的Clazz对象。

/**
 * Applies the given ClassVisitor to the class with the given name,
 * if it is present in the class pool.
 */
public void classAccept(String className, ClassVisitor classVisitor)
{
    Clazz clazz = getClass(className);
    if (clazz != null)
    {
        clazz.accept(classVisitor);
    }
}

Clazz对象找到后就开始标记这个Clazz跟它的依赖。这里的ClassVisitor就是前面创建的ClassAccessFilter,由于ClassAccessFilter只是用来匹配过滤下访问权限,这里我们跳过,只分析比较核心的UsageMarker。

public void visitProgramClass(ProgramClass programClass)
{
    if (shouldBeMarkedAsUsed(programClass))
    {
        // Mark this class.
        markAsUsed(programClass);

        markProgramClassBody(programClass);
    }
}

第一步,如果这个类时没有被标记过的话,直接标记为USED,接着开始标记类内部数据,代码如下:

protected void markProgramClassBody(ProgramClass programClass)
{
    //1. 标记常量池里的本类引用
    markConstant(programClass, programClass.u2thisClass);

    //2. 标记常量池里的父类引用
    if (programClass.u2superClass != 0)
    {
        markConstant(programClass, programClass.u2superClass);
    }

    //3. 遍历父类也同样做一次标记
    programClass.hierarchyAccept(false, false, true, false,
            interfaceUsageMarker);

    //4. 标记类的<init>方法
    programClass.methodAccept(ClassConstants.METHOD_NAME_CLINIT,
            ClassConstants.METHOD_TYPE_CLINIT,
            nonEmptyMethodUsageMarker);

    //5. 标记类成员
    programClass.fieldsAccept(possiblyUsedMemberUsageMarker);
    programClass.methodsAccept(possiblyUsedMemberUsageMarker);

    //5. 标记属性表
    programClass.attributesAccept(this);
}

代码比较简单,通过上面调用本类相关的数据都标记为USED了,接着是AllFieldVisitor跟AllMemberVisitor,因为都是类同的逻辑,这里我们只分析AllMemberVisitor,

public class AllMemberVisitor implements ClassVisitor
{
    
    //这里省略部分代码....
    public void visitProgramClass(ProgramClass programClass)
    {
        programClass.fieldsAccept(memberVisitor);
        programClass.methodsAccept(memberVisitor);
    }
}

可以看到AllMemberVisitor本质上就是遍历处理类的所有方法,前面已经提到过,其最终实现都是在UsageMarker类里,我们直接看它的visitProgramMethod实现,过程如下:

public void visitProgramMethod(ProgramClass programClass, ProgramMethod programMethod)
{
    if (shouldBeMarkedAsUsed(programMethod))
    {
        // Is the method's class used?
        if (isUsed(programClass))
        {
            markAsUsed(programMethod);

            // Mark the method body.
            markProgramMethodBody(programClass, programMethod);

            // Mark the method hierarchy.
            markMethodHierarchy(programClass, programMethod);
        }
        //这里省略部分代码...
    }
}

protected void markProgramMethodBody(ProgramClass programClass, ProgramMethod programMethod)
{
    // Mark the name and descriptor.
    markConstant(programClass, programMethod.u2nameIndex);
    markConstant(programClass, programMethod.u2descriptorIndex);

    // Mark the attributes.
    programMethod.attributesAccept(programClass, this);

    // Mark the classes referenced in the descriptor string.
    programMethod.referencedClassesAccept(this);
}

也是简单的标记一下方法的相关数据为USED,这里有一点值得注意的就是programMethod.referencedClassesAccept(this); 这句代码的调用,referencedClasses指向了这个方法的依赖对象,但不包含方法局部变量依赖,怎么理解这句话呢,举个例子,譬如有以下测试代码

    fun test(test: TestClass?) {
        test?.test1()
        val test1 = TestClass1()
        test1.test()
    }

这里的referencedClasses仅包含了TestClass类,而不包含TestClass1类,referencedClasses的检索我们在上一节中以及分析过了,这里就不再赘述了。
referencedClassesAccept方法的实现如下,这里的ClassVisitor就是UsageMarker,这意味着依赖类也会走相同一遍逻辑,也会被标记为USED

public void referencedClassesAccept(ClassVisitor classVisitor)
{
    if (referencedClasses != null)
    {
        for (int index = 0; index < referencedClasses.length; index++)
        {
            if (referencedClasses[index] != null)
            {
                referencedClasses[index].accept(classVisitor);
            }
        }
    }
}

到此我们已经知道了proguard是如何把类、父类、字段、方法以及字段方法的依赖给标记起来了,除此以外一些方法局部变量注解等也会引入类依赖,这些也得被标记出来不能把删除,还有实现接口等也是一样。
回到Shrinkerexecute方法接着往下看

    //1. 标记接口
    programClassPool.classesAccept(new InterfaceUsageMarker(usageMarker));

    //2. 标记内部类 注解 方法局部变量带进来的依赖
    programClassPool.classesAccept(
            new UsedClassFilter(usageMarker,
        new AllAttributeVisitor(true,
        new MultiAttributeVisitor(new AttributeVisitor[]
{
    new InnerUsageMarker(usageMarker),
            new AnnotationUsageMarker(usageMarker),
            new LocalVariableTypeUsageMarker(usageMarker)
}))));

InterfaceUsageMarker负责了类的实现接口标记,LocalVariableTypeUsageMarker便是方法局部变量引用标记的实现,这些依赖类对象都是通过读取对应的属性表数据来获取的,譬如还是上面的测试代码,它的class字节码如下


它的LocalVariableTable(方法的局部变量描述表)里就会有依赖类信息了。

压缩

前面标记完成后,接下来就可以开始做压缩工作了,压缩就是把前面标记了USED的东西留下来,其余都抹掉,从而达到减少包体积的效果。

回到Shrinkerexecute方法接着往下看,压缩的代码如下:

//1. 创建新的ClassPool
ClassPool newProgramClassPool = new ClassPool();
//2. 创建ClassVisitor 压缩ClassPool
programClassPool.classesAccept(
        new UsedClassFilter(usageMarker,
        new MultiClassVisitor(
                new ClassVisitor[] {
                new ClassShrinker(usageMarker),
                new ClassPoolFiller(newProgramClassPool)
})));
//3. 清空旧的ClassPool
programClassPool.clear();

第一步是重新创建了新的ClassPool,用来接受压缩后的Clazz对象数据,接着是创建一系列的ClassVisitor负责压缩的工作,

  • UsedClassFilter 负责类级别的过滤,把没有被标记为USED的类给过滤掉
  • ClassShrinker 是更细粒度的过滤器,负责把Clazz内部没有被标记为USED的数据过滤掉
  • ClassPoolFiller 它的任务比较简单,负责接收前面两个过滤器筛选下来的Clazz对象,最终保存在新的ClassPool里

先看下UsedClassFilter,代码如下

public class UsedClassFilter
        implements   ClassVisitor
{
    
    //这里省略部分代码...
    public void visitProgramClass(ProgramClass programClass)
    {
        if (usageMarker.isUsed(programClass))
        {
            classVisitor.visitProgramClass(programClass);
        }
    }
}

代码也是非常的简单,遍历ClassPool,前面没有被标记为USED的Clazz统统过滤掉。

接着是ClassShrinker,它会对Clazz结构进行一些压缩过滤,譬如我们keep了某个方法,标记的阶段会把此方法给标记为USED,其余没被keep的方法由于没有被标记为USED会在这里被过滤掉。代码如下:

public void visitProgramClass(ProgramClass programClass)
{
    //1. 引用接口压缩.
    if (programClass.u2interfacesCount > 0)
    {
        new InterfaceDeleter(shrinkFlags(programClass.constantPool,
                programClass.u2interfaces,
                programClass.u2interfacesCount))
                .visitProgramClass(programClass);
    }

    //2 .常量池压缩
    int newConstantPoolCount =
            shrinkConstantPool(programClass.constantPool,
                    programClass.u2constantPoolCount);
    //3. 字段集合压缩
    programClass.u2fieldsCount =
            shrinkArray(programClass.fields,
                    programClass.u2fieldsCount);

    //4. 方法集合压缩
    programClass.u2methodsCount =
            shrinkArray(programClass.methods,
                    programClass.u2methodsCount);

    //5. 属性表压缩
    programClass.u2attributesCount =
            shrinkArray(programClass.attributes,
                    programClass.u2attributesCount);

    //6. 遍历所有字段或方法的属性表进行压缩
    programClass.fieldsAccept(this);
    programClass.methodsAccept(this);
    programClass.attributesAccept(this);

    
    if (newConstantPoolCount < programClass.u2constantPoolCount)
    {
        programClass.u2constantPoolCount = newConstantPoolCount;

        //7. 重新建立常量池里面的ID索引
        constantPoolRemapper.setConstantIndexMap(constantIndexMap);
        constantPoolRemapper.visitProgramClass(programClass);
    }

    //8. 清除无用类索引
    ClassShrinker.MySignatureCleaner signatureCleaner = new ClassShrinker.MySignatureCleaner();
    programClass.fieldsAccept(new AllAttributeVisitor(signatureCleaner));
    programClass.methodsAccept(new AllAttributeVisitor(signatureCleaner));
    programClass.attributesAccept(signatureCleaner);

    // Compact the extra field pointing to the subclasses of this class.
    programClass.subClasses =
            shrinkToNewArray(programClass.subClasses);
}

可以看见ClassShrinker做的事情非常的多,也十分的复杂,它会对class字节码结构里面的每一块数据进行压缩,压缩完后还得修复索引index。

这里我们只分析一下方法的压缩过程,我们把测试用的demo修改为这样

class MyClass {

    fun test(test: TestClass?) {
        test?.test1()
    }

    fun test1() {
        val test = TestClass1()
        test.test()
    }
}

把proguard keep规则修改为这样:

keep public class com.nls.lib.MyClass { 
       public void test1();
}

我们只keep test1方法,通过之前的标记过程分析,test方法以及它所依赖的TestClass类都不会被标记为USED ,最终都会被剔除掉。

在ClassShrinker开始工作之前,我们能看见此时的MyClass类方法数是3(有一个隐藏的<init>方法),前面已经提过了,虽然test方法依赖了TestClass类,但是由于我们并没有把test方法给keep住,最终导致TestClass类也是无用的,会被剔除掉。

我们单步进入shrinkConstantPool方法,方法如下:

private int shrinkConstantPool(Constant[] constantPool, int length)
{
//1. 遍历常量池
for (int index = 1; index < length; index++)
{

    Constant constant = constantPool[index];
    if (constant != null)
    {
        isUsed = usageMarker.isUsed(constant);
    }

    //2. 判断常量池内容是否被打了USED标签
    if (isUsed)
    {
        // Remember the new index.
        constantIndexMap[index] = counter;

        //3. 打过USED标签的可以保留下来
        constantPool[counter++] = constant;
    }
    else
    {
        //4. 没打过USED标签的剔除
        constantIndexMap[index] = -1;
    }
}

//5. 把常量池里多余的部分清空为null
Arrays.fill(constantPool, counter, length, null);
}

实现也是比较简单,有打过USED标签的留,没打标签的就不要,譬如我们的测试代码里TestClass类肯定是没被打标签的,需要被剔除的


不负众望的,跑到了下面的分支去了,这样ClassShrinker就会把常量池里的TestClass类引用数据给抹掉了。

压缩完常量池接着就是压缩类成员了,包括了类字段集合跟类方法集合,代码也是比较简单,下面直接给出

private int shrinkArray(VisitorAccepter[] array, int length)
{
    int counter = 0;

    // Shift the used objects together.
    for (int index = 0; index < length; index++)
    {
        VisitorAccepter visitorAccepter = array[index];

        if (usageMarker.isUsed(visitorAccepter))
        {
            array[counter++] = visitorAccepter;
        }
    }

    // Clear any remaining array elements.
    if (counter < length)
    {
        Arrays.fill(array, counter, length, null);
    }

    return counter;
}

同样的也是遍历集合,判断集合里面的对象是否被打了USED标签,打过标签的会被保留下来,没打标签的就直接跳过,最后也是调用Arrays.fill把集合里无用的空间赋值为空。


我们单步调试也能清楚的看见,调用前方法数是3,执行完后方法数就变成了2了,集合里有一个对象被置空了。

最后proguard会以同样的方式对class字节码的各个结构进行压缩,因为压缩后集合里面的数据索引index会发生变化,在压缩完的最后阶段还得做一次索引的重定向修复任务。

总结

本节主要是从源码的角度出发,简单的分析了proguard的压缩功能实现过程,主要分为两个步骤,其一是标记,另外一个就是压缩,压缩的过程较为简单,主要就是根据前面的标记结果,打了USED标签的就保留,没打标签的就会被剔除。压缩完接下来就是代码优化,再下一节中我会继续给大家分析一下代码优化的过程。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,639评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,093评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 167,079评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,329评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,343评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,047评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,645评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,565评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,095评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,201评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,338评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,014评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,701评论 3 332
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,194评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,320评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,685评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,345评论 2 358

推荐阅读更多精彩内容