嗶哩嗶哩Android項目編譯優(yōu)化
背景
嗶哩嗶哩的安卓項目的工程結(jié)構(gòu)是Monorepo(單倉)變種,也就是所有的代碼都在一個工程結(jié)構(gòu)下編譯。我們認為Monorepo(單倉)是一個非常適合我們的開發(fā)模式,主要是因為其提供的原子提交,可見性,參與度,切片的穩(wěn)定性等等優(yōu)點,這些都是我們選擇Monorepo的原因。但是因為權(quán)限管控,ijkplayer等雙端通用工程的原因,還是拆開了多個git倉庫。通過git權(quán)限的方式來拆分了工程結(jié)構(gòu),然后通過Gradle Plugin的形式進行了多工程的組合,在CI打包環(huán)境上讓工程具備Monorepo的能力。
隨著代碼長時間迭代,業(yè)務(wù)模塊數(shù)量增多,當(dāng)前工程有500+的模塊以及19個復(fù)合構(gòu)建。如果所有的模塊都用源代碼編譯,打一個包本地可能需要大概30分鐘的時間才能完成編譯。而且嗶哩嗶哩的安卓工程是一個上百人同時開發(fā)的項目,如果一小個改動都需要30分鐘的時間投入編譯中,對于開發(fā)同學(xué)來說可能心態(tài)都要崩了。
下圖是工程在CI上全量編譯的情況下,編譯耗時大概是16.6分鐘,打包機的性能是優(yōu)于本地電腦的,所以速度會更快一點。
讓編譯速度變得更快也就迫在眉睫,而且這個模式是針對開發(fā)同學(xué),讓他們可以快速對模塊變更的代碼負責(zé)。同時也希望這個模式是在不影響當(dāng)前的工程結(jié)構(gòu),讓他們的打包速度能變得更快。
編譯優(yōu)化
我們通過添加--scan參數(shù),觀察了全源碼情況下的編譯流程。在沒有編譯緩存的情況下,每個模塊都是源代碼,所以都需要執(zhí)行將源代碼編譯成字節(jié)碼的過程。同時還需要完成所有工程的依賴配置等等展開,這些過程都是比較耗時的。而工程編譯的大部分耗時都集中在將源碼編譯字節(jié)碼產(chǎn)物的過程中,也就是Gradle Task階段。下圖就是我從buildScan中找出來耗時相對較長的Task任務(wù),可以看出來有的編譯任務(wù)的耗時在2min以上。
如果能將模塊直接變更成aar產(chǎn)物,那么就可以跳過這些模塊的編譯任務(wù),直接使用他們的二進制產(chǎn)物。但是直接使用aar產(chǎn)物是有風(fēng)險的,會犧牲一部分代碼的正確性,同時代碼可能會有滯后性。另外編譯階段會進行很多語法校驗的操作,而直接更換成aar產(chǎn)物了就會跳過這部分檢查。
而我們快編的做法就是犧牲一部分代碼正確性,將沒有變更的源代碼變成aar產(chǎn)物,在編譯階段直接采用aar產(chǎn)物進行編譯,這樣就可以跳過部分源代碼編譯環(huán)節(jié)。但是因為跳過了源代碼編譯階段,所以有一部分編譯時的常量優(yōu)化,方法簽名變更或者其他問題就可能會出現(xiàn)。
在快編模式下編譯的平均耗時優(yōu)化到了6min左右。因為ci的特性,每次都需要清空文件夾之后重新clone工程,這樣會有額外的工程準備時間。另外在變更的模塊比較少的情況下,編譯速度則會更快一點。
另外這些問題使用快編的人是否可以接受?我們目的主要是優(yōu)化下開發(fā)編譯速度,能讓他們可以更快的出包,更快的調(diào)試代碼,所以我們認為這種取舍還是合理的。
如何可以及時的發(fā)現(xiàn)這些方法簽名變更的問題呢?下面讓我們慢慢展開我們的編譯優(yōu)化方案。
工作流程
我們要先從gradle build的生命周期開始展開了,可以分為三個大流程,一個是初始化階段,一個是配置階段,另外一個是task執(zhí)行階段。一般配置階段會加載完成工程的依賴關(guān)系以及配置信息等工作。
我們會在生命周期的不同階段,執(zhí)行一些的代碼,然后篡改一些編譯相關(guān)的屬性,從而做到源代碼和aar產(chǎn)物之間的替換操作。下面是我列出來我們的項目的快編的工作流程。
andruid是當(dāng)前的工程名,因為要做多個業(yè)務(wù)之間的代碼隔離,所以我們還是把單個倉庫拆分成了多個工程結(jié)構(gòu),如果一個有所有倉庫權(quán)限的人或者CI機器可以獲取到所有代碼倉庫的權(quán)限,并全源碼打包。
babel commit 是負責(zé)來存放這些倉庫的穩(wěn)定切片信息,會在當(dāng)前分支push到遠端之后pipeline完成之后生成,然后以git commit的形式存放在andruid工程下。當(dāng)沒有對應(yīng)倉庫代碼權(quán)限則會使用babel commit內(nèi)存放的version版本信息。
- Gradle 配置階段之前先根據(jù)緩存信息獲取所有子倉切片
- 同步多個業(yè)務(wù)倉庫配置
- 通過遍歷展開文件樹,生成工程對應(yīng)的數(shù)據(jù)結(jié)構(gòu)
- 基于當(dāng)前模塊的commit來生成version,嘗試下載aar
- 基于下載結(jié)果選擇使用源碼還是aar進行編譯
快編插件
上面我們講完了大體的流程是如何的,下面我會拋出一些問題,就以下幾個問題來看我們是如何解決的:
如何確保代碼相對來說的準確性呢?以什么來作為唯一標識符?
如何在gradle的配置階段就完成源碼到aar的替換呢?
當(dāng)前的aar產(chǎn)物出現(xiàn)了方法簽名變更的情況,我們有沒有辦法快速的刷新所有的緩存呢?
有沒有辦法讓同步流程也變得更快?
在什么情況下誰來生成上傳這個aar產(chǎn)物?
獲取工程樹結(jié)構(gòu)
我們要在settings.gradle執(zhí)行的階段就先獲取到當(dāng)前目錄下有多少個工程,然后我們才能基于這個工程數(shù)據(jù)結(jié)構(gòu),先去嘗試下載每個模塊的遠端aar產(chǎn)物,如果無法下載到該模塊,則意味著模塊已經(jīng)出現(xiàn)了變更。然后在gradle配置階段之前進行替換操作替換掉能下載到aar的模塊。
這里我們需要先定義出一個數(shù)據(jù)結(jié)構(gòu)來負責(zé)存放我們所需要收集的模塊編譯信息。先盤點下我們所要收集的數(shù)據(jù)結(jié)構(gòu)Project,Project 數(shù)據(jù)結(jié)構(gòu)如下表格所示。后面我們會多次使用到這個數(shù)據(jù)結(jié)構(gòu),通過改變其中的值屬性來變更我們的編譯流程。
字段名 | 含義 |
dir | 文件路徑 |
group | 組名 |
name | 模塊名 |
version | 版本號 |
change | 是否變更 |
a8Change | 方法簽名是否變更 |
maven.yaml負責(zé)存放當(dāng)前工程的group+name 信息
數(shù)據(jù)結(jié)構(gòu)的生成規(guī)則如下,我們會以當(dāng)前文件夾作為根節(jié)點,之后遍歷展開當(dāng)前的文件樹形結(jié)構(gòu),每當(dāng)檢測到一個文件夾下面同時含有build.gralde和一個maven.yaml的文件的情況下,我們就會生成一個Project的數(shù)據(jù)結(jié)構(gòu),之后加入列表中。
當(dāng)這次文件夾遍歷操作完成之后,我們就會得到當(dāng)前工程下有多少個模塊,然后他們的group+name是什么,另外通過計算出緩存的version版本是多少。通過這些信息來幫助我們?nèi)ネ瓿晌覀兊目炀庍壿?。但是?dāng)前這個操作需要耗費大概1分鐘的時間。
version版本
我們沒有按照Gradle標準的1.0.0這種版本方式來定義模塊版本,這種方式很難和當(dāng)前的代碼變更結(jié)合到一起,而且需要一套全局version來進行版本管理。另外也很難達到準確表達當(dāng)前分支下的真實緩存情況。
為什么要用commit的sha這個作為版本號呢?因為大倉是基于一個穩(wěn)定切片的編程模式。既然切片是穩(wěn)定的情況下,那么也就是當(dāng)前的每個Project的commit提交也都是穩(wěn)定的。
我們通過git指令去獲取到當(dāng)前的Project數(shù)據(jù)結(jié)構(gòu)對應(yīng)的文件夾下的commit的sha值,一定是每一個Project下面的最接近的commit。然后根據(jù)上面的加鹽規(guī)則來生成這個版本號,之后作為數(shù)據(jù)結(jié)構(gòu)的一部分。
static def getGitSha(String file) { def text = "git rev-parse --show-toplevel".execute(null, new File(file)).text.trim() if (text.length() == 0) { return "" } def releaPath = file.replace(text + "/", "") def cmd = 'git log --max-count=1 --pretty=%H ' if (releaPath.length() > 0) { cmd = cmd + " " + releaPath } def sha = cmd.execute(null, new File(text)).text.trim() if (sha.startsWith("HEAD")) { return "" } if (sha.startsWith("fatal:")) { return "" } return sha }
當(dāng)前的aar產(chǎn)物出現(xiàn)了方法簽名變更的情況下,我們有沒有辦法快速的刷新所有的緩存呢?
所以我們引入了一個鹽值,然后將這個鹽值和sha結(jié)合到一起作為當(dāng)前倉庫的真實的version。當(dāng)我們的鹽值發(fā)生變化就會導(dǎo)致當(dāng)前所有的緩存失效,然后就會觸發(fā)重新生成新的version版本了。所以只要修改鹽值的值就可以將所有aar的版本進行統(tǒng)一的升級。
還需要對variant也加入version版本生成的邏輯中。不同的變種的代碼也都是不同的,需要區(qū)分變種來選擇下載不同的aar版本。
源碼orAAR
等完成上述步驟之后,我們的工程的快編的前置工作就準備的差不多了,接下來就是嘗試性去下載這個version版本的aar。如果這個版本存在,意味著當(dāng)前倉庫并沒有發(fā)生實際的變更,可以用該產(chǎn)物直接替換掉當(dāng)前的源代碼。
如果當(dāng)前的version的版本無法下載,可以認為這個commit在遠端aar并不存在,之后我們就可以將Project標記為已經(jīng)發(fā)生變更,然后它將直接使用源碼編譯。
這里還有些邊界條件,如果當(dāng)前的變更沒有提交的情況下,我們需要獲取當(dāng)前的git項目的變更內(nèi)容,然后基于文件路徑匹配到對應(yīng)的Project,將它切換到源碼編譯的情況下,獲取變更文件路徑代碼如下:
static List<String> getAllChangeFile(File file) { def text = "git status -u -s".execute(null, file).text String[] txts = text.split("\n") List<String> result = new ArrayList<>() txts.each { String item -> if (item.length() > 3) { result.add(item.substring(3)) } } return result }
主動Skip模塊
完成上述幾步之后,工程結(jié)構(gòu)就已經(jīng)是一個很清晰的狀態(tài)了,我們已經(jīng)知道當(dāng)前工程有多少個模塊,哪些模塊發(fā)生了變更哪些模塊沒有變更,另外沒有變更模塊的aar version版本,同時也拿到了下載的產(chǎn)物。
接下來我們需要做的操作是快編里面非常重要的,將當(dāng)前的沒有變更的源代碼移除。這樣工程所剩下來的就是發(fā)生了變更的模塊,通過這種方式就可以編譯最少的變更模塊了,從而縮短打包流程和避免無變更的模塊配置時間。
工程內(nèi)含有10+的復(fù)合構(gòu)建(Composing builds),所以這里我們需要支持兩個不同的場景,一個是當(dāng)前當(dāng)前工程下的settings.gradle下的include模塊進行移除,還有一個就是移除沒有變更的includebuilding工程。
摘自 Gradle 文檔:復(fù)合構(gòu)建只是包含其他構(gòu)建的構(gòu)建。在許多方面,復(fù)合構(gòu)建類似于 Gradle 多項目構(gòu)建,不同之處在于,它包括完整的 builds ,而不是包含單個 projects。
組合通常獨立開發(fā)的構(gòu)建,例如,在應(yīng)用程序使用的庫中嘗試錯誤修復(fù)時,將大型的多項目構(gòu)建分解為更小,更孤立的塊,可以根據(jù)需要獨立或一起工作。
復(fù)合構(gòu)建的屬性都是存放在Gradle Settings內(nèi)的IncludedBuildSpec屬性。通過groovy語言的動態(tài)屬性,獲取到DefaultSettings下的getIncludedBuilds方法的返回值,從而獲取到當(dāng)前所有的復(fù)合構(gòu)建,然后根據(jù)IncludedBuildSpec的rootDir路徑來決定哪些復(fù)合構(gòu)建是不需要參與當(dāng)前編譯的。
還有就是Settings下的模塊通過調(diào)用settings.rootProject.children.clear(),include對應(yīng)的是Gradle Settings下的ProjectDescriptor,我們將所有的原始模塊數(shù)據(jù)的清空,然后基于Project數(shù)據(jù)結(jié)構(gòu)將變更的Project的文件路徑來插入列表,重新生成新的ProjectDescriptor。
Configuration策略
配置階段最后就是要將項目內(nèi)的依賴版本更換到我們當(dāng)前Project數(shù)據(jù)結(jié)構(gòu)內(nèi)的version版本上去了。我們項目內(nèi)的依賴版本只是起到一個占位的作用,全量編譯的時候源代碼,快速編譯的情況下就需要替換成對應(yīng)版本的aar產(chǎn)物。
gradle在配置階段后期,Configuration提供一個ResolutionStrategy策略,讓我們主動的來篩選更改所需要的遠端依賴庫的版本。
我們可以基于Project中的group,moduleName,version,然后通過ResolutionStrategy來進行版本替換,簡單的說就是當(dāng)group+name的組合符合的情況下替換成我們計算出來的version,之后再重新指向這個新版本就可以了。
eachDependency { DependencyResolveDetails details -> Logger.debug("requested " + requested.group + ":" + requested.name + ":" + requested.version) Logger.debug("target " + details.getTarget()) def targetInfo = details.getTarget().toString().split(":") String sVersion = compare.select(targetInfo[0], targetInfo[1], targetInfo[2]) if (sVersion != targetInfo[2]) { def tar = targetInfo[0] + ":" + targetInfo[1] + ":" + sVersion Logger.debug("git select " + tar) details.useTarget(tar) details.because(" git flow ") } Logger.debug("target new " + details.getTarget()) }
遠端upload
那么是由誰來執(zhí)行這個aar版本的發(fā)布任務(wù)的呢?
當(dāng)我們的每個commit push提交到遠端時,gitlab的ci都會執(zhí)行一些pipeline任務(wù)。這里我們加了一個發(fā)布任務(wù),將所有變更的模塊發(fā)布到遠端。
這部分和之前的version計算相同,先計算出工程所有的commit version的aar,然后嘗試下載,之后將下載不到的定義為變更模塊,最后將這部分發(fā)布到遠端。upload階段就是通過maven-publish插件,發(fā)布aar到自定義遠端地址,當(dāng)然內(nèi)部有些小改動這里就不多展開了。
另外我們還進行了一些小小的優(yōu)化調(diào)整,區(qū)分當(dāng)前到底是idea同步還是編譯任務(wù),只在編譯階段插入了maven-publish插件。這樣略微加快了一點點項目的同步速度。
R8 class check
當(dāng)把一大部分模塊切換到aar產(chǎn)物后,有可能會發(fā)生方法簽名常量優(yōu)化等變更不同步的問題。這里我們就需要一種手段來檢測當(dāng)前apk中是否有一些危險的方法調(diào)用,然后讓這些可能會崩潰的地方在代碼合入之前就暴露出來。
我們在Android R8的基礎(chǔ)上,開發(fā)了一套專門針對于方法簽名檢查的任務(wù),我們叫做A8檢查。我們先來簡單的了解下R8的一些小知識。
正常情況下混淆可以拿來壓縮代碼體積,其中包括刪除無效代碼等等。代碼縮減(也稱為“搖樹優(yōu)化”)是指移除 R8 確定在運行時不需要的代碼的過程。此過程可以大大減小應(yīng)用的大小,例如,當(dāng)您的應(yīng)用包含許多庫依賴項,但只使用它們的一小部分功能時。
也就是在R8進行代碼壓縮的過程中,其實就已經(jīng)包含有所有語法樹信息了。我們?nèi)绻妙愃频臋C制,就可以獲取到對應(yīng)的方法簽名,然后通過遍歷循環(huán)之后檢測出是不是當(dāng)前類方法簽名存在問題。
public static void run(A8Command command) throws Throwable { AndroidApp app = command.getInputApp(); InternalOptions options = command.getInternalOptions(); ExecutorService executor = ThreadUtils.getExecutorService(options); new A8(options, command.forceReflectionError).run(app, executor, false); }
A8其中AndroidApp和R8內(nèi)的實現(xiàn)是一樣的,會根據(jù)當(dāng)前的Android Sdk版本以及需要檢查的文件先去生成AppView,然后我們基于這個AppView內(nèi)的appView.appInfo().classes(),之后遍歷展開,判斷當(dāng)前apk中是否含有一些非法的函數(shù)調(diào)用。
這種檢測方式的可以完全模擬出apk實際安裝情況下的一些函數(shù)調(diào)用情況,包括android源代碼內(nèi)的。一般的字節(jié)碼訪問之后生成的函數(shù)調(diào)用信息是難以模擬出安卓源代碼內(nèi)的api的,而且一些lambda因為脫糖后置,可能也會產(chǎn)生一系列的問題。
而且R8作為一個apk方法優(yōu)化工具,他原始提供的功能會比我們自己寫的更合理和靠譜,同時R8階段中已經(jīng)完成了脫糖,并轉(zhuǎn)化成了dex。所以我們認為可靠性更高,這個也就是我們快編的最后一道屏障。
我們會將這個屏障設(shè)置在gitlab ci的pipeline中,如果A8檢查沒有通過代碼是不允許被合入的。
Faster
在快編的基礎(chǔ)上,我們還是希望工程能有更快的編譯速度。我們當(dāng)前采用了云端編譯,獨立的編譯單元以及后續(xù)打算在編譯單元的基礎(chǔ)上構(gòu)造獨立的application殼工程來優(yōu)化我們的編譯速度。
在內(nèi)部碰撞的過程中,有過一些對于編譯優(yōu)化的設(shè)想,可以將選擇權(quán)交還給開發(fā)同學(xué),讓他們主動來選擇當(dāng)前的開發(fā)模式,是只對自己當(dāng)前的模塊負責(zé),還是一些全局性的改動。在不同的模式下他們可以選擇打開不同的工作路徑,更快速的切換開發(fā)模式。
當(dāng)前工程有大量的復(fù)合構(gòu)建邏輯,而復(fù)合構(gòu)建就是將多個本來完全獨立的工程結(jié)構(gòu)進行組合編譯。每個業(yè)務(wù)都是一個獨立的Gradle Project,都具有一個settings.gradle文件,所以他們天生就具有獨立打開以及編譯的條件,但是因為內(nèi)部依賴是源代碼,還是需要組合多個復(fù)合構(gòu)建。在這個前提的基礎(chǔ)上,也就誕生了在當(dāng)前模式下更獨立的編譯單元。
云編譯
云編譯的工作就是將本地的所有變更內(nèi)容和上一個babel commit進行計算,之后將變更的內(nèi)容傳輸?shù)竭h端打包機,然后依托于遠端的編譯緩存和遠端更牛的機器進行打包工作,等打包完成之后再將apk傳輸回本地之后安裝到手機中。
云編譯系統(tǒng)則不同于ci,可以保留當(dāng)前的build cache。平均的打包速度會更加快一點,大概是3min左右。另外一個優(yōu)化點是獲取工程結(jié)構(gòu)能緩存的話,當(dāng)分支沒有變化的情況下直接使用緩存數(shù)據(jù),優(yōu)化完這部分應(yīng)該能更快。
云編譯的情況下平均的編譯速度會比快編更加快一點,大概是3min。
獨立的編譯單元
當(dāng)全源代碼展開的時候,工程的同步速度會變得非常的慢,當(dāng)前工程有500+的模塊以及19個復(fù)合構(gòu)建,一次同步大概需要消耗半個小時左右的時間。并且當(dāng)切換分支都需要重新同步工程,這個也是當(dāng)前迫切需要解決的問題。對比了下單倉和多倉的優(yōu)劣,多倉模式下的一個優(yōu)勢就是工程依賴都是aar依賴,工程可以獨立編譯并運行起來。這也是單倉所缺少的能力,那么有沒有辦法將單倉也具有類似的能力呢?
基于前面的快編原理,可以挑選出當(dāng)前切片下所依賴的模塊的aar產(chǎn)物。接下來我們要做的就是讓每一個業(yè)務(wù)或者sdk等等具有可以獨立打開的能力就行了。
另外我們的工程結(jié)構(gòu)上也將基礎(chǔ)層,中間層等都進行了獨立拆分,一個業(yè)務(wù)模塊 + common + framework 就是一個完全可以獨立編的工作單元。
所以只需要把快編插件添加點簡單的邏輯,生成一個子模板的插件。之后將插件引入到這個獨立工程的settings.gradle下。為了不影響?yīng)毩⒐こ痰倪壿?,這個插件只有在當(dāng)前工程作為root節(jié)點的情況下生效。
if (gradle.parent == null) { apply plugin: "fawkes.fast.build.sub.settings" }
在這個工程目錄下的添加一個saints_row的文件,之后在插件內(nèi)反序列化數(shù)據(jù)結(jié)構(gòu),基于這個結(jié)構(gòu)來決定后續(xù)的編譯單元屬性,展開的目錄結(jié)構(gòu),以及編譯模式等等。
info: - path: "framework" src: false - path: "common" src: true mode: "aarOnly"
其中我們可以通過src來任意關(guān)閉其中的一個工程進行重新同步索引,工程關(guān)閉情況下依賴會被替換成快編的aar version。然后我們也設(shè)立了三種完全不同的模式,來輔助業(yè)務(wù)同學(xué)進行日常的開發(fā)工作。
- normal 基礎(chǔ)模式 全部源代碼展開。
- aarOnly 引用快編的原理,將沒有變更的模塊變成aar,變更的模塊切換到源碼。
- owner 將自己作為owner的工程源代碼展開,其他的和aarOnly一致。
一般情況下,開發(fā)同學(xué)只需要更改自己業(yè)務(wù)代碼就行了,所以他們可以將自己不需要的模塊直接切換成aar,這樣的同步速度對他們來說是最快的,所以我們把默認模式都切換成aarOnly模式。
另外我們只是在原始的工程結(jié)構(gòu)下,開辟出了獨立的編譯單元,因為Android Studio打開的路徑不同,則選擇的模式就會出現(xiàn)差異,所以他并不會實際影響到當(dāng)前的工程結(jié)構(gòu)。
通過這兩張數(shù)據(jù)對比圖,可以看出我們在同步的時間上,是有非常大的數(shù)據(jù)提升的。在這種模式下,可以讓工程找到相對準確的aar版本,展開模塊數(shù)量變少,編譯和同步速度也就能更快的。對于開發(fā)來說,他也只需要對自己的業(yè)務(wù)代碼負責(zé)。
展望
后續(xù)我們計劃和自動化初始化框架進行配合,在每個可單獨打開的工程下都生成好一個殼工程,讓業(yè)務(wù)同學(xué)可以更快速感知到當(dāng)前代碼的變更并進行調(diào)試。然后通過idea插件,或命令行工具可以快速的生成這個殼工程。這個插件后續(xù)也能跟隨著b站的工程迭代而持續(xù)更新,提供更多更便利的功能給到開發(fā)同學(xué)。
我們的任務(wù)就是提供更多的可能性,更多的便利性,將選擇的權(quán)利交給業(yè)務(wù)同學(xué)。讓他們能在大倉的模式下快速穩(wěn)定的開發(fā)下去,可以選擇大倉,也可以選擇自己業(yè)務(wù)的獨立編譯路徑。
結(jié)語
我們是如何做編譯優(yōu)化的,到這里已經(jīng)聊得差不多了。項目中還有很多東西是可以繼續(xù)進行優(yōu)化的。比如說文件訪問速度,編譯緩存,將工程粒度更細化,更多idea插件,快速幫開發(fā)定位代碼等等。
B站在單倉的路上其實已經(jīng)走了很多年了,也碰到了很多的挑戰(zhàn)和問題,我們甚至一度想要放棄這種模式。但堅持下來之后,我們還是認為單倉的優(yōu)點是要大于多倉的。作為開發(fā)同學(xué),我們更多的時候應(yīng)該迎接挑戰(zhàn),然后思考如何去戰(zhàn)勝這些問題,更多關(guān)于嗶哩嗶哩Android編譯優(yōu)化的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android中判斷有無可用網(wǎng)絡(luò)的代碼(是否是3G或者WIFI網(wǎng)絡(luò))
在android開發(fā)中經(jīng)常會遇到的判斷有無可用網(wǎng)絡(luò)的代碼,防止客戶流量損失2013-01-01Android創(chuàng)建與解析XML(二)——詳解Dom方式
本篇文章主要介紹了Android創(chuàng)建與解析XML(二)——詳解Dom方式 ,這里整理了詳細的代碼,有需要的小伙伴可以參考下。2016-11-11RecyclerView實現(xiàn)側(cè)滑和網(wǎng)絡(luò)斷點續(xù)傳
這篇文章主要為大家詳細介紹了RecyclerView實現(xiàn)側(cè)滑和網(wǎng)絡(luò)斷點續(xù)傳,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-07-07Android實現(xiàn)漸變圓環(huán)、圓形進度條效果
這篇文章主要為大家詳細介紹了Android實現(xiàn)漸變圓環(huán)、圓形進度條效果,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-10-10Android?NDK開發(fā)之FFmpeg視頻添加水印
這篇文章主要介紹了在Android?NDK開發(fā)中如何通過FFmpeg為視頻添加水印,文中的示例代碼講解詳細,對我們了解Android開發(fā)有一定的幫助,感興趣的可以學(xué)習(xí)一下2021-12-12仿餓了嗎點餐界面ListView聯(lián)動的實現(xiàn)
這篇文章主要介紹了仿餓了嗎點餐界面ListView聯(lián)動的實現(xiàn)的相關(guān)資料,本文介紹的非常詳細,具有參考借鑒價值,需要的朋友可以參考下2016-09-09