通過Gradle的Transform配合ASM實(shí)戰(zhàn)路由框架和統(tǒng)計(jì)方法耗時(shí)

首先乎折,現(xiàn)在世面上的項(xiàng)目基本上都是N多個(gè)module并行開發(fā)很容易就會(huì)出現(xiàn)moduleA想跳轉(zhuǎn)到moduleB某一個(gè)界面去如果你沒有把moduleB在對(duì)應(yīng)的build.gradle中配置的話凶杖,AS就會(huì)友好的提示你跳不過去媒楼,這時(shí)候就需要一個(gè)路由來分發(fā)跳轉(zhuǎn)操作了疚顷。
其次辙诞,隨著時(shí)間的慢慢迭代發(fā)現(xiàn)需求功能已經(jīng)寫完了,慢慢開始要各種優(yōu)化了线婚,常見的優(yōu)化是速度優(yōu)化自然而然就需要查看方法的耗時(shí)情況贿衍,那么解放雙手的時(shí)候就需要一個(gè)正確的姿勢(shì)來統(tǒng)計(jì)方法耗時(shí)。

附上Github項(xiàng)目地址:https://github.com/Neacy/NeacyPlugin

思路

1.采用注解(Annotation)在要跳轉(zhuǎn)的界面和需要統(tǒng)計(jì)的地方加上相對(duì)應(yīng)的協(xié)議牡直。
2.用groovy語(yǔ)言實(shí)現(xiàn)一個(gè)Transform的gradle插件來解析相對(duì)應(yīng)的注解缀匕。
3.采用ASM框架生成相對(duì)應(yīng)的代碼主要是寫入或者插入class的字節(jié)碼。
4.路由框架中需要反射拿到ASM生成的路由表然后代碼中調(diào)用從而實(shí)現(xiàn)跳轉(zhuǎn)碰逸。

==============帶著這些思路接下來就是拼命寫代碼了.............

先上兩個(gè)用到的注釋乡小,注釋還是比較簡(jiǎn)單的分分鐘寫完,需要注意的是我們是class操作所以要選@Retention(RetentionPolicy.CLASS)

/**
 * 用于標(biāo)記協(xié)議
 */
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface NeacyProtocol {
    String value();
}

/**
 * 用于標(biāo)記方法耗時(shí)
 */
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface NeacyCost {
    String value();
}

換個(gè)姿勢(shì)寫一個(gè)gradle插件饵史,如何寫主要參考區(qū)長(zhǎng)的http://blog.csdn.net/sbsujjbcy/article/details/50782830满钟,按著步驟就好,假設(shè)我們看完了并設(shè)置好了那么就有一個(gè)雛形了:

public class NeacyPlugin extends Transform implements Plugin<Project> {

    private static final String PLUGIN_NAME = "NeacyPlugin"

    private Project project

    @Override
    void apply(Project project) {
        this.project = project

        def android = project.extensions.getByType(AppExtension);
        android.registerTransform(this)
    }

    @Override
    String getName() {
        return PLUGIN_NAME
    }

    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }

    @Override
    Set<QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    @Override
    boolean isIncremental() {
        return true
    }

    @Override
    void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
        
    }
}

我們要做的就是在transform中掃描相對(duì)應(yīng)的注解并用ASM寫入class字節(jié)碼胳喷。我們知道TransformInput對(duì)應(yīng)的有兩種可能性一種是目錄 一種是jar包所以要分開遍歷:

inputs.each { TransformInput input ->
            input.directoryInputs.each { DirectoryInput directoryInput ->
                if (directoryInput.file.isDirectory()) {
                    println "==== directoryInput.file = " + directoryInput.file
                    directoryInput.file.eachFileRecurse { File file ->
                        // ...對(duì)目錄進(jìn)行插入字節(jié)碼
                    }
                }
                //處理完輸入文件之后湃番,要把輸出給下一個(gè)任務(wù)
                def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
                FileUtils.copyDirectory(directoryInput.file, dest)
            }

            input.jarInputs.each { JarInput jarInput ->
                println "------=== jarInput.file === " + jarInput.file.getAbsolutePath()
                File tempFile = null
                if (jarInput.file.getAbsolutePath().endsWith(".jar")) {
                    // ...對(duì)jar進(jìn)行插入字節(jié)碼
                }
                /**
                 * 重名輸出文件,因?yàn)榭赡芡?會(huì)覆蓋
                 */
                def jarName = jarInput.name
                def md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
                if (jarName.endsWith(".jar")) {
                    jarName = jarName.substring(0, jarName.length() - 4)
                }
                //處理jar進(jìn)行字節(jié)碼注入處理
                def dest = outputProvider.getContentLocation(jarName + md5Name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
                FileUtils.copyFile(jarInput.file, dest)
            }
        }

對(duì)于代碼中陌生的代碼風(fēng)格可以查閱這篇文章:http://blog.csdn.net/innost/article/details/48228651保證看完之后什么都懂了,好文強(qiáng)烈推薦吭露。

然后吠撮,最麻煩的就是字節(jié)碼注入的部分功能了,先看一下主要的調(diào)用代碼:

ClassReader classReader = new ClassReader(file.bytes)
                            ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                            NeacyAsmVisitor classVisitor = new NeacyAsmVisitor(Opcodes.ASM5, classWriter)
                            classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)

調(diào)用的主要代碼量還是比較少的讲竿,主要是自定義一個(gè)ClassVisitor纬向。在每一個(gè)ClassVisitor中它會(huì)分別visitAnnotationvisitMethod

    @Override
    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
        NeacyLog.log("=====---------- NeacyAsmVisitor visitAnnotation ----------=====");
        NeacyLog.log("=== visitAnnotation.desc === " + desc);
        AnnotationVisitor annotationVisitor = super.visitAnnotation(desc, visible);

        if (Type.getDescriptor(NeacyProtocol.class).equals(desc)) {// 如果注解不為空的話
            mProtocolAnnotation = new NeacyAnnotationVisitor(Opcodes.ASM5, annotationVisitor, desc);
            return mProtocolAnnotation;
        }
        return annotationVisitor;
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        NeacyLog.log("=====---------- visitMethod ----------=====");
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
        mMethodVisitor = new NeacyMethodVisitor(Opcodes.ASM5, mv, access, name, desc);
        return mMethodVisitor;
    }

visitAnnotation中就是我們掃描相對(duì)應(yīng)的注解的地方類似Type.getDescriptor(NeacyProtocol.class).equals(desc)判斷是否是我們需要的處理的注解,像這里我們主要處理前面定義好的注解NeacyProtocolNeacyCost兩個(gè)注解就好戴卜。

這里我要展示一下注入成功之后的class中的代碼是什么模樣:
生成好的路由表:

這里寫圖片描述

注入成功的耗時(shí)代碼:


這里寫圖片描述

看一眼logcat打印出來的耗時(shí)時(shí)間逾条,感覺離成功不遠(yuǎn)了⊥栋可是是怎么注入的呢师脂,首先要看一眼class結(jié)構(gòu) 這里推薦使用IntelliJ IDEA然后裝個(gè)插件叫Bytecode outline這里距離看一眼耗時(shí)的生成的class文件字節(jié)碼。


這里寫圖片描述

左邊是我們對(duì)應(yīng)的java文件,右邊是編譯之后生成的class字節(jié)碼吃警。對(duì)于右邊一般是看不懂的但是神奇的ASM就能看的懂而且提供了一系列的api供我們調(diào)用糕篇,我們只要對(duì)著編寫就好了,按照上面的操作很大程度上減少了巨大的工作難度酌心,再次感謝巴掌大神拌消。

所以我們路由框架的代碼字節(jié)生成,我把整個(gè)類貼上來吧代碼量不是很多:

/**
 * 生成路由class文件
 */
public class NeacyRouterWriter implements Opcodes {

    public byte[] generateClass(String pkg, HashMap<String, String> metas) {

        ClassWriter cw = new ClassWriter(0);
        FieldVisitor fv;
        MethodVisitor mv;
        // 生成class類標(biāo)識(shí)
        cw.visit(Opcodes.V1_7, Opcodes.ACC_PUBLIC + Opcodes.ACC_SUPER, pkg, null, "java/lang/Object", null);
        
        // 聲明一個(gè)靜態(tài)變量
        fv = cw.visitField(Opcodes.ACC_PUBLIC + Opcodes.ACC_STATIC, "map", "Ljava/util/HashMap;", "Ljava/util/HashMap<Ljava/lang/String;Ljava/lang/String;>;", null);
        fv.visitEnd();
        
        // 默認(rèn)的構(gòu)造函數(shù)<init>
        mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
        mv.visitCode();
        mv.visitVarInsn(Opcodes.ALOAD, 0);
        mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
        mv.visitInsn(Opcodes.RETURN);
        mv.visitMaxs(1, 1);

        // 生成一個(gè)getMap方法
        mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "getMap", "()Ljava/util/HashMap;", "()Ljava/util/HashMap<Ljava/lang/String;Ljava/lang/String;>;", null);
        mv.visitCode();
        mv.visitFieldInsn(Opcodes.GETSTATIC, pkg, "map", "Ljava/util/HashMap;");
        mv.visitInsn(Opcodes.ARETURN);
        mv.visitMaxs(1, 1);
        mv.visitEnd();

        // 將掃描到的注解生成相對(duì)應(yīng)的路由表 主要寫在靜態(tài)代碼塊中
        mv = cw.visitMethod(Opcodes.ACC_STATIC, "<clinit>", "()V", null, null);
        mv.visitCode();
        mv.visitTypeInsn(Opcodes.NEW, "java/util/HashMap");
        mv.visitInsn(Opcodes.DUP);
        mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/util/HashMap", "<init>", "()V", false);
        mv.visitFieldInsn(Opcodes.PUTSTATIC, pkg, "map", "Ljava/util/HashMap;");

        for (Map.Entry<String, String> entrySet : metas.entrySet()) {
            String key = entrySet.getKey();
            String value = entrySet.getValue();
            NeacyLog.log("=== key === " + key);
            NeacyLog.log("=== value === " + value);
            mv.visitFieldInsn(Opcodes.GETSTATIC, pkg, "map", "Ljava/util/HashMap;");
            mv.visitLdcInsn(key);
            mv.visitLdcInsn(value);
            mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/util/HashMap", "put", "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", false);
            mv.visitInsn(Opcodes.POP);
        }
        mv.visitInsn(Opcodes.RETURN);
        mv.visitMaxs(3, 0);
        mv.visitEnd();

        cw.visitEnd();
       
        return cw.toByteArray();
    }
}

然后對(duì)方法耗時(shí)的進(jìn)行的代碼插入主要代碼有:

    @Override
    protected void onMethodEnter() {
        if (isInject) {
            NeacyLog.log("====== 開始插入方法 = " + methodName);

            /** 
            NeacyCostManager.addStartTime("xxxx", System.currentTimeMillis());
            */
            mv.visitLdcInsn(methodName);
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "neacy/router/NeacyCostManager", "addStartTime", "(Ljava/lang/String;J)V", false);
        }
    }

    @Override
    protected void onMethodExit(int opcode) {
        if (isInject) {
            /** 
            NeacyCostManager.addEndTime("xxxx", System.currentTimeMillis());
            */
            mv.visitLdcInsn(methodName);
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "neacy/router/NeacyCostManager", "addEndTime", "(Ljava/lang/String;J)V", false);

            /**
             NeacyCostManager.startCost("xxxx");
            */
            mv.visitLdcInsn(methodName);
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "neacy/router/NeacyCostManager", "startCost", "(Ljava/lang/String;)V", false);

            NeacyLog.log("==== 插入結(jié)束 ====");
        }
    }

基本上這樣子相對(duì)應(yīng)的路由表相對(duì)應(yīng)的代碼插入都寫完安券,然后只需要在gradle插件中進(jìn)行調(diào)用一下即可墩崩,而對(duì)于遍歷目錄的時(shí)候沒有什么難點(diǎn)就是直接覆蓋當(dāng)前class即可:

               if (isDebug) {// 只有Debug才進(jìn)行掃描const耗時(shí)
                                // 掃描耗時(shí)注解 NeacyCost
                                byte[] bytes = classWriter.toByteArray()
                                File destFile = new File(file.parentFile.absoluteFile, name)
                                project.logger.debug "========== 重新寫入的位置->lastFilePath = " + destFile.getAbsolutePath()
                                FileOutputStream fileOutputStream = new FileOutputStream(destFile)
                                fileOutputStream.write(bytes)
                                fileOutputStream.close()
                            }

而對(duì)于jar遍歷的時(shí)候需要做的是先拆jar然后注入代碼完成之后需要再生產(chǎn)一個(gè)jar,所以我們需要?jiǎng)?chuàng)建一個(gè)臨時(shí)地址來存放新的jar侯勉。

                    if (isDebug) {
                        // 將jar包解壓后重新打包的路徑
                        tempFile = new File(jarInput.file.getParent() + File.separator + "neacy_const.jar")
                        if (tempFile.exists()) {
                            tempFile.delete()
                        }
                        fos = new FileOutputStream(tempFile)
                        jarOutputStream = new JarOutputStream(fos)
                        
                        // 省略一些代碼....
                        
ZipEntry zipEntry = new ZipEntry(entryName)
                                jarOutputStream.putNextEntry(zipEntry)
                                // 掃描耗時(shí)注解 NeacyCost
                                byte[] bytes = classWriter.toByteArray()
                                jarOutputStream.write(bytes)
                    }

這里有必要插入一個(gè)插件配置鹦筹,因?yàn)閷?duì)于方法耗時(shí)統(tǒng)計(jì)只要開發(fā)的時(shí)候debug模式下使用就好其他模式禁止使用了,這就是為什么上面有if(debugOn)的判斷址貌。
先定義一個(gè)Extension:

/**
 * 配置
 */
public class NeacyExtension {
    boolean debugOn = true

    public NeacyExtension(Project project) {

    }
}

然后在transfrom中進(jìn)行讀阮砉铡:

    void apply(Project project) {
        this.project = project
        project.extensions.create("neacy", NeacyExtension, project)

        def android = project.extensions.getByType(AppExtension);
        android.registerTransform(this)


        project.afterEvaluate {
            def extension = project.extensions.findByName("neacy") as NeacyExtension
            def debugOn = extension.debugOn

            project.logger.error '========= debugOn = ' + debugOn

            project.android.applicationVariants.each { varient ->
                project.logger.error '======== varient Name = ' + varient.name
                if (varient.name.contains(DEBUG) && debugOn) {
                    isDebug = true
                }
            }
        }
    }

最后在build.gradle中進(jìn)行配置就可以愉快的使用了..

apply plugin: com.neacy.plugin.NeacyPlugin
neacy {
    debugOn true
}

當(dāng)然更多的代碼可以參考demo的git庫(kù)了解更多。

最后路由庫(kù)要怎么讓代碼調(diào)用呢练对,這就是前面講到的反射因?yàn)槭蔷幾g生成的class無法直接調(diào)用唯有反射大法遍蟋,反射會(huì)稍微影響性能所以我們一開始就直接做好這些初始化工作就可以了。

    /**
     * 初始化路由
     */
    public void initRouter() {
        try {
            Class clazz = Class.forName("com.neacy.router.NeacyProtocolManager");
            Object newInstance = clazz.newInstance();
            Field field = clazz.getField("map");
            field.setAccessible(true);
            HashMap<String, String> temps = (HashMap<String, String>) field.get(newInstance);
            if (temps != null && !temps.isEmpty()) {
                mRouters.putAll(temps);
                Log.w("Jayuchou", "=== mRouters.Size === " + mRouters.size());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 根據(jù)協(xié)議找尋路由實(shí)現(xiàn)跳轉(zhuǎn)
     */
    public void startIntent(Context context, String protocol, Bundle bundle) {
        if (TextUtils.isEmpty(protocol)) return;
        String protocolValue = mRouters.get(protocol);
        try {
            Class destClass = Class.forName(protocolValue);
            Intent intent = new Intent(context, destClass);
            if (bundle != null) {
                intent.putExtras(bundle);
            }
            context.startActivity(intent);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

最最最后螟凭,怎么使用呢匿值?

@NeacyProtocol("Neacy://app/MainActivity")
public class MainActivity extends AppCompatActivity {

    @Override
    @NeacyCost("MainActivity.onCreate")
    protected void onCreate(Bundle savedInstanceState) {

根據(jù)上面的注解標(biāo)識(shí)之后,方法耗時(shí)就已經(jīng)完成當(dāng)然路由還需要哪里需要哪里傳協(xié)議進(jìn)行跳轉(zhuǎn)就好了赂摆,當(dāng)然也是一句代碼的事挟憔。

NeacyRouterManager.getInstance().startIntent(TestActivity.this, "Neacy://neacymodule/NeacyModuleActivity", bundle);

這樣一個(gè)完整的路由框架以及方法耗時(shí)統(tǒng)計(jì)V1.0版本就打完收工了。

Thanks............
感謝巴神的文章:http://www.wangyuwei.me/2017/03/05/ASM實(shí)戰(zhàn)統(tǒng)計(jì)方法耗時(shí)/#more

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末烟号,一起剝皮案震驚了整個(gè)濱河市绊谭,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌汪拥,老刑警劉巖达传,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異迫筑,居然都是意外死亡宪赶,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門脯燃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來搂妻,“玉大人,你說我怎么就攤上這事辕棚∮鳎” “怎么了邓厕?”我有些...
    開封第一講書人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)扁瓢。 經(jīng)常有香客問我详恼,道長(zhǎng),這世上最難降的妖魔是什么引几? 我笑而不...
    開封第一講書人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任昧互,我火速辦了婚禮,結(jié)果婚禮上伟桅,老公的妹妹穿的比我還像新娘敞掘。我一直安慰自己,他們只是感情好贿讹,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開白布渐逃。 她就那樣靜靜地躺著够掠,像睡著了一般民褂。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上疯潭,一...
    開封第一講書人閱讀 51,631評(píng)論 1 305
  • 那天赊堪,我揣著相機(jī)與錄音,去河邊找鬼竖哩。 笑死哭廉,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的相叁。 我是一名探鬼主播遵绰,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼增淹!你這毒婦竟也來了椿访?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤虑润,失蹤者是張志新(化名)和其女友劉穎成玫,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體拳喻,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡哭当,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了冗澈。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片钦勘。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖亚亲,靈堂內(nèi)的尸體忽然破棺而出个盆,到底是詐尸還是另有隱情脖岛,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布颊亮,位于F島的核電站柴梆,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏终惑。R本人自食惡果不足惜绍在,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望雹有。 院中可真熱鬧偿渡,春花似錦、人聲如沸霸奕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)质帅。三九已至适揉,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間煤惩,已是汗流浹背嫉嘀。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留魄揉,地道東北人剪侮。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像洛退,于是被迫代替她去往敵國(guó)和親瓣俯。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

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

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,162評(píng)論 25 707
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,822評(píng)論 6 342
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理兵怯,服務(wù)發(fā)現(xiàn)彩匕,斷路器,智...
    卡卡羅2017閱讀 134,659評(píng)論 18 139
  • 先上demo地址:https://github.com/JeasonWong/CostTime 需求 實(shí)際業(yè)務(wù)開發(fā)...
    神來一巴掌閱讀 5,216評(píng)論 10 9
  • 準(zhǔn)備好基本的知識(shí)之后正式參與百人計(jì)劃摇零,與其說是組織的推掸,不如說是自己對(duì)自己的一個(gè)要求計(jì)劃的啟動(dòng)。一直以來驻仅,做一...
    肖聖欽閱讀 277評(píng)論 2 5