Android application 中使用 provided aar 并没有那么简单
前言
首先簡單講一下這個需求的背景,大部分場景下,是沒有這個需求的,這個需求出現在插件化中,當一個android插件引用aar中的類的時候,并且這個插件是使用com.android.application 這個gradle插件進行打包的,但是這個類已經在宿主中或者其他插件中,其實就沒有必要將這個重復類打包到插件中,因此只需要進行引用,不需要打包到插件中。引用是其中一個目的,保證混淆的正確性則是另一個目的。尋找這個需求的解決方案的過程中,發現這個問題其實并沒有想象中的那么好解決,會遇到許許多多的細節問題。
Atlas的解決方案
atlas的插件并沒有使用com.android.application進行編譯,而是使用com.android.library進行編譯,然后發布maven中的也只是aar(awb),真正編譯插件的時機是在宿主中,使用bundleCompile引用插件aar,在編譯宿主的過程中會執行插件編譯,然后將編譯好的插件放入動態庫armeabi目錄,而在com.android.library中使用provided aar是完全可以的,于是atlas也就沒有了在com.android.application中使用provided aar的需求,完全不存在這個問題。但是假設我們的插件在集成到宿主中前就已經編譯好了,因此就需要使用com.android.application進行編譯,對于一些宿主中或其他插件中存在的重復類,就必然需要使用到provided的功能,對于jar來說,沒有問題,正常使用即可,但是對于aar來說,android gradle plugin肯定會爆出一個問題,該問題如下
| 1 | Provided dependencies can only be jars. |
因此必須尋找一個在com.android.application中使用provided aar的方法。
方法一:自定義Configuration
使用自定義的Configuration,然后將provided這個Configuration繼承我們自定義的Configuration,這個方法理所當然的會最先被想到。
首先創建一個自定義的Configuration對象providedAar,然后將providedAar中的所有依賴解析出來,將解析出來的文件進行判斷處理,如果是aar,則提取classes.jar,如果是jar,則直接使用,然后添加到provided的scope上。這樣就完成了,代碼如下:
| 123456789101112131415161718192021222324252627282930313233343536 | @Overridevoid apply(Project project) { //創建自定義的configuration,且進行傳遞依賴,SNAPSHOT版本立即更新Configuration configuration = project.getConfigurations().create("providedAar") { //進行傳遞依賴it.setTransitive(true)it.resolutionStrategy { //SNAPSHOT版本更新時間為0scacheChangingModulesFor(0, 'seconds') //動態版本更新實際為5分鐘cacheDynamicVersionsFor(5, 'minutes')}} //遍歷解析出來的所有依賴configuration.incoming.dependencies.all { //過濾收集aar和jarFileCollection collection = configuration.fileCollection(it).filter { return it.name.endsWith(".aar") || it.name.endsWith(".jar")} //遍歷過濾后的文件collection.each { if (it.name.endsWith(".aar")) { //如果是aar,則提取里面的jar文件FileCollection jarFormAar = project.zipTree(it).filter {it.name == "classes.jar"} //將jar依賴添加到provided的scope中 project.dependencies.add("provided", jarFormAar)} else if (it.name.endsWith(".jar")) { //如果是jar則直接添加 //將jar依賴添加到provided的scope中 project.dependencies.add("provided", project.files(it))}}}} |
這里需要注意,如果要添加provided依賴的jar文件,只需要調用project.dependencies.add(“provided”, FileCollection fileCollection)方法進行添加即可。
其實上面的代碼忽視了一個十分重要的問題,即完全無視了gradle的生命周期。依賴對應的文件類型FileCollection,只有依賴被解析之后才能拿到,即在afterResolve回調之后才能拿到,而afterResolve之后如果再對dependencies對象繼續修改,必然會拋出一個異常
| 1 | Cannot change dependencies of configuration ':app:providedAar' after it has been resolved |
所以這段代碼思路是行不通的,會存在一個循環依賴的問題,添加依賴前無法獲取依賴文件,獲取到依賴文件后,無法添加到provided scope依賴中去。這就是這種方法行不通的根本原因。
方法二:實現maven下載協議
參考這篇文章?如何在Android Application里provided-aar,解決的思路是在beforeResolve中添加依賴,但是在beforeResolve是拿不到依賴對應的文件FileCollection對象的,于是這個方法最大的問題變成了如何獲取依賴文件。這個文件獲取如果要做的完美,其實是很復雜的,涉及到整個依賴樹的有向圖構建,版本沖突解決,以及傳遞依賴的遞歸問題。
首先來了解一下maven下載的最簡單的邏輯。
對于release版本的依賴,其實很簡單,拼接依賴的path路徑即可,路徑格式為
| 1 | /$groupId[0]/../${groupId[n]/$artifactId/$version/$artifactId-$version.$extension |
假設依賴為io.github.lizhangqu:test:1.0.0,則pom文件對應的依賴path路徑以及pom文件對應的md5和sha1文件路徑為
| 123 | /io/github/lizhangqu/test/1.0.0/test-1.0.0.pom/io/github/lizhangqu/test/1.0.0/test-1.0.0.pom.md5/io/github/lizhangqu/test/1.0.0/test-1.0.0.pom.sha1 |
根據這個路徑獲取到pom文件中的內容,讀取pom.xml中的packaging的值,假設pom.xml文件內容為
| 123456789 | xml version="1.0" encoding="UTF-8"<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <modelVersion>4.0.0</modelVersion> <groupId>io.github.lizhangqu</groupId> <artifactId>test</artifactId> <version>1.0.0</version> <packaging>aar</packaging></project> |
從上述內容很容易得到這里packaging的值為aar,則對應的文件以及該文件對應的md5和sha1文件路徑為
| 123 | /io/github/lizhangqu/test/1.0.0/test-1.0.0.aar/io/github/lizhangqu/test/1.0.0/test-1.0.0.aar.md5/io/github/lizhangqu/test/1.0.0/test-1.0.0.aar.sha1 |
如果該依賴存在classifier,則路徑更復雜,比如javadoc和sources,其路徑格式會變成
| 1 | /$groupId[0]/../$groupId[n]/$artifactId/$version/$artifactId-$version-$classifier.$extension |
上述依賴的classifier路徑及classifier文件對應的md5和sha1文件的路徑則是
| 123456 | /io/github/lizhangqu/test/1.0.0/test-1.0.0-javadoc.jar/io/github/lizhangqu/test/1.0.0/test-1.0.0-javadoc.jar.md5/io/github/lizhangqu/test/1.0.0/test-1.0.0-javadoc.jar.sha1/io/github/lizhangqu/test/1.0.0/test-1.0.0-sources.jar/io/github/lizhangqu/test/1.0.0/test-1.0.0-sources.jar.md5/io/github/lizhangqu/test/1.0.0/test-1.0.0-sources.jar.sha1 |
如果classifier不是jar文件,則gradle中必須強制手動指定extension,否則會提示找不到對應的classifier文件。假設classifier為so文件,則需要強制指定為
| 12 | io.github.lizhangqu:test:1.0.0:javadoc@soio.github.lizhangqu:test:1.0.0:sources@so |
而對于SNAPSHOT版本,就比較復雜了,假設依賴為io.github.lizhangqu:test:1.0.0-SNAPSHOT,則我們需要獲取該版本的maven-metadata.xml文件,對應的path路徑及其md5和sha1值的文件路徑為
| 123 | /io/github/lizhangqu/test/1.0.0-SNAPSHOT/maven-metadata.xml/io/github/lizhangqu/test/1.0.0-SNAPSHOT/maven-metadata.xml.md5/io/github/lizhangqu/test/1.0.0-SNAPSHOT/maven-metadata.xml.sha1 |
假設這個文件內容如下
| 123456789101112 | <metadata> <groupId>io.github.lizhangqu</groupId> <artifactId>test</artifactId> <version>1.0.0-SNAPSHOT</version> <versioning> <snapshot> <timestamp>20171222.013814</timestamp> <buildNumber>200</buildNumber> </snapshot> <lastUpdated>20171222013814</lastUpdated> </versioning></metadata> |
從該文件中看出當前1.0.0-SNAPSHOT的信息是這個artifact發布了200次,最新發布的時間是20171222.013814,拼接這兩個值,得到20171222.013814-200,于是就獲得了該依賴版本的最新快照的path路徑以及md5和sha1文件路徑
| 123 | /io/github/lizhangqu/test/1.0.0-SNAPSHOT/test-1.0.0-20171222.013814-200.pom/io/github/lizhangqu/test/1.0.0-SNAPSHOT/test-1.0.0-20171222.013814-200.pom.md5/io/github/lizhangqu/test/1.0.0-SNAPSHOT/test-1.0.0-20171222.013814-200.pom.sha1 |
假設這個pom.xml文件中的內容如下
| 123456789 | xml version="1.0" encoding="UTF-8"<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <modelVersion>4.0.0</modelVersion> <groupId>io.github.lizhangqu</groupId> <artifactId>test</artifactId> <version>1.0.0-SNAPSHOT</version> <packaging>aar</packaging></project> |
從pom.xml中可以看到packaging為aar,則對應的文件的路徑及該文件的md5和sha1文件路徑為
| 123 | /io/github/lizhangqu/test/1.0.0-SNAPSHOT/test-1.0.0-20171222.013814-200.aar/io/github/lizhangqu/test/1.0.0-SNAPSHOT/test-1.0.0-20171222.013814-200.aar.md5/io/github/lizhangqu/test/1.0.0-SNAPSHOT/test-1.0.0-20171222.013814-200.aar.sha1 |
之后的處理就和release版本是一樣的。
依賴部分的path知道了,那么還需要知道maven服務所在地址,可通過gradle直接拿到
| 123456789101112131415 | project.getGradle().addListener(new DependencyResolutionListener() {@Override void beforeResolve(ResolvableDependencies dependencies) { //此回調會多次進入,我們只需要解析一次,因此只要進入,就remove,然后執行我們的解析操作roject.gradle.removeListener(this) project.getRepositories().each {def repository -> //repository.url就是maven服務的前綴路徑,可能是文件協議,也可能是http協議,或是其他協議,如ftp}}@Override void afterResolve(ResolvableDependencies resolvableDependencies) {}}) |
拼接最終的地址,如下
| 12 | []內的內容為可選repository.url/$groupId[0]/../$groupId[n]/$artifactId/$version/$artifactId-$version[-$classifier].$extension |
上述只是簡單的講了下下載的邏輯。文件是如何緩存的,如何更新本地的SNAPSHOT版本,如何將本地信息與遠程信息進行合并,以及傳遞依賴是如何處理的都沒有講到,實際情況要比這個遠遠復雜的多。
就拿緩存路徑來說,對于mavenLocal,其本地文件緩存的路徑是最簡單的,為
| 12 | #[]內的內容為可選~/.m2/repository/$groupId[0]/../$groupId[n]/$artifactId/$version/$artifactId-$version[-$classifier].$extension |
而gradle的緩存目錄十分復雜,由gradle自己處理,且各個版本路徑可能會有差異,大致位于
| 12 | #[]內的內容為可選~/.gradle/caches/modules-2/files-2.1/$groupId/$artifactId/$version/$sha1Value/$artifactId-$version[-$classifier].$extension |
其中sha1Value的值為$artifactId-$version[-$classifier].$extension文件對應的sha1值
gradle的緩存策略可以見文檔?gradle caching
gradle各個版本元數據的緩存目錄如下
| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455 | public LocallyAvailableResourceFinder<ModuleComponentArtifactMetadata> create() {List<LocallyAvailableResourceFinder<ModuleComponentArtifactMetadata>> finders = new LinkedList<LocallyAvailableResourceFinder<ModuleComponentArtifactMetadata>>();// Order is important here, because they will be searched in that order// The current filestorefinders.add(new LocallyAvailableResourceFinderSearchableFileStoreAdapter<ModuleComponentArtifactMetadata>(new FileStoreSearcher<ModuleComponentArtifactMetadata>() {@Overridepublic Set<? extends LocallyAvailableResource> search(ModuleComponentArtifactMetadata key) {return fileStore.search(key.getId());}}));// 1.8addForPattern(finders, "artifacts-26/filestore/[organisation]/[module](/[branch])/[revision]/[type]/*/[artifact]-[revision](-[classifier])(.[ext])");// 1.5addForPattern(finders, "artifacts-24/filestore/[organisation]/[module](/[branch])/[revision]/[type]/*/[artifact]-[revision](-[classifier])(.[ext])");// 1.4addForPattern(finders, "artifacts-23/filestore/[organisation]/[module](/[branch])/[revision]/[type]/*/[artifact]-[revision](-[classifier])(.[ext])");// 1.3addForPattern(finders, "artifacts-15/filestore/[organisation]/[module](/[branch])/[revision]/[type]/*/[artifact]-[revision](-[classifier])(.[ext])");// 1.1, 1.2addForPattern(finders, "artifacts-14/filestore/[organisation]/[module](/[branch])/[revision]/[type]/*/[artifact]-[revision](-[classifier])(.[ext])");// rc-1, 1.0addForPattern(finders, "artifacts-13/filestore/[organisation]/[module](/[branch])/[revision]/[type]/*/[artifact]-[revision](-[classifier])(.[ext])");// Milestone 8 and 9addForPattern(finders, "artifacts-8/filestore/[organisation]/[module](/[branch])/[revision]/[type]/*/[artifact]-[revision](-[classifier])(.[ext])");// Milestone 7addForPattern(finders, "artifacts-7/artifacts/*/[organisation]/[module](/[branch])/[revision]/[type]/[artifact]-[revision](-[classifier])(.[ext])");// Milestone 6addForPattern(finders, "artifacts-4/[organisation]/[module](/[branch])/*/[type]s/[artifact]-[revision](-[classifier])(.[ext])");addForPattern(finders, "artifacts-4/[organisation]/[module](/[branch])/*/pom.originals/[artifact]-[revision](-[classifier])(.[ext])");// Milestone 3addForPattern(finders, "../cache/[organisation]/[module](/[branch])/[type]s/[artifact]-[revision](-[classifier])(.[ext])");// Maven localtry {File localMavenRepository = localMavenRepositoryLocator.getLocalMavenRepository();if (localMavenRepository.exists()) {addForPattern(finders, localMavenRepository, new M2ResourcePattern("[organisation]/[module]/[revision]/[artifact]-[revision](-[classifier])(.[ext])"));}} catch (CannotLocateLocalMavenRepositoryException ex) {finders.add(new NoMavenLocalRepositoryResourceFinder(ex));}return new CompositeLocallyAvailableResourceFinder<ModuleComponentArtifactMetadata>(finders);} |
知道了這些后,我們可以自己去實現一套maven下載協議,但是還是十分復雜的,所以個人認為這沒有必要,如果自己去實現,首先,不能復用gradle現有的緩存,其次自己下載的緩存,無法正常的與gradle緩存進行合并,雖然可以放到mavenLocal目錄下,但是一些策略性的問題,不能考慮得十分周全,如SNAPSHOT版本的更新。因此,這里就不自己實現一套下載協議,而是復用gradle內部的下載代碼及現有的緩存。不過經過測試,很遺憾,不支持傳遞依賴。具體是怎么實現的請見方法三里的一部分處理。
方法三:合理使用反射,Hack一下代碼
方法三,是看了幾天android gradle plugin以及gradle代碼總結出來的,看代碼的過程是十分痛苦的,只能一點點猜測相關的類在哪里,不過最后實現的腦洞比較大,比較黑科技。
既然在com.android.application中使用provided aar會報出Provided dependencies can only be jars. 異常,那么有沒有辦法把這個異常消除掉呢。我們發現在com.android.library中是支持provided aar的,這說明android gradle plugin其自身是支持provided aar的,只是在哪個環節做了下校驗,在com.android.application中拋出了異常而已。簡單的瀏覽了下android gradle plugin的代碼,發現是完全可以行的通的,不過比較遺憾的是只能消除2.2.0之后的版本的異常,2.2.0之前的版本能消除,但是無法正常使用provided功能,即使是provided的,也會被看成是compile依賴,所以2.2.0之前的版本需要單獨處理一下。除此之外,3.0.0與之前的版本消除方式不大一樣,需要做區分處理。而對于2.2.0以下的版本怎么做呢?答案是使用方法二中的復用gradle現有代碼及緩存。
于是我們需要進行一下android gradle plugin的版本區分,這里做了如下分界
- android gradle plugin [1.5.0,2.2.0), 不支持傳遞依賴 ,且gradle版本必須為2.10以上,而android gradle plugin 1.5.0以下版本太過久遠,就不支持了
- android gradle plugin [2.2.0,2,5.0), 支持傳遞依賴 ,使用反射消除異常
- android gradle plugin [2.5.0,3.1.0+], 支持傳遞依賴 ,使用反射替換task執行邏輯,將拋異常修改為輸出日志信息
其中,2.4.0+和2.5.0+都未發布過正式版本,而2.4.0預覽版代碼比較趨向于2.3.0的代碼,2.5.0預覽版代碼比較趨向于3.0.0的代碼,于是產生了上述的分界。
先從最簡單的版本開始,我們先處理[2.2.0,2,5.0)的版本,這里以2.3.3的代碼為例,只要在[2.2.0,2,5.0)這個區間內的版本,都可以適用。
通過搜索 Provided dependencies can only be jars,發現這個異常在com.android.build.gradle.internal.dependency.DependencyChecker中被記錄,類型type為7,錯誤嚴重級別為2,在配置評估階段只會打出WARNING的日志,而真正拋出異常的地方是在PrepareDependenciesTask執行的時候拋出的。其關鍵代碼如下
| 123456789101112131415161718 | private final List<DependencyChecker> checkers = Lists.newArrayList();protected void prepare() { //省略代碼... boolean foundError = false; //省略代碼... for (SyncIssue syncIssue : checker.getSyncIssues()) { if (syncIssue.getSeverity() == SyncIssue.SEVERITY_ERROR) {foundError = true;getLogger().error(syncIssue.getMessage());}} if (foundError) { throw new GradleException("Dependency Error. See console for details.");}} |
也就是說我們只要在這個task action被執行前,把checkers變量中對應類型的異常remove掉,就不會出現報錯了,實現一下,具體代碼如下
| 1234567891011121314151617181920212223242526272829303132333435363738394041 | //遍歷所有變體android.applicationVariants.all {def variant -> //獲得該變體對應的prepareDependencies task def prepareDependenciesTask = project.tasks.findByName("prepare${variant.getName().capitalize()}Dependencies") //如果存在該task,則處理,否則無視 if (prepareDependenciesTask) { //移除異常的閉包操作,因為需要復用,所以這里提取成了閉包 def removeSyncIssues = {try { //反射獲取checkers List<DependencyChecker>對象Class prepareDependenciesTaskClass = Class.forName("com.android.build.gradle.internal.tasks.PrepareDependenciesTask")Field checkersField = prepareDependenciesTaskClass.getDeclaredField('checkers')checkersField.setAccessible(true) //得到checkers字段值 def checkers = checkersField.get(prepareDependenciesTask) //因為要進行移除操作,所以用迭代器去遍歷checkerscheckers.iterator().with {checkersIterator ->checkersIterator.each {dependencyChecker -> //需要將DependencyChecker對象中的syncIssues進行遍歷,獲得對應類型,將其移除 //遍歷checker中的syncIssues def syncIssues = dependencyChecker.syncIssuessyncIssues.iterator().with {syncIssuesIterator ->syncIssuesIterator.each {syncIssue -> //如果類型是7,并且錯誤嚴重級別為2,則將其移除,并且輸出日志提示 if (syncIssue.getType() == 7 && syncIssue.getSeverity() == 2) {project.logger.lifecycle "[providedAar] WARNING: providedAar has been enabled in com.android.application you can ignore ${syncIssue}" //移除syncIssuesIterator.remove()}}}}}} catch (Exception e) {e.printStackTrace()}} //在配置階段執行該閉包prepareDependenciesTask.configure removeSyncIssues}} |
簡單測試一下,跑了下,沒問題,后面測試了下兼容性,在[2.2.0,2,5.0)內的版本都適用該代碼,不過有個細節性的問題需要處理,即2.4.0+預覽版的gradle,其代碼雖然偏向2.3.0+的代碼,但是內部一些邏輯還是比較偏向于3.0.0的代碼的,經過試驗發現,不能在prepareDependenciesTask.configure去執行該閉包,那個時候checkers中的syncIssues還是空的,因此需要延遲執行,但必須在任務執行前執行,因此需要將其移到doFirst中去執行,但是對于非2.4.0+預覽版的版本,doFirst執行就太晚了,必須在configure階段進行移除,否則就會報異常,所以需要做下版本區分,需要獲取gradle的版本號,那么這個版本號怎么獲取呢。很簡單,代碼如下
| 123456789101112 | String getAndroidGradlePluginVersionCompat() {String version = null try {Class versionModel = Class.forName("com.android.builder.model.Version")def versionFiled = versionModel.getDeclaredField("ANDROID_GRADLE_PLUGIN_VERSION")versionFiled.setAccessible(true) version = versionFiled.get(null)} catch (Exception e) { version = "unknown"} return version} |
然后做下版本區分
| 1234567 | String androidGradlePluginVersion = getAndroidGradlePluginVersionCompat()if (androidGradlePluginVersion.startsWith("2.2") || androidGradlePluginVersion.startsWith("2.3")) {prepareDependenciesTask.configure removeSyncIssues //configure階段執行} else if (androidGradlePluginVersion.startsWith("2.4")) {prepareDependenciesTask.doFirst removeSyncIssues //configure階段過早,無法正常異常,延遲到doFirst中去執行} |
這樣[2.2.0,2,5.0)這個區間內的版本就處理完了,接下來處理第二復雜的版本,即[2.5.0,3.1.0+],這里以3.0.1的代碼為例,修改代碼適用于[2.5.0,3.1.0+]區間內的所有版本
對于這個區間范圍內的版本,如果在com.android.application中使用provided aar,則會報出這個錯誤信息
| 1 | Android dependency '$dependency' is set to compileOnly/provided which is not supported |
通過搜索報錯的關鍵字符串信息,可以定位到這個異常是在AppPreBuildTask中報出來的,其關鍵代碼如下
| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263 | private ArtifactCollection compileManifests;private ArtifactCollection runtimeManifests;@TaskActionvoid run() {Set<ResolvedArtifactResult> compileArtifacts = compileManifests.getArtifacts();Set<ResolvedArtifactResult> runtimeArtifacts = runtimeManifests.getArtifacts(); // create a map where the key is either the sub-project path, or groupId:artifactId for // external dependencies. // For external libraries, the value is the version.Map<String, String> runtimeIds = Maps.newHashMapWithExpectedSize(runtimeArtifacts.size()); // build a list of the runtime artifacts for (ResolvedArtifactResult artifact : runtimeArtifacts) {handleArtifact(artifact.getId().getComponentIdentifier(), runtimeIds::put);} // run through the compile ones to check for provided only. for (ResolvedArtifactResult artifact : compileArtifacts) { final ComponentIdentifier compileId = artifact.getId().getComponentIdentifier();handleArtifact(compileId,(key, value) -> { String runtimeVersion = runtimeIds.get(key); if (runtimeVersion == null) { String display = compileId.getDisplayName(); throw new RuntimeException( "Android dependency '"+ display+ "' is set to compileOnly/provided which is not supported");} else if (!runtimeVersion.isEmpty()) { // compare versions. if (!runtimeVersion.equals(value)) { throw new RuntimeException( String.format( "Android dependency '%s' has different version for the compile (%s) and runtime (%s) classpath. You should manually set the same version via DependencyResolution", key, value, runtimeVersion));}}});}}private void handleArtifact(@NonNull ComponentIdentifier id, @NonNull BiConsumer<String, String> consumer) { if (id instanceof ProjectComponentIdentifier) {consumer.accept(((ProjectComponentIdentifier) id).getProjectPath().intern(), "");} else if (id instanceof ModuleComponentIdentifier) {ModuleComponentIdentifier moduleComponentId = (ModuleComponentIdentifier) id;consumer.accept(moduleComponentId.getGroup() + ":" + moduleComponentId.getModule(),moduleComponentId.getVersion());} else if (id instanceof OpaqueComponentArtifactIdentifier) { // skip those for now. // These are file-based dependencies and it's unlikely to be an AAR.} else {getLogger().warn( "Unknown ComponentIdentifier type: "+ id.getClass().getCanonicalName());}} |
簡單說下上述代碼的邏輯
- 獲取Android aar依賴的編譯期依賴compileManifests和運行期依賴runtimeManifests
- 創建一個map對象runtimeIds,遍歷運行期依賴,將依賴的坐標作為key,依賴的版本號作為值
- 遍歷編譯期依賴,用依賴的坐標從runtimeIds map中取值,如果取到的值,即依賴的版本號為空,則表示編譯期的aar依賴在運行期依賴中不存在,于是拋出Android dependency ‘$dependency’ is set to compileOnly/provided which is not supported異常
- 如果版本號不為空,則比較編譯期和運行期的版本號,如果不一致,則拋版本號不一致的異常,這個需要業務上控制版本號一致,不需要特殊處理
從上面的分析可以看出,拋出異常的原因是aar的編譯期依賴在運行期依賴中不存在。所以消除這個異常的方法就是那么我們讓其存在即可,但是如果讓其存在,最終編譯期provided的aar也會被編譯進apk中去,這不是我們想要的。換一種方式,替換執行邏輯。因此我們需要替換這個task真正執行的內容,讓其拋出異常的部分修改為打日志。具體的修改代碼如下,可以直接看注釋,關鍵部分就是將拋異常的邏輯修改為輸出日志,其他邏輯和原代碼一樣。還有一點需要注意的是2.5.0+預覽版的代碼和3.0.0+版本的代碼有一點區別,需要進行兼容處理。
| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112 | //尋找pre${buildType}Build任務def prepareBuildTask = project.tasks.findByName("pre${variant.getName().capitalize()}Build")//如果task存在,則進行處理if (prepareBuildTask) { //是否需要重定向執行的action內容boolean needRedirectAction = false //迭代該任務的actions,如果存在AppPreBuildTask這個名字,則將其移除,標記重定向標記為trueprepareBuildTask.actions.iterator().with {actionsIterator ->actionsIterator.each {action -> //取action名字,判斷是否包含AppPreBuildTask字符串 if (action.getActionClassName().contains("AppPreBuildTask")) { //移除,并進行標記需要重定向實現actionsIterator.remove()needRedirectAction = true}}} //如果重定向標記為true,則將原有邏輯拷貝下來,用反射實現一遍,然后將拋異常的部分修改為輸出日志 if (needRedirectAction) { //添加新的action,代替被移除的action執行邏輯prepareBuildTask.doLast { //下面一大坨是兼容處理,3.0.0+的版本是compileManifests和runtimeManifests,并且在配置階段這兩個值已經被賦值,2.5.0+預覽版的代碼是compileClasspath和runtimeClasspath,其值在執行時通過variantScope獲取 def compileManifests = null def runtimeManifests = nullClass appPreBuildTaskClass = Class.forName("com.android.build.gradle.internal.tasks.AppPreBuildTask")try { //3.0.0+的版本直接取compileManifests和runtimeManifests字段的值Field compileManifestsField = appPreBuildTaskClass.getDeclaredField("compileManifests")Field runtimeManifestsField = appPreBuildTaskClass.getDeclaredField("runtimeManifests")compileManifestsField.setAccessible(true)runtimeManifestsField.setAccessible(true)compileManifests = compileManifestsField.get(prepareBuildTask)runtimeManifests = runtimeManifestsField.get(prepareBuildTask)} catch (Exception e) {try { //2.5.0+的版本,由于其值是在run階段賦值的,因此我們需要換一個方式獲取,通過variantScope去拿對應的內容Field variantScopeField = appPreBuildTaskClass.getDeclaredField("variantScope")variantScopeField.setAccessible(true) def variantScope = variantScopeField.get(prepareBuildTask) //為了兼容,需要避免import導包,這里使用全類名路徑進行引用,且使用注釋消除ide警告 //noinspection UnnecessaryQualifiedReferencecompileManifests = variantScope.getArtifactCollection(com.android.build.gradle.internal.publishing.AndroidArtifacts.ConsumedConfigType.COMPILE_CLASSPATH, com.android.build.gradle.internal.publishing.AndroidArtifacts.ArtifactScope.ALL, com.android.build.gradle.internal.publishing.AndroidArtifacts.ArtifactType.MANIFEST)runtimeManifests = variantScope.getArtifactCollection(com.android.build.gradle.internal.publishing.AndroidArtifacts.ConsumedConfigType.RUNTIME_CLASSPATH, com.android.build.gradle.internal.publishing.AndroidArtifacts.ArtifactScope.ALL, com.android.build.gradle.internal.publishing.AndroidArtifacts.ArtifactType.MANIFEST)} catch (Exception e1) {}}try { //下面還原原有action的邏輯 //獲取編譯期和運行期android aar依賴Set<ResolvedArtifactResult> compileArtifacts = compileManifests.getArtifacts()Set<ResolvedArtifactResult> runtimeArtifacts = runtimeManifests.getArtifacts() //創建MapMap<String, String> runtimeIds = new HashMap<>(runtimeArtifacts.size()) //原有的handleArtifact函數,這里改成了閉包 def handleArtifact = {id, consumer -> if (id instanceof ProjectComponentIdentifier) {consumer(((ProjectComponentIdentifier) id).getProjectPath().intern(), "")} else if (id instanceof ModuleComponentIdentifier) {ModuleComponentIdentifier moduleComponentId = (ModuleComponentIdentifier) idconsumer(moduleComponentId.getGroup() + ":" + moduleComponentId.getModule(),moduleComponentId.getVersion())} else {getLogger().warn( "Unknown ComponentIdentifier type: "+ id.getClass().getCanonicalName())}} //處理原有的for循環邏輯,將runtime部分放入runtimeIds的map,鍵是坐標,值為版本號runtimeArtifacts.each {def artifact -> def runtimeId = artifact.getId().getComponentIdentifier() def putMap = {def key, def value -> runtimeIds.put(key, value)}handleArtifact(runtimeId, putMap)} //遍歷compile依賴部分的內容,判斷是否在runtime依賴中存在版本號,如果不存在,則會拋異常compileArtifacts.each {def artifact -> final ComponentIdentifier compileId = artifact.getId().getComponentIdentifier() def checkCompile = {def key, def value -> String runtimeVersion = runtimeIds.get(key) if (runtimeVersion == null) {String display = compileId.getDisplayName() //這里拋的異常,修改為打日志,僅此一處修改project.logger.lifecycle( "[providedAar] WARNING: providedAar has been enabled in com.android.application you can ignore 'Android dependency '"+ display+ "' is set to compileOnly/provided which is not supported'")} else if (!runtimeVersion.isEmpty()) { // compare versions. if (!runtimeVersion.equals(value)) {throw new RuntimeException(String.format( "Android dependency '%s' has different version for the compile (%s) and runtime (%s) classpath. You should manually set the same version via DependencyResolution",key, value, runtimeVersion));}}}handleArtifact(compileId, checkCompile)}} catch (Exception e) {e.printStackTrace()}}}} |
[2.5.0,3.1.0+]區間內的版本也搞定了,通過兼容性測試,發現這段代碼完美的適配了[2.5.0,3.1.0+]這個區間內的所有版本。
當然,為了進行各個版本的區分處理,需要進行一些邏輯上的自定義,所以,我們最好不要直接使用provided這個configuration,而是創建自定義的providedAar,這么做的好處就是為了后續擴展,提供更加靈活的途徑,而不需要業務修改現有的代碼。這部分的代碼如下:
| 12345678910111213141516171819202122 | //如果沒有引用com.android.application插件,直接returnif (!project.getPlugins().hasPlugin("com.android.application")) { return}//如果已經添加過providedAar,直接returnif (project.getConfigurations().findByName("providedAar") != null) { return}//如果不存在provided,直接returnConfiguration providedConfiguration = project.getConfigurations().findByName("provided")if (providedConfiguration == null) { return}//創建providedAarConfiguration providedAarConfiguration = project.getConfigurations().create("providedAar")//獲取android gradle plugin版本號String androidGradlePluginVersion = getAndroidGradlePluginVersionCompat()//這里只處理2.2.0+的版本的provided與providedAar的繼承關系,2.2.0以下的版本,不能添加這個繼承關系,添加了會報錯,且無法消除錯誤if (androidGradlePluginVersion.startsWith("2.2") || androidGradlePluginVersion.startsWith("2.3") || androidGradlePluginVersion.startsWith("2.4") || androidGradlePluginVersion.startsWith("2.5") || androidGradlePluginVersion.startsWith("3.")) { //大于2.2.0的版本讓provided繼承providedAar,低于2.2.0的版本,手動提取aar中的jar添加依賴到scope為provided的依賴中去providedConfiguration.extendsFrom(providedAarConfiguration)} |
業務上使用
| 123 | dependencies { providedAar 'com.tencent.tinker:tinker-android-lib:1.9.1'} |
不要使用
| 123 | dependencies { provided 'com.tencent.tinker:tinker-android-lib:1.9.1'} |
值得注意的是2.2.0+以上的版本,處理后providedAar是支持傳遞依賴的,但是2.2.0以下由于其特殊性,不能支持傳遞依賴。
接下里就是最蛋疼的2.2.0以下的版本的處理,2.2.0以下的版本,雖然也可以使用[2.2.0,2.5.0)這個區間的方法將異常進行消除,但是即使消除了,因為其代碼的特殊性,導致即使是provided依賴的aar,最終也會被打包進apk中,這里以2.1.3的代碼為例,如下:
| 1234567891011121314151617181920 | //遍歷過濾后剩余的編譯期的依賴for (LibInfo lib : compiledAndroidLibraries) { if (!copyOfPackagedLibs.contains(lib)) { if (isLibrary || lib.isOptional()) { //如果是com.android.library或者是可選的,則設置該lib為可選 lib.setIsOptional(true);} else { //否則,也就是在com.android.application中,將這個問題記錄了下來,后續的處理就在PrepareDependenciesTask執行的過程中報異常 //noinspection ConstantConditionsvariantDeps.getChecker().addSyncIssue(extraModelInfo.handleSyncError( lib.getResolvedCoordinates().toString(),SyncIssue.TYPE_NON_JAR_PROVIDED_DEP, String.format( "Project %s: provided dependencies can only be jars. %s is an Android Library.",project.getName(), lib.getResolvedCoordinates())));}} else {copyOfPackagedLibs.remove(lib);}} |
對比2.3.3版本的代碼
| 12345678910111213141516 | //獲得maven坐標MavenCoordinates resolvedCoordinates = compileLib.getCoordinates();//如果不是com.android.library,也不是com.android.atom,也不是用于測試的com.android.test,即是com.android.applicationif (variantType != VariantType.LIBRARY&& variantType != VariantType.ATOM&& (testedVariantType != VariantType.LIBRARY || !variantType.isForTesting())) { //就會將這個異常記錄下來,后續的處理就在PrepareDependenciesTask執行的過程中報異常handleIssue(resolvedCoordinates.toString(),SyncIssue.TYPE_NON_JAR_PROVIDED_DEP,SyncIssue.SEVERITY_ERROR, String.format( "Project %s: Provided dependencies can only be jars. %s is an Android Library.",projectName,resolvedCoordinates.toString()));} |
從兩個版本的代碼中可以看出有一處不同,如果不存在provided aar問題的話,2.2.0以下的版本在com.android.library中會將lib設為可選,但是在com.android.application就會直接拋出異常,具體的區別代碼如下
| 123456 | if (isLibrary || lib.isOptional()) {//如果是com.android.library或者是可選的,則設置該lib為可選 lib.setIsOptional(true);} else { //拋異常} |
一旦將lib設置為可選,后續就不會打包進apk,那么我們能不能將其設置為可選從而跳過該異常呢,答案是不能,第一個原因是時機過早,無法替換,第二個原因是待替換部分的代碼過多,并且該邏輯在DependencyManager中,這個類比較關鍵,能不改還是不要改的比較好。于是只能回退到最原始的解決方案,復用gradle下載依賴的邏輯和緩存策略,手動獲取aar,提取jar文件,添加到provided這個scope上。所以這個問題拆分出來就變成了如下問題。
- 如何判斷當前是否在離線模式
- 如何在非離線模式下使用gradle現有代碼獲取最新的依賴文件
- 如何在離線模式下使用現有的gradle緩存
- 在線獲取失敗的情況下使用本地緩存進行重試,如場景:公司maven,外網無法訪問,但是本地有緩存
- SNAPSHOT版本的獲取
- Release版本的獲取
對于第一個問題,如何判斷當前gradle是否在離線模式下,很簡單,這個值位于project.gradle.startParameter中,調用其isOffline()函數即可獲取是否在離線模式下
| 1234567891011 | /** * 獲取是否是離線模式 */StartParameter startParameter = project.gradle.startParameterboolean isOffline = startParameter.isOffline()project.logger.lifecycle("[providedAar] gradle offline: ${isOffline}")if (isOffline) {project.logger.lifecycle("[providedAar] use local cache dependency because offline is enabled")} else {project.logger.lifecycle("[providedAar] use remote dependency because offline is disabled")} |
對于第2,3,4個問題,其解決問題的大致偽代碼如下
| 123456789101112131415161718192021222324252627282930313233343536 | project.getGradle().addListener(new DependencyResolutionListener() {@Override void beforeResolve(ResolvableDependencies dependencies) { //此回調會多次進入,我們只需要解析一次,因此只要進入,就remove,然后執行我們的解析操作 project.gradle.removeListener(this) //遍歷所有依賴進行解析resolveDependencies(project, isOffline)}@Override void afterResolve(ResolvableDependencies resolvableDependencies) {}})def resolveDependencies = {Project project, boolean offline -> //遍歷providedAar的所有依賴 project.getConfigurations().getByName("providedAar").getDependencies().each { def dependency -> //解析依賴,將必要的參數傳入,最后一個參數為是否強制使用本地緩存 boolean matchArtifact = resolveArtifactFromRepositories(project, dependency, offline, false) if (!matchArtifact && offline) { //如果解析不成功并且是離線模式,則輸出日志,提示無法解析 project.logger.lifecycle("[providedAar] can't resolve ${dependency.group}:${dependency.name}:${dependency.version} from local cache, you must disable offline model in gradle")} else if (!matchArtifact && !offline) { //如果解析不成功并且是在線模式,則使用本地緩存進行重試 project.logger.lifecycle("[providedAar] can't resolve ${dependency.group}:${dependency.name}:${dependency.version} from remote, is this dependency correct?") //重試本地緩存,最后一個參數表示強制使用本地緩存 boolean matchArtifactFromRetryLocalCache = resolveArtifactFromRepositories(project, dependency, offline, true) if (matchArtifactFromRetryLocalCache) { //如果本地緩存重試成功了,則輸出日志提醒 project.logger.lifecycle("[providedAar] retry resolve ${dependency.group}:${dependency.name}:${dependency.version} from local cache success, you'd better disable offline model in gradle")}}}} |
于是問題就變成了resolveArtifactFromRepositories函數的實現,該函數中需要做的就是遍歷每一個maven倉庫,判斷依賴是否存在,找到一個就返回,平時我們在gradle中使用each去遍歷,而我們只要找到一個需要的值就返回,因此這里需要用到any去遍歷,找到之后返回true,其大致代碼如下
| 1234567891011121314151617181920 | /** * 從所有倉庫解析,只要找到就返回 */def resolveArtifactFromRepositories = {Project project,Dependency dependency, boolean offline, boolean forceLocalCache -> //從repositories中去找,用any是因為只要找到一個就需要return boolean matchArtifact = project.getRepositories().any { def repository -> //只處理maven if (repository instanceof DefaultMavenArtifactRepository) { boolean resolveArtifactFromRepositoryResult = resolveArtifactFromRepository(project, repository, dependency, offline, forceLocalCache) if (resolveArtifactFromRepositoryResult) { return true}}} return matchArtifact} |
最終代碼變成了resolveArtifactFromRepository函數的實現,該函數的功能主要是從單個maven倉庫中進行解析。其代碼如下
| 1234567891011121314151617181920212223242526272829303132333435363738 | /** * 從單個mavan倉庫解析 */def resolveArtifactFromRepository = {Project project, def repository,Dependency dependency, boolean offline, boolean forceLocalCache -> //從repository對象中創建MavenResolver解析對象MavenResolver mavenResolver = repository.createResolver() //創建依賴的信息持有類,這是一個簡單的java bean對象,反射創建,因為不同gralde版本類名發生了變化 def moduleComponentArtifactMetadata = createModuleComponentArtifactMetaData(mavenResolver, offline, forceLocalCache, dependency.group, dependency.name, dependency.version, "aar", "aar") if (moduleComponentArtifactMetadata != null) { //創建Artifact解析器對象,反射創建,因為一些函數和字段是私有的ExternalResourceArtifactResolver externalResourceArtifactResolver = createArtifactResolver(mavenResolver) if (externalResourceArtifactResolver != null) { //強制本地緩存或者離線模式,走本地緩存 if (forceLocalCache || offline) { //獲取本地緩存查找器,反射創建,因為一些函數和字段是私有的LocallyAvailableResourceFinder locallyAvailableResourceFinder = getLocallyAvailableResourceFinder(externalResourceArtifactResolver) if (locallyAvailableResourceFinder != null) { //從緩存中查找,找到了就返回true boolean fetchFromLocalCacheResult = fetchFromLocalCache(project, locallyAvailableResourceFinder, moduleComponentArtifactMetadata, dependency) if (fetchFromLocalCacheResult) { return true}}} else { //在線模式,走遠程依賴,實際邏輯gradle內部處理,找到了就返回true boolean fetchFromRemoteResult = fetchFromRemote(project, externalResourceArtifactResolver, moduleComponentArtifactMetadata, repository, dependency) if (fetchFromRemoteResult) { return true}}}} return false} |
由resolveArtifactFromRepository函數,拆分問題,于是問題變成了createModuleComponentArtifactMetaData函數、
createArtifactResolver函數,getLocallyAvailableResourceFinder函數,fetchFromLocalCache函數,fetchFromRemote函數的實現邏輯
首先來看createModuleComponentArtifactMetaData函數,具體的邏輯見代碼中的注釋
| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253 | /** * 創建ModuleComponentArtifactMetaData對象,之所以用反射,是因為gradle的不同版本,這個類的名字發生了變化,低版本可能是DefaultModuleComponentArtifactMetaData,而高版本改成了org.gradle.internal.component.external.model.DefaultModuleComponentArtifactMetadata */def createModuleComponentArtifactMetaData = {MavenResolver mavenResolver, boolean offline, boolean forceLocalCache, String group, String name, String version, String type, String extension -> try { //調用靜態函數,傳入group,name,version,返回一個ModuleComponentIdentifier對象ModuleComponentIdentifier componentIdentifier = DefaultModuleComponentIdentifier.newId(group, name, version) //獲得ModuleComponentArtifactMetadata的class對象,之所以用反射,是因為不同的gradle版本,這個類的類名發生了變化,從DefaultModuleComponentArtifactMetaData變成了DefaultModuleComponentArtifactMetadataClass moduleComponentArtifactMetadataClass = null try {moduleComponentArtifactMetadataClass = Class.forName("org.gradle.internal.component.external.model.DefaultModuleComponentArtifactMetaData")} catch (ClassNotFoundException e) { try {moduleComponentArtifactMetadataClass = Class.forName("org.gradle.internal.component.external.model.DefaultModuleComponentArtifactMetadata")} catch (ClassNotFoundException e1) {}} //找不到類則直接返回 if (moduleComponentArtifactMetadataClass == null) { return null} //獲得ModuleComponentArtifactMetadata類的構造函數Constructor moduleComponentArtifactMetadataConstructor = moduleComponentArtifactMetadataClass.getDeclaredConstructor(ModuleComponentArtifactIdentifier.class)moduleComponentArtifactMetadataConstructor.setAccessible(true) //離線模式或者強制使用本地緩存時不需要進行SNAPSHOT處理,gradle能夠查找到SNAPSHOT本地緩存 //在線模式并且版本號是以-SNAPSHOT結尾,進行處理,如果不處理,gradle無法定位當前最新的快照版本 if (!offline && !forceLocalCache && version.toUpperCase().endsWith("-SNAPSHOT")) { //調用MavenResolver對象的findUniqueSnapshotVersion函數,獲取當前SNASHOP版本的最新快照版本 //之所以用反射是因為這個函數不是公共的Method findUniqueSnapshotVersionMethod = MavenResolver.class.getDeclaredMethod("findUniqueSnapshotVersion", ModuleComponentIdentifier.class, ResourceAwareResolveResult.class)findUniqueSnapshotVersionMethod.setAccessible(true)def mavenUniqueSnapshotModuleSource = findUniqueSnapshotVersionMethod.invoke(mavenResolver, componentIdentifier, new DefaultResourceAwareResolveResult()) if (mavenUniqueSnapshotModuleSource != null) { //如果定位到了最新的快照版本,則使用MavenUniqueSnapshotComponentIdentifier對象對componentIdentifier和mavenUniqueSnapshotModuleSource重新包裝,將依賴坐標和時間戳傳入MavenUniqueSnapshotComponentIdentifier mavenUniqueSnapshotComponentIdentifier = new MavenUniqueSnapshotComponentIdentifier(componentIdentifier.getGroup(),componentIdentifier.getModule(),componentIdentifier.getVersion(),mavenUniqueSnapshotModuleSource.getTimestamp()) //創建DefaultModuleComponentArtifactIdentifier對象,只要傳入包裝過的mavenUniqueSnapshotComponentIdentifier對象和name, type, extension即可DefaultModuleComponentArtifactIdentifier moduleComponentArtifactIdentifier = new DefaultModuleComponentArtifactIdentifier(mavenUniqueSnapshotComponentIdentifier, name, type, extension) return moduleComponentArtifactMetadataConstructor.newInstance(moduleComponentArtifactIdentifier)}} else { //如果是release版本或者本地緩存,其版本是唯一的,不需要查找對應的時間戳,因此直接創建DefaultModuleComponentArtifactIdentifierDefaultModuleComponentArtifactIdentifier moduleComponentArtifactIdentifier = new DefaultModuleComponentArtifactIdentifier(componentIdentifier, name, type, extension) return moduleComponentArtifactMetadataConstructor.newInstance(moduleComponentArtifactIdentifier)}} catch (Exception e) {} return null} |
然后是createArtifactResolver函數,這個函數為了獲取ExternalResourceArtifactResolver,實現是調用MavenResolver對象的父類函數createArtifactResolver進行創建
| 123456789101112131415 | /** * 創建ExternalResourceArtifactResolver對象,用反射的原因是這個方法是protected的 */def createArtifactResolver = {MavenResolver mavenResolver -> if (mavenResolver != null) { try { Method createArtifactResolverMethod = ExternalResourceResolver.class.getDeclaredMethod("createArtifactResolver")createArtifactResolverMethod.setAccessible(true) return createArtifactResolverMethod.invoke(mavenResolver)} catch (Exception e) {e.printStackTrace()}} return null} |
接下來是getLocallyAvailableResourceFinder函數,這個函數是為了獲取本地緩存文件的查找器LocallyAvailableResourceFinder對象
| 123456789101112131415 | /** * 反射獲取locallyAvailableResourceFinder,反射獲取的原因是locallyAvailableResourceFinder這個字段是私有的 */def getLocallyAvailableResourceFinder = {ExternalResourceArtifactResolver externalResourceArtifactResolver -> if (externalResourceArtifactResolver != null) { try {Field locallyAvailableResourceFinderField = Class.forName("org.gradle.api.internal.artifacts.repositories.resolver.DefaultExternalResourceArtifactResolver").getDeclaredField("locallyAvailableResourceFinder")locallyAvailableResourceFinderField.setAccessible(true) return locallyAvailableResourceFinderField.get(externalResourceArtifactResolver)} catch (Exception e) {e.printStackTrace()}} return null} |
剩下的兩個函數,是最重要的兩個函數,即fetchFromLocalCache函數和fetchFromRemote函數
先來看怎么從本地緩存中獲取對應的依賴文件,具體邏輯已經在代碼中進行了注釋說明
| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162 | /** * 從本地緩存獲取 */def fetchFromLocalCache = {Project project,LocallyAvailableResourceFinder locallyAvailableResourceFinder, def moduleComponentArtifactMetadata, def dependency -> //獲取本地可選的候選列表LocallyAvailableResourceCandidates locallyAvailableResourceCandidates = locallyAvailableResourceFinder.findCandidates(moduleComponentArtifactMetadata) //如果本地的候選列表不為空 if (!locallyAvailableResourceCandidates.isNone()) { //候選列表的其中一個實現,組合候選列表對象CompositeLocallyAvailableResourceCandidates Class compositeLocallyAvailableResourceCandidatesClass = Class.forName('org.gradle.internal.resource.local.CompositeLocallyAvailableResourceFinder$CompositeLocallyAvailableResourceCandidates') //如果是CompositeLocallyAvailableResourceCandidates的實例 if (compositeLocallyAvailableResourceCandidatesClass.isInstance(locallyAvailableResourceCandidates)) { //獲取這個組合的候選列表字段allCandidates并遍歷它,allCandidates是持有組合對象的字段,我們取其中一個,只要找到然后return就可以了Field allCandidatesField = compositeLocallyAvailableResourceCandidatesClass.getDeclaredField("allCandidates")allCandidatesField.setAccessible(true)List<LocallyAvailableResourceCandidates> allCandidates = allCandidatesField.get(locallyAvailableResourceCandidates) //當list不為空的時候,遍歷,取其中一個文件 if (allCandidates != null) {FileCollection aarFiles = null //用any的原因是為了取一個就返回allCandidates.any {candidate -> //判斷是否是LazyLocallyAvailableResourceCandidates實例 if (candidate instanceof LazyLocallyAvailableResourceCandidates) { //如果該候選列表存在文件,則獲取文件,然后過濾aar文件,返回 if (!candidate.isNone()) { //getFiles函數獲取文件列表Method getFilesMethod = LazyLocallyAvailableResourceCandidates.class.getDeclaredMethod("getFiles")getFilesMethod.setAccessible(true)List<File> candidateFiles = getFilesMethod.invoke(candidate) //過濾aar文件aarFiles = project.files(candidateFiles).filter {it.name.endsWith(".aar")} //如果不為空,表示找到了,返回true if (!aarFiles.empty) { return true}}}} //如果找到了aar文件,則提取jar,添加到provided的scope上 if (!aarFiles.empty) { //遍歷找到的aar文件列表aarFiles.files.each {File aarFile ->FileCollection jarFromAar = project.zipTree(aarFile).filter {it.name == "classes.jar"} //添加到provided的scope上 project.getDependencies().add("provided", jarFromAar) //輸出日志信息 project.logger.lifecycle("[providedAar] convert aar ${dependency.group}:${dependency.name}:${dependency.version} to jar and add provided file ${jarFromAar.getAsPath()} from ${aarFile}")} return true}}}} return false} |
然后是從遠程獲取對應的依賴,更新本地文件,具體的實現邏輯由gradle內部取保證,我們只要調用其函數即可。
| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051 | /** * 從遠程獲取 */def fetchFromRemote = {Project project,ExternalResourceArtifactResolver externalResourceArtifactResolver, def moduleComponentArtifactMetadata, def repository, def dependency -> try { if (moduleComponentArtifactMetadata != null) { //判斷當前maven倉庫上是否存在該依賴 boolean artifactExists = externalResourceArtifactResolver.artifactExists(moduleComponentArtifactMetadata, new DefaultResourceAwareResolveResult()) //如果該遠程倉庫存在該依賴 if (artifactExists) { //進行依賴解析,獲得本地可用文件LocallyAvailableExternalResource locallyAvailableExternalResource = externalResourceArtifactResolver.resolveArtifact(moduleComponentArtifactMetadata, new DefaultResourceAwareResolveResult()) if (locallyAvailableExternalResource != null) { //獲取解析到的文件 File aarFile = null try { def locallyAvailableResource = locallyAvailableExternalResource.getLocalResource() if (locallyAvailableResource != null) {aarFile = locallyAvailableResource.getFile()}} catch (Exception e) { //高版本gradle兼容 try {aarFile = locallyAvailableExternalResource.getFile()} catch (Exception e1) {}} //該依賴對應的文件存在的話,提取jar,添加到provided的scope上 if (aarFile != null && aarFile.exists()) {FileCollection jarFromAar = project.zipTree(aarFile).filter {it.name == "classes.jar"} //添加到provided的scope上 project.getDependencies().add("provided", jarFromAar) //輸出日志 project.logger.lifecycle("[providedAar] convert aar ${dependency.group}:${dependency.name}:${dependency.version} in ${repository.url} to jar and add provided file ${jarFromAar.getAsPath()} from ${aarFile}") return true}}}}} catch (Exception e) { //可能會出現ssl之類的異常,無視掉} return false} |
將以上代碼進行組合,就得到了CompatPlugin.groovy中的providedAarCompat函數,搜索該文件中的providedAarCompat函數,即最終組合完成的代碼。
可以看到,整個實現邏輯還是十分復雜的,尤其是本地緩存依賴和在線依賴的解析部分,以及SNAPSHOT版本時間戳的獲取及對應類的包裝。需要理解整個過程還是需要自己過一遍整個代碼。
不過遺憾的是,這部分的實現是不支持傳遞依賴的,如果要支持傳遞依賴,則會涉及到configuration的傳遞,邏輯會變得更加復雜,因此這里就不處理了,實際上問題也不大,只需要手動聲明傳遞依賴即可。
總結
最重要的還是解決問題的思路,同一個問題,可能有不同的解決方法,需要整體考慮選擇何種方式去解決。解決問題的過程就是不斷學習的過程,通過實現這個需求,對gradle的依賴管理策略、緩存也更加熟悉了。
http://fucknmb.com/2017/12/24/Android-application%E4%B8%AD%E4%BD%BF%E7%94%A8provided-aar%E5%B9%B6%E6%B2%A1%E6%9C%89%E9%82%A3%E4%B9%88%E7%AE%80%E5%8D%95/總結
以上是生活随笔為你收集整理的Android application 中使用 provided aar 并没有那么简单的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Google Archive Patch
- 下一篇: Java-gt;Android并发编程引