proguard源碼分析四 Shrinker

上一節(jié)我們分析了proguard是如何把項目里面代碼的依賴關(guān)系給檢索出來钻注,有了依賴關(guān)系鏈之后就可以知道哪些代碼是有用的凳鬓,哪些是無用的,proguard會根據(jù)配置文件里的keep規(guī)則媒惕,配合上前面檢索出來的代碼依賴關(guān)系,就可以把部分無用的代碼給裁剪掉了笑诅。

proguard的代碼壓縮過程主要包括了兩個步驟:標(biāo)記壓縮,這些事情都是由 Shrinker接口來完成疮鲫。

標(biāo)記

Shrinker 首選會根據(jù)keep配置規(guī)則以及前面檢索出來的代碼依賴關(guān)系吆你,把有用代碼跟無用代碼都標(biāo)記出來。標(biāo)記的過程又可以分為兩個步驟俊犯,第一個是根據(jù)keep配置規(guī)則妇多,生成與之相對應(yīng)的classPool visitor,第二步是classPool visitor會遍歷ClassPool燕侠,找到需要keep的類者祖,標(biāo)記為USED,并且會把此類的依賴也標(biāo)記為USED

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

        //1 創(chuàng)建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 開始標(biāo)記類
        // Mark the seeds.
        programClassPool.accept(classPoolvisitor);
        libraryClassPool.accept(classPoolvisitor);
    }

首先是要創(chuàng)建classPool visitor绢彤,prougard為每一條keep規(guī)則都創(chuàng)建一個與其相對應(yīng)的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內(nèi)部會根據(jù)我們的keep配置,如keep class keepclassmembers keepclasseswithmembernames等等不同的配置生成不同的classPool visitor茫舶,不過是怎么樣的配置械巡,最終生成的classPool visitor要么就是NamedClassVisitor要么就是AllClassVisitor。由于proguard支持的keep規(guī)則比較多饶氏,這里我們不進行一一分析了讥耗,我們只分析最常見的keep class規(guī)則。

假如有這樣一條keep規(guī)則:
keep 'public class com.nls.lib.MyClass { *; }
我們通過這條keep規(guī)則告訴proguard要保留MyClass類以及類的所有成員嚷往,

    public static ClassPoolVisitor createClassPoolVisitor(ClassSpecification classSpecification,
                                                          ClassVisitor       classVisitor,
                                                          MemberVisitor      memberVisitor)
    {
        //1. 創(chuàng)建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. 最后創(chuàng)建了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方法比較復(fù)雜,為了方便理解柠衅,我把這條keep規(guī)則不會命中到的條件都剔除掉了皮仁,最終可以看見生成的classPool visitor就是NamedClassVisitor了(其實絕大多數(shù)的keep規(guī)則最終都是生成了NamedClassVisitor)

其中標(biāo)記了1createCombinedClassVisitor方法很重要,它內(nèi)部創(chuàng)建了類的成員訪問器菲宴,用作為標(biāo)記哪些類成員以及它們的依賴為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贷祈,其內(nèi)部包含了兩個類成員訪問器,它們都是通過addMemberVisitors方法創(chuàng)建的

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理解起來會比較復(fù)雜,為了快速記憶跟方便理解谣蠢,我這里畫了個圖來整理ClassVisitor的嵌套關(guān)系


最外層的是NamedClassVisitor粟耻,它的作用是根據(jù)名字來檢索出對應(yīng)的Clazz對象查近,NamedClassVisitor里面有兩個平級關(guān)系的ClassVisitor,一個是MultiClassVisitor挤忙,另外一個是ClassHierarchyTraveler霜威,其中MultiClassVisitor里面的NamedMethodVisitor是負責(zé)來檢索出類對象的<init>方法的,而ClassHierarchyTraveler內(nèi)部的兩個All*Visitor則是負責(zé)檢索所有的類成員變量跟類成員方法
其實不管套了多少層ClassVisitor册烈,每一層的ClassVisitor僅僅是多加了個條件而已戈泼,最終標(biāo)記都是由UsageMarker來完成

  • 遍歷ClassPool標(biāo)記類
    回來Shrinker的execute方法,
     programClassPool.accept(classPoolvisitor);
     libraryClassPool.accept(classPoolvisitor);
     libraryClassPool.classesAccept(usageMarker);

accept就開始遍歷ClassPool赏僧,這里傳遞的classPoolvisitor參數(shù)就是上一步創(chuàng)建的NamedClassVisitor大猛,NamedClassVisitor其實只是保存了keep規(guī)則里面的類名稱而已,代碼如下:

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方法會根據(jù)類名從ClassPool里面找到對應(yīng)的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對象找到后就開始標(biāo)記這個Clazz跟它的依賴挽绩。這里的ClassVisitor就是前面創(chuàng)建的ClassAccessFilter,由于ClassAccessFilter只是用來匹配過濾下訪問權(quán)限窑滞,這里我們跳過琼牧,只分析比較核心的UsageMarker。

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

        markProgramClassBody(programClass);
    }
}

第一步哀卫,如果這個類時沒有被標(biāo)記過的話巨坊,直接標(biāo)記為USED,接著開始標(biāo)記類內(nèi)部數(shù)據(jù)此改,代碼如下:

protected void markProgramClassBody(ProgramClass programClass)
{
    //1. 標(biāo)記常量池里的本類引用
    markConstant(programClass, programClass.u2thisClass);

    //2. 標(biāo)記常量池里的父類引用
    if (programClass.u2superClass != 0)
    {
        markConstant(programClass, programClass.u2superClass);
    }

    //3. 遍歷父類也同樣做一次標(biāo)記
    programClass.hierarchyAccept(false, false, true, false,
            interfaceUsageMarker);

    //4. 標(biāo)記類的<init>方法
    programClass.methodAccept(ClassConstants.METHOD_NAME_CLINIT,
            ClassConstants.METHOD_TYPE_CLINIT,
            nonEmptyMethodUsageMarker);

    //5. 標(biāo)記類成員
    programClass.fieldsAccept(possiblyUsedMemberUsageMarker);
    programClass.methodsAccept(possiblyUsedMemberUsageMarker);

    //5. 標(biāo)記屬性表
    programClass.attributesAccept(this);
}

代碼比較簡單趾撵,通過上面調(diào)用本類相關(guān)的數(shù)據(jù)都標(biāo)記為USED了,接著是AllFieldVisitor跟AllMemberVisitor共啃,因為都是類同的邏輯占调,這里我們只分析AllMemberVisitor,

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

可以看到AllMemberVisitor本質(zhì)上就是遍歷處理類的所有方法移剪,前面已經(jīng)提到過究珊,其最終實現(xiàn)都是在UsageMarker類里,我們直接看它的visitProgramMethod實現(xiàn)纵苛,過程如下:

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);
}

也是簡單的標(biāo)記一下方法的相關(guān)數(shù)據(jù)為USED剿涮,這里有一點值得注意的就是programMethod.referencedClassesAccept(this); 這句代碼的調(diào)用,referencedClasses指向了這個方法的依賴對象攻人,但不包含方法局部變量依賴取试,怎么理解這句話呢,舉個例子怀吻,譬如有以下測試代碼

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

這里的referencedClasses僅包含了TestClass類瞬浓,而不包含TestClass1類,referencedClasses的檢索我們在上一節(jié)中以及分析過了蓬坡,這里就不再贅述了猿棉。
referencedClassesAccept方法的實現(xiàn)如下磅叛,這里的ClassVisitor就是UsageMarker,這意味著依賴類也會走相同一遍邏輯铺根,也會被標(biāo)記為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);
            }
        }
    }
}

到此我們已經(jīng)知道了proguard是如何把類宪躯、父類、字段位迂、方法以及字段方法的依賴給標(biāo)記起來了访雪,除此以外一些方法局部變量注解等也會引入類依賴,這些也得被標(biāo)記出來不能把刪除掂林,還有實現(xiàn)接口等也是一樣臣缀。
回到Shrinkerexecute方法接著往下看

    //1. 標(biāo)記接口
    programClassPool.classesAccept(new InterfaceUsageMarker(usageMarker));

    //2. 標(biāo)記內(nèi)部類 注解 方法局部變量帶進來的依賴
    programClassPool.classesAccept(
            new UsedClassFilter(usageMarker,
        new AllAttributeVisitor(true,
        new MultiAttributeVisitor(new AttributeVisitor[]
{
    new InnerUsageMarker(usageMarker),
            new AnnotationUsageMarker(usageMarker),
            new LocalVariableTypeUsageMarker(usageMarker)
}))));

InterfaceUsageMarker負責(zé)了類的實現(xiàn)接口標(biāo)記,LocalVariableTypeUsageMarker便是方法局部變量引用標(biāo)記的實現(xiàn)泻帮,這些依賴類對象都是通過讀取對應(yīng)的屬性表數(shù)據(jù)來獲取的精置,譬如還是上面的測試代碼,它的class字節(jié)碼如下


它的LocalVariableTable(方法的局部變量描述表)里就會有依賴類信息了锣杂。

壓縮

前面標(biāo)記完成后脂倦,接下來就可以開始做壓縮工作了,壓縮就是把前面標(biāo)記了USED的東西留下來元莫,其余都抹掉赖阻,從而達到減少包體積的效果。

回到Shrinkerexecute方法接著往下看踱蠢,壓縮的代碼如下:

//1. 創(chuàng)建新的ClassPool
ClassPool newProgramClassPool = new ClassPool();
//2. 創(chuàng)建ClassVisitor 壓縮ClassPool
programClassPool.classesAccept(
        new UsedClassFilter(usageMarker,
        new MultiClassVisitor(
                new ClassVisitor[] {
                new ClassShrinker(usageMarker),
                new ClassPoolFiller(newProgramClassPool)
})));
//3. 清空舊的ClassPool
programClassPool.clear();

第一步是重新創(chuàng)建了新的ClassPool火欧,用來接受壓縮后的Clazz對象數(shù)據(jù),接著是創(chuàng)建一系列的ClassVisitor負責(zé)壓縮的工作茎截,

  • UsedClassFilter 負責(zé)類級別的過濾苇侵,把沒有被標(biāo)記為USED的類給過濾掉
  • ClassShrinker 是更細粒度的過濾器,負責(zé)把Clazz內(nèi)部沒有被標(biāo)記為USED的數(shù)據(jù)過濾掉
  • ClassPoolFiller 它的任務(wù)比較簡單企锌,負責(zé)接收前面兩個過濾器篩選下來的Clazz對象榆浓,最終保存在新的ClassPool里

先看下UsedClassFilter,代碼如下

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

代碼也是非常的簡單撕攒,遍歷ClassPool陡鹃,前面沒有被標(biāo)記為USED的Clazz統(tǒng)統(tǒng)過濾掉。

接著是ClassShrinker打却,它會對Clazz結(jié)構(gòu)進行一些壓縮過濾杉适,譬如我們keep了某個方法谎倔,標(biāo)記的階段會把此方法給標(biāo)記為USED柳击,其余沒被keep的方法由于沒有被標(biāo)記為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做的事情非常的多片习,也十分的復(fù)雜捌肴,它會對class字節(jié)碼結(jié)構(gòu)里面的每一塊數(shù)據(jù)進行壓縮蹬叭,壓縮完后還得修復(fù)索引index。

這里我們只分析一下方法的壓縮過程状知,我們把測試用的demo修改為這樣

class MyClass {

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

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

把proguard keep規(guī)則修改為這樣:

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

我們只keep test1方法秽五,通過之前的標(biāo)記過程分析,test方法以及它所依賴的TestClass類都不會被標(biāo)記為USED 饥悴,最終都會被剔除掉坦喘。

在ClassShrinker開始工作之前,我們能看見此時的MyClass類方法數(shù)是3(有一個隱藏的<init>方法)西设,前面已經(jīng)提過了瓣铣,雖然test方法依賴了TestClass類,但是由于我們并沒有把test方法給keep住贷揽,最終導(dǎo)致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. 判斷常量池內(nèi)容是否被打了USED標(biāo)簽
    if (isUsed)
    {
        // Remember the new index.
        constantIndexMap[index] = counter;

        //3. 打過USED標(biāo)簽的可以保留下來
        constantPool[counter++] = constant;
    }
    else
    {
        //4. 沒打過USED標(biāo)簽的剔除
        constantIndexMap[index] = -1;
    }
}

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

實現(xiàn)也是比較簡單蓖救,有打過USED標(biāo)簽的留,沒打標(biāo)簽的就不要印屁,譬如我們的測試代碼里TestClass類肯定是沒被打標(biāo)簽的循捺,需要被剔除的


不負眾望的,跑到了下面的分支去了库车,這樣ClassShrinker就會把常量池里的TestClass類引用數(shù)據(jù)給抹掉了巨柒。

壓縮完常量池接著就是壓縮類成員了,包括了類字段集合跟類方法集合柠衍,代碼也是比較簡單洋满,下面直接給出

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標(biāo)簽珍坊,打過標(biāo)簽的會被保留下來牺勾,沒打標(biāo)簽的就直接跳過,最后也是調(diào)用Arrays.fill把集合里無用的空間賦值為空阵漏。


我們單步調(diào)試也能清楚的看見驻民,調(diào)用前方法數(shù)是3,執(zhí)行完后方法數(shù)就變成了2了履怯,集合里有一個對象被置空了回还。

最后proguard會以同樣的方式對class字節(jié)碼的各個結(jié)構(gòu)進行壓縮,因為壓縮后集合里面的數(shù)據(jù)索引index會發(fā)生變化叹洲,在壓縮完的最后階段還得做一次索引的重定向修復(fù)任務(wù)柠硕。

總結(jié)

本節(jié)主要是從源碼的角度出發(fā),簡單的分析了proguard的壓縮功能實現(xiàn)過程,主要分為兩個步驟蝗柔,其一是標(biāo)記闻葵,另外一個就是壓縮,壓縮的過程較為簡單癣丧,主要就是根據(jù)前面的標(biāo)記結(jié)果槽畔,打了USED標(biāo)簽的就保留,沒打標(biāo)簽的就會被剔除胁编。壓縮完接下來就是代碼優(yōu)化厢钧,再下一節(jié)中我會繼續(xù)給大家分析一下代碼優(yōu)化的過程。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末嬉橙,一起剝皮案震驚了整個濱河市坏快,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌憎夷,老刑警劉巖莽鸿,帶你破解...
    沈念sama閱讀 222,252評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異拾给,居然都是意外死亡祥得,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評論 3 399
  • 文/潘曉璐 我一進店門蒋得,熙熙樓的掌柜王于貴愁眉苦臉地迎上來级及,“玉大人,你說我怎么就攤上這事额衙∫梗” “怎么了?”我有些...
    開封第一講書人閱讀 168,814評論 0 361
  • 文/不壞的土叔 我叫張陵窍侧,是天一觀的道長县踢。 經(jīng)常有香客問我,道長伟件,這世上最難降的妖魔是什么硼啤? 我笑而不...
    開封第一講書人閱讀 59,869評論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮斧账,結(jié)果婚禮上谴返,老公的妹妹穿的比我還像新娘。我一直安慰自己咧织,他們只是感情好嗓袱,可當(dāng)我...
    茶點故事閱讀 68,888評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著习绢,像睡著了一般渠抹。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,475評論 1 312
  • 那天逼肯,我揣著相機與錄音,去河邊找鬼桃煎。 笑死篮幢,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的为迈。 我是一名探鬼主播三椿,決...
    沈念sama閱讀 41,010評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼葫辐!你這毒婦竟也來了搜锰?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,924評論 0 277
  • 序言:老撾萬榮一對情侶失蹤耿战,失蹤者是張志新(化名)和其女友劉穎蛋叼,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體剂陡,經(jīng)...
    沈念sama閱讀 46,469評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡狈涮,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,552評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了鸭栖。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片歌馍。...
    茶點故事閱讀 40,680評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖晕鹊,靈堂內(nèi)的尸體忽然破棺而出松却,到底是詐尸還是另有隱情,我是刑警寧澤溅话,帶...
    沈念sama閱讀 36,362評論 5 351
  • 正文 年R本政府宣布晓锻,位于F島的核電站,受9級特大地震影響飞几,放射性物質(zhì)發(fā)生泄漏带射。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,037評論 3 335
  • 文/蒙蒙 一循狰、第九天 我趴在偏房一處隱蔽的房頂上張望窟社。 院中可真熱鬧,春花似錦绪钥、人聲如沸灿里。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽匣吊。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間色鸳,已是汗流浹背社痛。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留命雀,地道東北人蒜哀。 一個月前我還...
    沈念sama閱讀 49,099評論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像吏砂,于是被迫代替她去往敵國和親撵儿。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,691評論 2 361

推薦閱讀更多精彩內(nèi)容