将相关业务代码迁移到各自 module 下,同样也需要分别在两个 module 创建新的 CardManager 进行 Card 的注册和在 Application 调用 CardManager.init()
// module BusinessCN
public class CnCardManager {
public static void init() {
CardManager.registerCard(new CNACard());
CardManager.registerCard(new CNBCard());
}
}
// module BusinessExp
public class ExpCardManager {
public static void init() {
CardManager.registerCard(new ExpACard());
CardManager.registerCard(new ExpBCard());
}
}
这里关于 Application 需要说明下,因为这里使用了多个 Application 会在编译的时候导致冲突,需要特别处理下,以 BusinessCn 为例说明:
BusinessLayer 中注册的 LayerApplication 和 BusinessCn 中注册的 CnApplication 发生冲突,需要在 BusinessCn 的 AndroidManifest.xml 中使用 tools:replace 声明使用 CnApplication 替代 LayerApplication。
<manifest xmlns:android=“http://schemas.android.com/apk/res/android”
xmlns:tools=“http://schemas.android.com/tools”
package=“com.wbh.decoupling.businesscn” >
<application
android:name=“.CnApplication”
tools:replace=“android:name”>
因为 LayerApplication 被替换了,所以 CnApplication 和 ExpApplication 都需要继承 LayerApplication,这样才能保证 LayerApplication 中的代码会执行。
两个 module 的代码结构如下:
2.3 调整主项目
在主项目中,src 中的代码全部都迁移到各自对应的 module 里了,所以 src 中是没有代码的,但是在 build.gradle 里,我们通过渠道打包的方式,针对国内和国外两种情况各自打出不同的 apk 包,build.gradle 的配置如下:
android {
// … …
flavorDimensions ‘test’
productFlavors {
cn {}
exp {}
}
}
dependencies {
// … …
cnImplementation project(path: ‘:businesscn’)
expImplementation project(path: ‘:businessexp’)
}
通过 gradlew assembleCnRelease 和 gradlew assembleExpRelease 就能打包出不同的 apk 包,且不会出现 cn 的 apk 包中有 exp 的 Card 代码或者 exp 的 apk 包中有 cn 的 Card 代码。
2.4 小结
小结下上述重构后的代码结构。
-
module BusinessLayer 中包含了公共的所有业务代码;
-
modlue BusinessCn 中只包含国内的业务代码,并且依赖 BusinessLayer;
-
module BusinessExp 中只包含国外的业务代码,并且依赖 BusinessLayer;
-
主项目 app 中,通过渠道打包的方式,cn 渠道依赖 BusinessCn, exp 渠道依赖 BussinessExp。
到这里,其实代码已经做了很好的隔离,但这毕竟只是 Demo,场景比较简单,而实际工作中的业务场景往往比 Demo 会复杂得多。
仔细观察每个 module, 都有一个 XXCardManager
类,都有一个 init()
方法,方法中都是 CardManager.registerCard(...)
的形式来注册每一个 Card。从这个角度来改造,我们使用 APT + javapoet 的方式来自动生成这些代码。
3.1 明确生成的代码内容
我们先整理下思路,明确好要生成的代码是怎样的:
-
为了方便后续的管理(看到后面的内容就明白了),对于生成的代码,统一放在
com.wbh.decoupling.generate
包下; -
多个 module,所以会生成多个 CardManager,所以需要在类名后加个后缀以作区分;
// com.wbh.decoupling.generate
public class CardManager_??? {
public static void init() {
CardManager.registerCard(new ???Card());
CardManager.registerCard(new ???Card());
}
}
3.2 APT 的引入
3.2.1 创建注解 module
创建一个 java module,名为 annotation,用来存放注解相关的代码。
创建一个 Register 注解:
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface Register {
}
3.2.2 创建注解处理器 module
创建一个 Java module,名为 compile,用来存放注解处理相关代码。
- build.gradle 中引入以下三个依赖
dependencies {
implementation project(path: “:annotation”)
implementation ‘com.google.auto.service:auto-service:1.0-rc3’
annotationProcessor ‘com.google.auto.service:auto-service:1.0-rc4’
implementation ‘com.squareup:javapoet:1.8.0’
}
-
auto-service 是为了更方便的注册 processor
-
javapoet 是为了更方便的生成代码
- 创建注解处理器,并且注册该处理器
@AutoService(Processor.class)
public class RegisterProcessor extends AbstractProcessor{
private Map<String, TypeElement> mMap = new HashMap<>();
private ProcessingEnvironment mProcessingEnvironment;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
mProcessingEnvironment = processingEnvironment;
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public Set getSupportedAnnotationTypes() {
return Collections.singleton(Register.class.getCanonicalName());
}
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
for (Element element : roundEnvironment.getElementsAnnotatedWith(Register.class)) {
processElement(element);
}
if (roundEnvironment.processingOver()) {
generateCode();
}
return true;
}
private void processElement(Element element) {
TypeElement typeElement = (TypeElement) element;
String qualifiedName = typeElement.getQualifiedName().toString();
if (mMap.get(qualifiedName) == null) {
mMap.put(qualifiedName, typeElement);
}
}
private void generateCode() {
if (mMap.isEmpty()) return;
Set set = new HashSet<>();
set.addAll(mMap.values());
GenerateClassHelper helper = new GenerateClassHelper(mProcessingEnvironment, set);
helper.generateCode();
}
}
- 使用 @AutoService 的方式来自动注册此处理器,会在 build/classes/java/main 下生成 META-INF/services/javax.annotation.processing.Processor 文件:
打开此文件内容如下:
com.wbh.decoupling.compile.RegisterProcessor
如果不使用注解方式注册,就需要自己手动实现 META-INF/services/javax.annotation.processing.Processor 内容。
- process() 方法中收集所有被 Register 注解的 Element,然后通过 GenerateClassHelper 类来生成代码。因为 process() 方法会执行多次,所以使用 roundEnvironment.processingOver() 判断只有在最后的时候再去生成代码。
- 我们来实现 GenerateClassHelper 的代码:
public class GenerateClassHelper {
private static final String PACKAGE_NAME = “com.wbh.decoupling.generate”;
private static final String CLASS_NAME_PREFIX = “CardManager_”;
private static final String METHOD_NAME = “init”;
private static final ClassName CARD_MANAGER = ClassName.get(“com.wbh.decoupling.businesslayer.card”, “CardManager”);
private Filer mFiler;
private Elements mElementUtils;
private Set mElementSet;
public GenerateClassHelper(ProcessingEnvironment processingEnvironment, Set set) {
mFiler = processingEnvironment.getFiler();
mElementUtils = processingEnvironment.getElementUtils();
mElementSet = set;
}
public void generateCode() {
try {
JavaFile javaFile = JavaFile.builder(PACKAGE_NAME, getGenTypeSpec()).build();
javaFile.writeTo(mFiler);
} catch (IOException e) {
e.printStackTrace();
}
}
private TypeSpec getGenTypeSpec() {
return TypeSpec.classBuilder(getClassName())
.addModifiers(Modifier.PUBLIC)
.addMethod(getGenInitMethodSpec())
.build();
}
private String getClassName() {
for (TypeElement element : mElementSet) {
// 姑且用获取到的第一个类的md5来做生成的类名的后缀,实际是不合理,可能会有问题
return CLASS_NAME_PREFIX + EncryptHelper.md5String(mElementUtils.getPackageOf(element).getQualifiedName().toString());
}
return “”;
}
private MethodSpec getGenInitMethodSpec() {
String format = “$T.registerCard(new $T())”;
CodeBlock.Builder builder = CodeBlock.builder();
for (TypeElement typeElement : mElementSet) {
ClassName className = ClassName.get(typeElement);
builder.addStatement(format, CARD_MANAGER, className);
}
CodeBlock codeBlock = builder.build();
return MethodSpec.methodBuilder(METHOD_NAME)
.addModifiers(Modifier.PUBLIC)
.addModifiers(Modifier.STATIC)
.addCode(codeBlock)
.build();
}
}
GenerateClassHelper 使用 JavaPoet 库来生成代码,JavaFile、TypeSpec、MethodSpec、CodeBlock 等,都是 JavaPoet 的,代码里按照我们想要生成的 CardManager_??? 类来编写。
3.2.3 业务中使用自定义注解和解析器
在 BusinessLayer、BusinessCn、BusinessExp 中的各个 build.gradle 中都依赖 annotation 和 compile 两个 module
dependencies {
implementation project(path: ‘:annotation’)
annotationProcessor project(path: ‘:compile’)
}
然后对需要注册的 Card 类加上 @Register 的注解,比如:
@Register
public class ACard implements ICard {
}
最后我们运行下代码,就能自动生成 CardManager 相关的代码,它在 build/generated/source/apt 目录下。
我们来看下 BusinessLayer 的目录:
打开 CardManager_becf3fc7606c9b461025f1def7ff27ac 文件:
package com.wbh.decoupling.generate;
import com.wbh.decoupling.businesslayer.card.ACard;
import com.wbh.decoupling.businesslayer.card.BCard;
import com.wbh.decoupling.businesslayer.card.CardRegister;
public class CardManager_becf3fc7606c9b461025f1def7ff27ac {
public static void init() {
CardManager.registerCard(new ACard());
CardManager.registerCard(new BCard());
}
}
其他 module 中也会生成类似的此文件,这里就不一一展示了。
既然 CardManager 已经是通过 APT 自动生成了,那么我们手写的各个 module 中的 CardManager 类就可以删掉了。
3.3 调用注解生成的各个类文件
那么问题来了,CardManager_??? 这几个文件自动生成后,该怎么调用呢?我们不知道现在以及未来可能有多少个 module,也不知道这些 CardManager_??? 的具体名字。
一个简单粗暴的方法,就是遍历 dex 文件的方式。
这种方式主要是通过遍历 dex 文件中所有以com.wbh.decoupling.generate
开头的类,这个也是之前说到要将所有生成的类都放在这个包下的原因,然后反射的方式来调用这些类的 init() 方法。
在 BusinessLayer 里创建 CrossCompileUtils 类来完成这些操作,然后在 Application 中调用 CrossCompileUtils.init() 方法执行此过程,看下代码的实现:
public class CrossCompileUtils {
private static final String GENERATE_CODE_PACKAGE_NAME = “com.wbh.decoupling.generate”;
private static final String METHOD_NAME = “init”;
public static void init(Context context) {
try {
List targetClassList = getTargetClassList(context, GENERATE_CODE_PACKAGE_NAME);
for (String className : targetClassList) {
Class<?> cls = Class.forName(className);
Method method = cls.getMethod(METHOD_NAME);
method.invoke(null);
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 获取以 target 为开头的所有类
private static List getTargetClassList(Context context, String target) {
List classList = new ArrayList<>();
try {
ApplicationInfo info = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0);
String path = info.sourceDir;
DexFile dexFile = new DexFile(path);
Enumeration entries = dexFile.entries();
while (entries.hasMoreElements()) {
String name = entries.nextElement();
if (name.startsWith(target)) {
classList.add(name);
}
}
} catch (Exception e) {
e.printStackTrace();
}
return classList;
}
}
这种方式有个很明显的缺陷,就是整个过程是在运行时进行的,当代码量越多,此过程越耗时,对冷启动会有很明显的影响。
这种方式主要通过在编译期插入相应字节码的方式,将耗时放在编译期以优化运行时效率。
4.1 明确插桩的代码
我们先整理接下来的主要任务,在编译期生成类AsmCrossCompileUtils
,这个类的有个 init() 方法,这个方法会调用所有 CardManager_XXX.init():
package com.wbh.decoupling.generate;
public class AsmCrossCompileUtils {
public static void init() {
CardManager_becf3fc7606c9b461025f1def7ff27ac.init();
CardManager_dc2db21188334cfca97494d99700395.init();
}
}
假定以上的类已经生成了,那么 CrossCompileUtils 可以改为:
public class CrossCompileUtils {
public static void init(Context context) {
initByAsm();
}
private static void initByAsm() {
try {
Class cls = Class.forName(“com.wbh.decoupling.generate.AsmCrossCompileUtils”);
Method method = cls.getMethod(METHOD_NAME);
method.invoke(null);
} catch (Exception e) {
e.printStackTrace();
}
}
}
这种方式只需要通过一次反射,相比遍历 dex 的方式,性能就好很多了。
这种方式有两个关键点:一个是如何获取注解生成的各个类,一个是如何生成 AsmCrossCompileUtils 类。
4.2 自定义 gradle plugin
我们创建一个 module(哪一种类型都可以) ,姑且命名为 WPlugin 作为插件名字,然后将 src 目录下的所有文件都删除,只保留 main 目录,main 目录下的文件也全部删除,然后在 main 目录下创建 groovy 目录(这是因为 gradle 的语法是 groovy),我们编写的插件代码就在这目录下。
接下来我们需要配置下 build.gradle,将原本的 build.gradle 内容都删除,配置如下:
apply plugin: ‘groovy’ // 因为plugin是由groovy语法编写,所以需要应用此插件
dependencies {
implementation fileTree(dir: ‘libs’, include: [‘*.jar’])
implementation gradleApi() // 自定义插件中需要用到 gradle 的各种 api
}
sourceCompatibility = “1.7”
targetCompatibility = “1.7”
group = ‘com.wbh.decoupling.plugin’ // 自定义插件的组别
version = ‘1.0.0’ // 自定义插件的版本号
然后我们在 groovy 目录下创建一个 groovy 文件,命名为 WPlugin:
import org.gradle.api.Plugin
import org.gradle.api.Project
class WPlugin implements Plugin {
@Override
void apply(Project project) {
println(‘this is my WPlugin’)
}
}
插件代码执行的入口就是这里的 apply() 方法,不过我们还得需要注册这个插件。在 main 目录下创建 resources 目录,在该目录下创建 META-INF.gradle-plugins 目录,然后在该目录下创建 xxx.properties 文件,这里的 xxx 表示当我们在其他项目应用此插件时的名字 apply plugin: ‘xxx’,我们还是将之命名为 WPlugin,在这个文件里编写内容如下:
implementation-class=WPlugin
implementation-class
用来配置插件入口类的,这里也就是配置我们自定义的 WPlugin 类。
至此整个自定义插件的结构已经完成了,在看下这个目录结构如下:
4.3 发布 plugin
在使用自定义的插件前,得先发布这个插件。
这里我们将插件发布到本地目录下,在根目录下创建 maven 目录,用来存放我们发布的插件,然后在 build.gradle 配置如下:
apply plugin: ‘maven’
uploadArchives {
repositories {
mavenDeployer {
repository(url: uri(‘…/maven’)) // 指定发布到的目录
}
}
}
然后点击以下 uploadArchives ,运行发布插件。
接下来我们就能在 maven 目录下看到我们发布的插件了。
4.4 使用自定义的 plugin
在 app 这个 module 中使用自定义的插件 WPlugin,在 build.gradle 中配置如下:
apply plugin: WPlugin
buildscript {
repositories {
maven {
url uri(‘…/maven’)
}
}
dependencies {
classpath ‘com.wbh.decoupling.plugin:WPlugin:1.0.0’
}
}
然后 Sync Gradle,我们就能在 Build 窗口中看到 WPlugin 打印的内容。
至此,我们说完了如何自定义、发布以及使用 gradle 插件。
接下来我们该完善下这个 WPlugin 的内容。
4.5 自定义 Transform
编写 Transform,需要使用到 gradle 的 API,所以我们需要在 build.gradle 添加如下的依赖:
implementation’com.android.tools.build:gradle-api:3.4.2’
注意:由于 gradle 插件使用了上述依赖,所以在使用该插件的项目的build.grdle里需要配置如下:
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath ‘com.android.tools.build:gradle:3.4.2’
}
}
接着创建一个类继承 Transform :
class WTransform extends Transform {
@Override
String getName() {
return null
}
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return null
}
@Override
Set<? super QualifiedContent.Scope> getScopes() {
return null
}
@Override
boolean isIncremental() {
return false
}
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
}
}
-
getName(): 返回自定义的 Transform 名字
-
geInputTypes(): 指定 Transform 要处理的输入类型,主要有
QualifiedContent.DefaultContentType.CLASSES
和QualifiedContent.DefaultContentType.RESOURCES
两种类型,对应为 .class 文件 和 java 资源文件 -
getScopes():指定输入文件的所属的范围。
public interface QualifiedContent {
enum Scope implements ScopeType {
/** Only the project (module) content */
PROJECT(0x01),
/** Only the sub-projects (other modules) */
SUB_PROJECTS(0x04),
/** Only the external libraries */
EXTERNAL_LIBRARIES(0x10),
/** Code that is being tested by the current variant, including dependencies */
TESTED_CODE(0x20),
/** Local or remote dependencies that are provided-only */
PROVIDED_ONLY(0x40),
}
}
-
isIncremental():当前是支持增量编译
-
transform():执行转化的方法,通过参数 transformInvocation 可以获取到该节点的输入和输出:
Collection inputs = transformInvocation.getInputs()
TransformOutputProvider outputProvider = transformInvocation.getOutputProvider()
TransformInput 分为两类,一类是 Jarinput,TransformInput#getJarInputs()
,一类是 DirectoryInput,TransformInput#getDirectoryInputs()
。而 TransformOutputProvider 指向了文件/目录输出路径。
4.6 注册 Transform
要使我们自定义的 Transform 生效参与到编译过程中,还需要注册 WTransform,注册过程很简单,只要在 Plugin 中注册即可。
class WPlugin implements Plugin {
@Override
void apply(Project project) {
println(‘this is my WPlugin’)
def andr = project.extensions.getByName(‘android’)
andr.registerTransform(new WTransform())
}
}
4.7 完善 Transform
import com.android.build.api.transform.DirectoryInput
import com.android.build.api.transform.JarInput
import com.android.build.api.transform.QualifiedContent
import com.android.build.api.transform.Transform
import com.android.build.api.transform.TransformException
import com.android.build.api.transform.TransformInput
import com.android.build.api.transform.TransformInvocation
import com.android.build.api.transform.TransformOutputProvider
import com.android.build.api.transform.Format
import com.android.utils.FileUtils
class WTransform extends Transform {
@Override
String getName() {
return ‘WTransform’
}
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return Collections.singleton(QualifiedContent.DefaultContentType.CLASSES)
}
@Override
Set<? super QualifiedContent.Scope> getScopes() {
Set<? super QualifiedContent.Scope> set = new HashSet<>()
set.add(QualifiedContent.Scope.PROJECT)
set.add(QualifiedContent.Scope.SUB_PROJECTS)
return set
}
@Override
boolean isIncremental() {
return false
}
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
Collection inputs = transformInvocation.getInputs()
TransformOutputProvider outputProvider = transformInvocation.getOutputProvider()
inputs.each {
it.getJarInputs().each { jarInput ->
transformJar(jarInput, outputProvider)
}
it.getDirectoryInputs().each { dirInput ->
transformDir(dirInput, outputProvider)
}
}
}
private static void transformJar(JarInput jarInput, TransformOutputProvider outputProvider) {
File dstFile = outputProvider.getContentLocation(
jarInput.getName(),
jarInput.getContentTypes(),
jarInput.getScopes(),
Format.JAR)
FileUtils.copyFile(jarInput.getFile(), dstFile)
println('jarInputFile ==> ’ + jarInput.file.absolutePath)
println('dstFile ==> ’ + dstFile.absolutePath)
}
private static void transformDir(DirectoryInput dirInput, TransformOutputProvider outputProvider) {
File dstDir = outputProvider.getContentLocation(
dirInput.getName(),
dirInput.getContentTypes(),
dirInput.getScopes(),
Format.DIRECTORY)
FileUtils.copyDirectory(dirInput.getFile(), dstDir)
println('directory input ==> ’ + dirInput.file.absolutePath)
println('dstDir ==> ’ + dstDir.absolutePath)
}
}
transform() 方法做的事很简单,就是获取所有的输入文件/目录,然后拷贝到输出路径。
我们重新 uploadArchives 这个插件,然后 clear project,在重新构建项目,就能在 build 窗口看到我们打印的输出:
根据打印的输入,我们能在相应的目录下找到 WTransform 生成的文件:
由于每次编译所有文件/目录都会重新复制一次,所以加上支持增量编译:
class WTransform extends Transform {
@Override
boolean isIncremental() {
return true
}
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation)
boolean isIncremental = transformInvocation.isIncremental()
println('isIncremental ==> ’ + isIncremental)
Collection inputs = transformInvocation.getInputs()
TransformOutputProvider outputProvider = transformInvocation.getOutputProvider()
if (!isIncremental) {
outputProvider.deleteAll()
}
inputs.each {
it.getJarInputs().each { jarInput ->
transformJar(jarInput, outputProvider, isIncremental)
}
it.getDirectoryInputs().each { dirInput ->
transformDir(dirInput, outputProvider, isIncremental)
}
}
}
private static void transformJar(JarInput jarInput, TransformOutputProvider outputProvider, boolean isIncremental) {
File dstFile = outputProvider.getContentLocation(
jarInput.getName(),
jarInput.getContentTypes(),
jarInput.getScopes(),
Format.JAR)
println('jar input ==> ’ + jarInput.file.absolutePath)
println('dstFile ==> ’ + dstFile.getAbsolutePath())
if (!isIncremental) {
FileUtils.copyFile(jarInput.file, dstFile)
return
}
Status status = jarInput.status
switch (status) {
case Status.NOTCHANGED:
break
case Status.ADDED:
case Status.CHANGED:
FileUtils.deleteIfExists(dstFile)
FileUtils.copyFile(jarInput.file, dstFile)
break
case Status.REMOVED:
FileUtils.deleteIfExists(dstFile)
break
}
}
private static void transformDir(DirectoryInput dirInput, TransformOutputProvider outputProvider, boolean isIncremental) {
File dstDir = outputProvider.getContentLocation(
dirInput.getName(),
dirInput.getContentTypes(),
dirInput.getScopes(),
Format.DIRECTORY)
println('directory input ==> ’ + dirInput.file.absolutePath)
println('dstDir ==> ’ + dstDir.absolutePath)
if (!isIncremental) {
FileUtils.copyDirectory(dirInput.getFile(), dstDir)
return
}
String srcDirPath = dirInput.getFile().getAbsolutePath()
String dstDirPath = dstDir.getAbsolutePath()
Map<File, Status> fileStatusMap = dirInput.getChangedFiles()
fileStatusMap.entrySet().each { Map.Entry<File, Status> changedFileMapEntry ->
Status status = changedFileMapEntry.getValue()
File inputFile = changedFileMapEntry.getKey()
println('change file: ’ + inputFile.getAbsolutePath() + ", status: " + status)
String dstFilePath = inputFile.getAbsolutePath().replace(srcDirPath, dstDirPath)
File dstFile = new File(dstFilePath)
switch (status) {
case Status.NOTCHANGED:
break
case Status.REMOVED:
FileUtils.deleteIfExists(dstFile)
break
case Status.ADDED:
case Status.CHANGED:
FileUtils.deleteIfExists(dstFile)
FileUtils.copyFile(inputFile, dstFile)
break
}
}
}
}
增量编译的文件都有个状态 Status,根据文件的状态做相应不同对应操作即可。
-
Status.NOTCHANGED:该文件没有变动,所以不需要重新复制一份
-
Status.REMOVED:该文件被删除,所以对应输出文件也要删除
-
Status.ADDED:该文件为新加的,所以需要复制一份到输出路径
-
Status.CHANGED:该文件被修改,所以需要重新复制一份到输出路径
修改完后,我们测试下效果,在执行一次完整编译后,创建一个Test.java 类,再执行一次编译,能看到以下打印结果:
Task :app:transformClassesWithWTransformForExpDebug
isIncremental ==> true
jar input ==> /Users/wubohua/work/project/Android/Application_decoupling2/annotation/build/libs/annotation.jar
dstFile ==> /Users/wubohua/work/project/Android/Application_decoupling2/app/build/intermediates/transforms/WTransform/exp/debug/2.jar
jar input ==> /Users/wubohua/work/project/Android/Application_decoupling2/businessexp/build/intermediates/runtime_library_classes/debug/classes.jar
dstFile ==> /Users/wubohua/work/project/Android/Application_decoupling2/app/build/intermediates/transforms/WTransform/exp/debug/0.jar
jar input ==> /Users/wubohua/work/project/Android/Application_decoupling2/businesslayer/build/intermediates/runtime_library_classes/debug/classes.jar
dstFile ==> /Users/wubohua/work/project/Android/Application_decoupling2/app/build/intermediates/transforms/WTransform/exp/debug/1.jar
directory input ==> /Users/wubohua/work/project/Android/Application_decoupling2/app/build/intermediates/javac/expDebug/compileExpDebugJavaWithJavac/classes
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
总结
最后为了帮助大家深刻理解Android相关知识点的原理以及面试相关知识,这里放上相关的我搜集整理的24套腾讯、字节跳动、阿里、百度2019-2021面试真题解析,我把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包知识脉络 + 诸多细节。
还有 高级架构技术进阶脑图、Android开发面试专题资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
2021年虽然路途坎坷,都在说Android要没落,但是,不要慌,做自己的计划,学自己的习,竞争无处不在,每个行业都是如此。相信自己,没有做不到的,只有想不到的。祝大家2021年万事大吉。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
sers/wubohua/work/project/Android/Application_decoupling2/businesslayer/build/intermediates/runtime_library_classes/debug/classes.jar
dstFile ==> /Users/wubohua/work/project/Android/Application_decoupling2/app/build/intermediates/transforms/WTransform/exp/debug/1.jar
directory input ==> /Users/wubohua/work/project/Android/Application_decoupling2/app/build/intermediates/javac/expDebug/compileExpDebugJavaWithJavac/classes
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-Xo4CEsil-1712335565838)]
[外链图片转存中…(img-lFeYxarL-1712335565839)]
[外链图片转存中…(img-N4wf3Qds-1712335565839)]
[外链图片转存中…(img-NrE5iCPk-1712335565839)]
[外链图片转存中…(img-dW9MxIuJ-1712335565839)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)
总结
最后为了帮助大家深刻理解Android相关知识点的原理以及面试相关知识,这里放上相关的我搜集整理的24套腾讯、字节跳动、阿里、百度2019-2021面试真题解析,我把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包知识脉络 + 诸多细节。
还有 高级架构技术进阶脑图、Android开发面试专题资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。
[外链图片转存中…(img-MP2VfOay-1712335565840)]
[外链图片转存中…(img-kQsSpz7C-1712335565840)]
[外链图片转存中…(img-mwab3ksN-1712335565840)]
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
2021年虽然路途坎坷,都在说Android要没落,但是,不要慌,做自己的计划,学自己的习,竞争无处不在,每个行业都是如此。相信自己,没有做不到的,只有想不到的。祝大家2021年万事大吉。