| — | — |
| @Override | 表示当前的方法定义将覆盖超类中的方法,如果方法拼写错误或者方法签名不匹配,编译器便会提出错误提示 |
| @Deprecated | 表示当前方法已经被弃用,如果开发者使用了注解为它的元素,编译器便会发出警告信息 |
| @SuppressWarnings | 可以关闭不当的编译器警告信息 |
2. java 提供的四种元注解和一般注解
所谓元注解(meta-annotation)也是一种注解,只不过这种注解负责注解其他的注解。所以再说元注解之前我们来看一下普通的注解:
public @interface LogClassMessage {
}
这是一个最普通的注解,注解的定义看起来很像一个接口,在 interface 前加上 @ 符号。事实上在语言级别上,注解也和 java 中的接口、类、枚举是同一个级别的,都会被编译成 class 文件。而前面提到的元注解存在的目的就是为了修饰这些普通注解,但是要明确一点,元注解只是给普通注解提供了作用,并不是必须存在的。
| java 提供的元注解 | 作用 |
| — | — |
| @Target | 定义你的注解应用到什么地方〔详见下文解释〕 |
| @Retention | 定义该注解在哪个级别可用〔详见下文解释〕 |
| @Documented | 将此注解包含在 javadoc 中 |
| @Inherited | 允许子类继承超类中的注解 |
〔1〕@Target使用的时候添加一个 ElementType 参数,表示当前注解可以应用到什么地方,即可以指定一种,也可以同时指定多种,使用方法如下:
//当前的注解只能应用到类、接口(包括注解)、enum上面
@Target(ElementType.TYPE)
public @interface LogClassMessage {
}
//当前的注解只能应用到方法和成员变量上面
@Target({ElementType.METHOD,ElementType.FIELD})
public @interface LogClassMessage {
}
下面来看一下 ElementType 全部的参数:
| ElementType 参数 | 说明 |
| — | — |
| ElementType.CONSTRUCTOR | 构造器的声明 |
| ElementType.FIELD | 域的声明(包括enum的实例) |
| ElementType.LOCATION_VARLABLE | 局部变量的声明 |
| ElementType.METHOD | 方法的声明 |
| ElementType.PACKAGE | 包的声明 |
| ElementType.PARAMETER | 参数的声明 |
| ElementType.TYPE | 类、接口(包括注解类型)、enum声明 |
〔2〕@Retention用来注解在哪一个级别可用,需要添加一个 RetentionPolicy 参数,用来表示在源代码中(SOURCE),在类文件中(CLASS)或者运行时(RUNTIME):
//当前注解运行时可用
@Retention(RetentionPolicy.RUNTIME)
public @interface LogClassMessage {
}
下面来看一下 RetentionPolicy 全部的参数:
| RetentionPolicy 参数 | 说明 |
| — | — |
| RetentionPolicy.SOURCE | 注解将被编译器丢弃,只能存于源代码中 |
| RetentionPolicy.CLASS | 注解在class文件中可用,能够存于编译之后的字节码之中,但会被VM丢弃 |
| RetentionPolicy.RUNTIME | VM在运行期也会保留注解,因此运行期注解可以通过反射获取注解的相关信息 |
在注解中,一般都会包含一些元素表示某些值,并且可以为这些元素设置默认值,没有元素的注解也称为标记注解(marker annotation)
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD,ElementType.FIELD})
public @interface LogClassMessage {
public int id () default -1;
public String message() default “”;
}
注:虽然上面的 id 和 message 定义和接口的方法定义很类似,但是在注解中将 id 和 message 称为:int元素id , String 元素 message。而且注解元素的类型是有限制的,并不是任何类型都可以,主要包括:基本数据类型(理论上是没有基本类型的包装类型的,但是由于自动封装箱,所以也不会报错)、String 类型、enum 类型、Class 类型、Annotation 类型、以及以上类型的数组,(小伙伴们没有等字哦,说明目前注解的元素类型只支持上面列出的这几种),否则编译器便会提示错误。
invalid type ‘void ’ for annotation member //例如注解类型为void的错误信息
对于默认值限制 ,Bruce Eckel (美) 在其书中是这样描述的:编译器对元素的默认值有些过分挑剔,首先,元素不能有不确定的值。也就是说,元素必须要么具有默认值,要么在使用注解时提供注解的值。其次,对于非基本类型的元素,无论在源代码声明中,或者在注解接口中定义默认值时,都不能以 null 作为其值。这个约束使得处理器很难表现一个元素的存在或缺失的状态,因为在每个注解的声明中,所有元素都存在,并且都具有相应的值。为了绕开这个约束,我们只能自己定义一些特殊的值,例如空字符串或者负数,以此表示某个元素的不存在,这算得上是一个习惯用法。
3. 参考下系统的标准注解
怎么说呢,接触一种新的知识的途径有很多,可能每一种的结果都是大同小异的,都能让你学到东西,但是实现的方式、实现过程中的规范、方法和思路却并不一定是最佳的,本人选择的是借鉴源码,效果还不错,在这里推荐给大家。
上文讲到的是注解的基本语法,那么系统是怎么用的呢?首先让我们来看一下使用频率最高的 @Override :
@Target(ElementType.METHOD) @Retention(RetentionPolicy.SOURCE) public @interface Override { }
-
1
-
2
-
3
-
4
〔1〕首先系统定义一个没有元素的标记注解 Override ,随后使用元注解 @Target 指明 Override 注解只能应用于方法之上(你可以细想想,是不是在我们实际使用这个注解的时候,只能是重写的方法,没有见过重写类或者字段的吧),使用注解 @Retention 表示当前注解只能存在源代码中,并不会出现在编译之后的 class 文件之中。
@Override protected void onResume() { super.onResume(); }
-
1
-
2
-
3
-
4
〔2〕在活动 activity 中我们可以重写 onResume() 方法,添加注解 @override 之后编译器便会去检查父类中是否存在相同方法,如果不存在便会报错。
〔3〕也许到这里你会感到很疑惑,注解到底是怎么工作的,怎么系统这样定义一个注解 Override 它就能工作了?黑魔法吗,擦擦,完成看不到实现过程嘛(泪流满面),经过查阅了一些资料(非权威)了解到,其实处理过程都编写在了编译器里面,也就是说编译器已经给我们写好了处理方法,当编译器进行检查的时候就会调用相应的处理方法。
4. 注解处理器
介绍之前,先引用 Jeremy Meyer 的一段话:如果没有用来读取注解的工具,那么注解也不会比注释更有用。使用注解的过程中,很重要的一个部分就是创建与使用注解处理器。Java SE5 扩展了反射机制的API,以帮助程序员构造这类工具。同时,它还提供了一个外部工具 apt帮助程序员解析带有注解的 java 源代码。
根据上面描述我们可以知道,注解处理器并不是一个特定格式,并不是只有继承了 AbstractProcessor 这个抽象类才叫注解处理器,凡是根据相关API 来读取注解的类或者方法都可以称为注解处理器。
1、最简单的注解处理器莫过于,直接使用反射机制的 getDeclaredMethods 方法获取类上所有方法(字段原理是一样的),再通过调用 getAnnotation 获取每个方法上的特定注解,有了注解便可以获取注解之上的元素值,方法如下:
public void getAnnoUtil(Class<?> cl) { for(Method m : cl.getDeclaredMethods()) { LogClassMessage logClassMessage = m.getAnnotation(LogClassMessage .class); if(logClassMessage != null) { int id = logClassMessage.id(); String method = logClassMessage.message(); } } }
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
2、由于反射对性能会有一定的损耗,所以上述类型的注解处理器并不占主流,现在使用最多的还是 AbstractProcessor 自定义注解处理器,因为后者并不需要通过反射实现,效率和直接调用普通方法没有区别,这也是为什么编译期注解比运行时注解更受欢迎,但是并不是说为了性能运行期注解就不能用了,只能说不能滥用,要在性能方面给予考虑。目前主要的用到运行期注解的框架差不多都有缓存机制,只有在第一次使用时通过反射机制,当再次使用时直接从缓存中取出。好了,说着说着就跑题,我们还是来聊一下这个 AbstractProcessor 类吧,到底有何魅力让这么多人为她沉迷,方法如下(注意得在 java Moudle 下哦,android project 没有提供相关的包):
/** * Created by zmj on 2017/6/19. */ public class MyFirstProcessor extends AbstractProcessor { /** * 做一些初始化工作,注释处理工具框架调用了这个方法, * 当我们使用这个方法的时候会给我们传递一个 ProcessingEnvironment * 类型的实参。 * 如果在同一个对象多次调用此方法,则抛出IllegalStateException异常 * @param processingEnvironment 这个参数里面包含了很多工具方法 */ @Override public synchronized void init(ProcessingEnvironment processingEnvironment) { /** * 返回用来在元素上进行操作的某些工具方法的实现 */ Elements es = processingEnvironment.getElementUtils(); /** * 返回用来创建新源、类或辅助文件的Filer */ Filer filer = processingEnvironment.getFiler(); /** * 返回用来在类型上进行操作的某些实用工具方法的实现 */ Types types = processingEnvironment.getTypeUtils(); /** * 这是提供给开发者日志工具,我们可以用来报告错误和警告以及提示信息 * 注意 message 使用后并不会结束过程 * Kind 参数表示日志级别 */ Messager messager = processingEnvironment.getMessager(); messager.printMessage(Diagnostic.Kind.ERROR,"例如当默认值为空则提示一个错误"); /** * 返回任何生成的源和类文件应该符合的源版本 */ SourceVersion version = processingEnvironment.getSourceVersion(); super.init(processingEnvironment); } /** * 注解的处理逻辑 * @param set * @param roundEnvironment * @return 如果返回true 不要求后续Processor处理它们,反之,则继续执行处理 */ @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { /** * TypeElement 这表示一个类或者接口元素集合 * 常用方法不多,TypeMirror getSuperclass()返回直接超类 * * 详细介绍下 RoundEnvironment 这个类 * 常用方法: * boolean errorRaised() 如果在以前的处理round中发生错误,则返回true * Set<? extends Element> getElementsAnnotatedWith(Class<? extends Annotation> a) * 这里的 a 即你自定义的注解class类,返回使用给定注解类型注解的元素的集合 * Set<? extends Element> getElementsAnnotatedWith(TypeElement a) * * Element 的用法: * TypeMirror asType() 返回此元素定义的类型 如int * ElementKind getKind() 返回元素的类型 如 e.getkind() = ElementKind.FIELD 字段 * boolean equals(Object obj) 如果参数表示与此元素相同的元素,则返回true * Name getSimpleName() 返回此元素的简单名称 * List<? extends Elements> getEncloseElements 返回元素直接封装的元素 * Element getEnclosingElements 返回此元素的最里层元素,如果这个元素是个字段等,则返回为类 */ return false; } /** * 指出注解处理器 处理哪种注解 * 在 jdk1.7 中,我们可以使用注解 @SupportedAnnotationTypes()代替 */ @Override public Set<String> getSupportedAnnotationTypes() { return super.getSupportedAnnotationTypes(); } /** * 指定当前注解器使用的Jdk版本 * 在 jdk1.7 中,我们可以使用注解 @SupportedSourceVersion()代替 */ @Override public SourceVersion getSupportedSourceVersion() { return super.getSupportedSourceVersion(); } }
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
-
21
-
22
-
23
-
24
-
25
-
26
-
27
-
28
-
29
-
30
-
31
-
32
-
33
-
34
-
35
-
36
-
37
-
38
-
39
-
40
-
41
-
42
-
43
-
44
-
45
-
46
-
47
-
48
-
49
-
50
-
51
-
52
-
53
-
54
-
55
-
56
-
57
-
58
-
59
-
60
-
61
-
62
-
63
-
64
-
65
-
66
-
67
-
68
-
69
-
70
-
71
-
72
-
73
-
74
-
75
-
76
-
77
-
78
-
79
-
80
-
81
-
82
-
83
-
84
-
85
-
86
-
87
-
88
-
89
-
90
-
91
-
92
-
93
-
94
5. 自定义运行期注解(RUNTIME)
我们在开发中经常会需要计算一个方法所要执行的时间,以此来直观的比较哪个实现方式最优,常用方法是开始结束时间相减
System.currentTimeMillis()
但是当方法多的时候,是不是减来减去都要减的怀疑人生啦,哈哈,那么下面我就来写一个运行时注解来打印方法执行的时间。
1.首先我们先定义一个注解,并给注解添加我们需要的元注解:
/** * Created by zmj on 2017/6/12. * * 这是一个计算方法执行时间的注解 * 只能作用于方法之上 * 属于运行时注解,能被VM处理,可以通过反射得到注解信息 * */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface CalculateMethodRunningTime { //要计算时间的方法的名字 String methodName() default "no method to set"; }
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
2.利用反射方法在程序运行时,获取被添加注解的类的信息:
/** * Created by zmj on 2017/6/12. * * 注解实现过程中所需要的工具方法 * */ public class AnnotationUtils { //使用反射通过类名获取类的相关信息 public static void getClassInfo(String className) { try { Class c = Class.forName(className); //获取所有公共的方法 Method[] methods = c.getMethods(); for(Method m : methods) { Class<CalculateMethodRunningTime> ctClass = CalculateMethodRunningTime.class; if(m.isAnnotationPresent(ctClass)){ CalculateMethodRunningTime anno = m.getAnnotation(ctClass); //当前方法包含查询时间的注解时 if(anno != null){ long beginTime = System.currentTimeMillis(); m.invoke(c.newInstance(),null); long endTime = System.currentTimeMillis(); long time = endTime - beginTime; Log.i("Tag",anno.methodName()+"方法执行所需要时间:" + time + "ms"); } } } } catch (Exception e) { e.printStackTrace(); } } }
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
-
21
-
22
-
23
-
24
-
25
-
26
-
27
-
28
-
29
-
30
-
31
-
32
-
33
-
34
-
35
-
36
-
37
3.在 activity 中使用注解,注意咱们的注解是作用于方法之上的:
public class ActivityAnnotattion extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_anno); AnnotationUtils.getClassInfo("com.annotation.zmj.annotationtest.ActivityAnnotattion"); } @CalculateMethodRunningTime(methodName = "method1") public void method1() { long i = 100000000L; while(i > 0) { i--; } } }
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
4.运行结果:
6. 自定义编译期注解(CLASS)
为什么我要最后说编译期注解呢,因为相对前面的自定义注解来说,编译期注解有些难度,涉及到的东西比较多,但其却是平时用到的最多的注解,因为编译期注解不存在反射,所以对性能没有影响。
本来也想用绑定 view 的例子讲解,但是现在这样的 demo 网上真是各种泛滥啊,而且还有各路大牛写的,所以我就没必要班门弄斧了。在我们的实际开发中,肯定都被跳转界面烦过,几乎每个界面都会来个:
Intent intent = new Intent (this,NextActivity.class);
startActivity (intent);
好烦人,所以本着方便就是改进的原则,让我们定义一个编译期注解,来自动生成上述的代码,想想每次需要的时候只需要一个注解就能跳转到想要跳转的界面是不是很刺激。
1.首先新建一个 android 项目,在创建两个 java module(File -> New -> new Module ->java Module),因为有的类在android项目中不支持,建完后项目结构如下:
其中 annotation 中盛放自定义的注解,annotationprocessor 中创建注解处理器并做相关处理,最后的 app 则为我们的项目。
注意:MyFirstProcessor类为上文讲解 AbstractProcessor 所建的类,可以删去,跟本项目没有关系。
2.新建后的三个工程进行依赖,注解处理器必须依赖注解 module ,而app 需要同时依赖注解 module 和 注解处理器 module,这个很好理解,这三个都是独立的,你如果不进行依赖怎么去调用。
3.编写自定义注解,这是一个应用到字段之上的注解,被注解的字段为传递的参数。
/** * 这是一个自定义的跳转传值所用到的注解 * value 表示要跳转到哪个界面activity的元素,传入那个界面的名字 */ @Retention(RetentionPolicy.CLASS) @Target(ElementType.FIELD) public @interface IntentField { String value () default " "; }
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
3.自定义注解处理器,获取被注解元素的类型,进行相应的操作,方法不懂的上文应该都有解释。
/** * 这是一个自定义注解处理器 */ @AutoService(javax.annotation.processing.Processor.class) public class MyProcessot extends AbstractProcessor{ private Map<Element, List<VariableElement>> items = new HashMap<>(); private List<Generator> generators = new LinkedList<>(); /** * 做一些初始化工作 */ @Override public synchronized void init(ProcessingEnvironment processingEnvironment) { super.init(processingEnvironment); Utils.init(); generators.add(new ActivityEnterGenerator()); generators.add(new ActivityInitFieldGenerator()); } @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { //获取所有注册IntentField注解的元素 for (Element elem : roundEnvironment.getElementsAnnotatedWith(IntentField.class)) { //主要获取ElementType 是不是null,即class,interface,enum或者注解类型 if (elem.getEnclosingElement() == null) { //直接结束处理器 return true; } //如果items的key不存在,则添加一个key if (items.get(elem.getEnclosingElement()) == null) { items.put(elem.getEnclosingElement(), new LinkedList<VariableElement>()); } //我们这里的IntentField是应用在一般成员变量上的注解 if (elem.getKind() == ElementKind.FIELD) { items.get(elem.getEnclosingElement()).add((VariableElement)elem); } } List<VariableElement> variableElements; for (Map.Entry<Element, List<VariableElement>> entry : items.entrySet()) { variableElements = entry.getValue(); if (variableElements == null || variableElements.isEmpty()) { return true; } //去通过自动javapoet生成代码 for (Generator generator : generators) { generator.genetate(entry.getKey(), variableElements, processingEnv); generator.genetate(entry.getKey(), variableElements, processingEnv); } } return false; } /** * 指定当前注解器使用的Java版本 */ @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latestSupported(); } /** * 指出注解处理器 处理哪种注解 */ @Override public Set<String> getSupportedAnnotationTypes() { Set<String> annotations = new LinkedHashSet<>(2); annotations.add(IntentField.class.getCanonicalName()); return annotations; } }
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
-
21
-
22
-
23
-
24
-
25
-
26
-
27
-
28
-
29
-
30
-
31
-
32
-
33
-
34
-
35
-
36
-
37
-
38
-
39
-
40
-
41
-
42
-
43
-
44
-
45
-
46
-
47
-
48
-
49
-
50
-
51
-
52
-
53
-
54
-
55
-
56
-
57
-
58
-
59
-
60
-
61
-
62
-
63
-
64
-
65
-
66
-
67
-
68
-
69
-
70
-
71
-
72
-
73
-
74
-
75
4.这是一个工具类方法,提供了本 demo 中所用到的一些方法,其实实际里面的方法都很常见,只不过做了一个封装而已。
public class Utils { private static Set<String> supportTypes = new HashSet<>(); /** * 当getIntent的时候,每种类型写的方式都不一样,所以把每种方式都添加到了Set容器中 */ static void init() { supportTypes.add(int.class.getSimpleName()); supportTypes.add(int[].class.getSimpleName()); supportTypes.add(short.class.getSimpleName()); supportTypes.add(short[].class.getSimpleName()); supportTypes.add(String.class.getSimpleName()); supportTypes.add(String[].class.getSimpleName()); supportTypes.add(boolean.class.getSimpleName()); supportTypes.add(boolean[].class.getSimpleName()); supportTypes.add(long.class.getSimpleName()); supportTypes.add(long[].class.getSimpleName()); supportTypes.add(char.class.getSimpleName()); supportTypes.add(char[].class.getSimpleName()); supportTypes.add(byte.class.getSimpleName()); supportTypes.add(byte[].class.getSimpleName()); supportTypes.add("Bundle"); } /** * 获取元素所在的包名 * @param element * @return */ public static String getPackageName(Element element) { String clazzSimpleName = element.getSimpleName().toString(); String clazzName = element.toString(); return clazzName.substring(0, clazzName.length() - clazzSimpleName.length() - 1); } /** * 判断是否是String类型或者数组或者bundle,因为这三种类型getIntent()不需要默认值 * @param typeName * @return */ public static boolean isElementNoDefaultValue(String typeName) { return (String.class.getName().equals(typeName) || typeName.contains("[]") || typeName.contains("Bundle")); } /** * 获得注解要传递参数的类型 * @param typeName 注解获取到的参数类型 * @return */ public static String getIntentTypeName(String typeName) { for (String name : supportTypes) { if (name.equals(getSimpleName(typeName))) { return name.replaceFirst(String.valueOf(name.charAt(0)), String.valueOf(name.charAt(0)).toUpperCase()) .replace("[]", "Array"); } } return ""; } /** * 获取类的的名字的字符串 * @param typeName 可以是包名字符串,也可以是类名字符串 * @return */ static String getSimpleName(String typeName) { if (typeName.contains(".")) { return typeName.substring(typeName.lastIndexOf(".") + 1, typeName.length()); }else { return typeName; } } /** * 自动生成代码 */ public static void writeToFile(String className, String packageName, MethodSpec methodSpec, ProcessingEnvironment processingEnv, ArrayList<FieldSpec> listField) { TypeSpec genedClass; if(listField == null) { genedClass = TypeSpec.classBuilder(className) .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addMethod(methodSpec).build(); }else{ genedClass = TypeSpec.classBuilder(className) .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addMethod(methodSpec) .addFields(listField).build(); } JavaFile javaFile = JavaFile.builder(packageName, genedClass) .build(); try { javaFile.writeTo(processingEnv.getFiler()); } catch (IOException e) { e.printStackTrace(); } } }
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
-
21
-
22
-
23
-
24
-
25
-
26
-
27
-
28
-
29
-
30
-
31
-
32
-
33
-
34
-
35
-
36
-
37
-
38
-
39
-
40
-
41
-
42
-
43
-
44
-
45
-
46
-
47
-
48
-
49
-
50
-
51
-
52
-
53
-
54
-
55
-
56
-
57
-
58
-
59
-
60
-
61
【附】相关架构及资料
源码、笔记、视频。高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter全方面的Android进阶实践技术,和技术大牛一起讨论交流解决问题。
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
teTo(processingEnv.getFiler()); } catch (IOException e) { e.printStackTrace(); } } }`
-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
-
21
-
22
-
23
-
24
-
25
-
26
-
27
-
28
-
29
-
30
-
31
-
32
-
33
-
34
-
35
-
36
-
37
-
38
-
39
-
40
-
41
-
42
-
43
-
44
-
45
-
46
-
47
-
48
-
49
-
50
-
51
-
52
-
53
-
54
-
55
-
56
-
57
-
58
-
59
-
60
-
61
【附】相关架构及资料
源码、笔记、视频。高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter全方面的Android进阶实践技术,和技术大牛一起讨论交流解决问题。
[外链图片转存中…(img-1RmM649n-1714330589490)]
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!