首页 > 其他分享 >探究Android中的注解

探究Android中的注解

时间:2022-11-10 21:32:14浏览次数:71  
标签:return Object id MainActivity 探究 注解 Android public


本文系GDG Android Meetup分享内容总结文章

注解是我们经常接触的技术,Java有注解,Android也有注解,本文将试图介绍Android中的注解,以及ButterKnife和Otto这些基于注解的库的一些工作原理.

归纳而言,Android中的注解大概有以下好处

  • 提高我们的开发效率
  • 更早的发现程序的问题或者错误
  • 更好的增加代码的描述能力
  • 更加利于我们的一些规范约束
  • 提供解决问题的更优解

准备工作

默认情况下,Android中的注解包并没有包括在framework中,它独立成一个单独的包,通常我们需要引入这个包.



dependencies {
compile 'com.android.support:support-annotations:22.2.0'
}

但是如果我们已经引入了​​appcompat​​​则没有必要再次引用​​support-annotations​​​,因为​​appcompat​​默认包含了对其引用.

替代枚举

在最早的时候,当我们想要做一些值得限定实现枚举的效果,通常是

  • 定义几个常量用于限定
  • 从上面的常量选取值进行使用

一个比较描述上面问题的示例代码如下



public static final int COLOR_RED = 0;
public static final int COLOR_GREEN = 1;
public static final int COLOR_YELLOW = 2;

public void setColor(int color) {
//some code here
}
//调用
setColor(COLOR_RED)

然而上面的还是有不尽完美的地方

  • ​setColor(COLOR_RED)​​​与​​setColor(0)​​效果一样,而后者可读性很差,但却可以正常运行
  • setColor方法可以接受枚举之外的值,比如​​setColor(3)​​,这种情况下程序可能出问题

一个相对较优的解决方法就是使用Java中的Enum.使用枚举实现的效果如下


// ColorEnum.java
public enum ColorEmun {
RED,
GREEN,
YELLOW
}

public void setColorEnum(ColorEmun colorEnum) {
//some code here
}

setColorEnum(ColorEmun.GREEN);

然而Enum也并非最佳,Enum因为其相比方案一的常量来说,占用内存相对大很多而受到曾经被Google列为不建议使用,为此Google特意引入了一些相关的注解来替代枚举.

Android中新引入的替代枚举的注解有​​IntDef​​​和​​StringDef​​​,这里以​​IntDef​​做例子说明一下.


public class Colors {
@IntDef({RED, GREEN, YELLOW})
@Retention(RetentionPolicy.SOURCE)
public @interface LightColors{}

public static final int RED = 0;
public static final int GREEN = 1;
public static final int YELLOW = 2;
}
  • 声明必要的int常量
  • 声明一个注解为LightColors
  • 使用@IntDef修饰LightColors,参数设置为待枚举的集合
  • 使用@Retention(RetentionPolicy.SOURCE)指定注解仅存在与源码中,不加入到class文件中

Null相关的注解

和Null相关的注解有两个

@Nullable 注解的元素可以是Null
@NonNull 注解的元素不能是Null

上面的两个可以修饰如下的元素

  • 成员属性
  • 方法参数
  • 方法的返回值
@Nullable
private String obtainReferrerFromIntent(@NonNull Intent intent) {
return intent.getStringExtra("apps_referrer");
}

NonNull检测生效的条件

  • 显式传入null
  • 在调用方法之前已经判断了参数为null时
setReferrer(null);//提示警告

//不提示警告
String referrer = getIntent().getStringExtra("apps_referrer");
setReferrer(referrer);

//提示警告
String referrer = getIntent().getStringExtra("apps_referrer");
if (referrer == null) {
setReferrer(referrer);
}

private void setReferrer(@NonNull String referrer) {
//some code here
}

区间范围注解

Android中的IntRange和FloatRange是两个用来限定区间范围的注解,



float currentProgress;

public void setCurrentProgress(@FloatRange(from=0.0f, to=1.0f) float progress) {
currentProgress = progress;
}

如果我们传入非法的值,如下所示



setCurrentProgress(11);

就会得到这样的错误



Value must be >=0.0 and <= 1.0(was 11)

长度以及数组大小限制

限制字符串的长度



private void setKey(@Size(6) String key) {
}

限定数组集合的大小



private void setData(@Size(max = 1) String[] data) {
}
setData(new String[]{"b", "a"});//error occurs

限定特殊的数组长度,比如3的倍数



private void setItemData(@Size(multiple = 3) String[] data) {
}

权限相关

在Android中,有很多场景都需要使用权限,无论是Marshmallow之前还是之后的动态权限管理.都需要在manifest中进行声明,如果忘记了,则会导致程序崩溃. 好在有一个注解能辅助我们避免这个问题.使用RequiresPermission注解即可.


@RequiresPermission(Manifest.permission.SET_WALLPAPER)
public void changeWallpaper(Bitmap bitmap) throws IOException {
}

资源注解

在Android中几乎所有的资源都可以有对应的资源id.比如获取定义的字符串,我们可以通过下面的方法


public String getStringById(int stringResId) {
return getResources().getString(stringResId);
}

使用这个方法,我们可以很容易的获取到定义的字符串,但是这样的写法也存在着风险.



getStringById(R.mipmap.ic_launcher)

如果我们在不知情或者疏忽情况下,传入这样的值,就会出现问题. 但是如果我们使用资源相关的注解修饰了参数,就能很大程度上避免错误的情况.



public String getStringById(@StringRes  int stringResId) {
return getResources().getString(stringResId);
}

在Android中资源注解如下所示

  • AnimRes
  • AnimatorRes
  • AnyRes
  • ArrayRes
  • AttrRes
  • BoolRes
  • ColorRes
  • DimenRes
  • DrawableRes
  • FractionRes
  • IdRes
  • IntegerRes
  • InterpolatorRes
  • LayoutRes
  • MenuRes
  • PluralsRes
  • RawRes
  • StringRes
  • StyleRes
  • StyleableRes
  • TransitionRes
  • XmlRes

Color值限定

上面部分提到了​​ColorRes​​​,用来限定颜色资源id,这里我们将使用​​ColorInt​​,一个用来限定Color值的注解. 在较早的TextView的setTextColor是这样实现的.



public void setTextColor(int color) {
mTextColor = ColorStateList.valueOf(color);
updateTextColors();
}

然而上面的方法在调用时常常会出现这种情况



myTextView.setTextColor(R.color.colorAccent);

如上,如果传递过去的参数为color的资源id就会出现颜色取错误的问题,这个问题在过去还是比较严重的.好在​​ColorInt​​出现了,改变了这一问题.



public void setTextColor(@ColorInt int color) {
mTextColor = ColorStateList.valueOf(color);
updateTextColors();
}

当我们再次传入Color资源值时,就会得到错误的提示.

CheckResult

这是一个关于返回结果的注解,用来注解方法,如果一个方法得到了结果,却没有使用这个结果,就会有错误出现,一旦出现这种错误,就说明你没有正确使用该方法。



@CheckResult
public String trim(String s) {
return s.trim();
}

线程相关

Android中提供了四个与线程相关的注解

  • @UiThread,通常可以等同于主线程,标注方法需要在UIThread执行,比如View类就使用这个注解
  • @MainThread 主线程,经常启动后创建的第一个线程
  • @WorkerThread 工作者线程,一般为一些后台的线程,比如AsyncTask里面的doInBackground就是这样的.
  • @BinderThread 注解方法必须要在BinderThread线程中执行,一般使用较少.

一些示例



new AsyncTask<Void, Void, Void>() {
//doInBackground is already annotated with @WorkerThread
@Override
protected Void doInBackground(Void... params) {
return null;
updateViews();//error
}
};

@UiThread
public void updateViews() {
Log.i(LOGTAG, "updateViews ThreadInfo=" + Thread.currentThread());
}

注意,这种情况下不会出现错误提示



new Thread(){
@Override
public void run() {
super.run();
updateViews();
}
}.start();

虽然updateViews会在一个新的工作者线程中执行,但是在compile时没有错误提示.

因为它的判断依据是,如果updateView的线程注解(这里为@UiThread)和run(没有线程注解)不一致才会错误提示.如果run方法没有线程注解,则不提示.

CallSuper

重写的方法必须要调用super方法

使用这个注解,我们可以强制方法在重写时必须调用父类的方法 比如Application的​​onCreate​​​,​​onConfigurationChanged​​等.

Keep

在Android编译生成APK的环节,我们通常需要设置minifyEnabled为true实现下面的两个效果

  • 混淆代码
  • 删除没有用的代码

但是出于某一些目的,我们需要不混淆某部分代码或者不删除某处代码,除了配置复杂的Proguard文件之外,我们还可以使用@Keep注解 .



@Keep
public static int getBitmapWidth(Bitmap bitmap) {
return bitmap.getWidth();
}

ButterKnife

ButterKnife是一个用来绑定View,资源和回调的提高效率的工具.作者为Jake Wharton. ButterKnife的好处

  • 使用BindView替代繁琐的findViewById和类型转换
  • 使用OnClick注解方法来替换显式声明的匿名内部类
  • 使用BindString,BindBool,BindDrawable等注解实现资源获取

一个摘自Github的示例



class ExampleActivity extends Activity {
@BindView(R.id.user) EditText username;
@BindView(R.id.pass) EditText password;

@BindString(R.string.login_error) String loginErrorMessage;

@OnClick(R.id.submit) void submit() {
// TODO call server...
}

@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.simple_activity);
ButterKnife.bind(this);
// TODO Use fields...
}
}

ButterKnife工作原理

以BindView注解使用为例,示例代码为



public class MainActivity extends AppCompatActivity {
@BindView(R.id.myTextView)
TextView myTextView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
}
}

1.程序在compile时,会根据注解自动生成两个类,这里为MainActivity_ViewBinder.classMainActivity_ViewBinding.class
2.当我们调用​​​ButterKnife.bind(this);​​​时,会查找当前类对应的ViewBinder类,并调用bind方法,这里会调用到​​MainActiivty_ViewBinder.bind​​​方法.
3.MainActiivty_ViewBinder.bind方法实际上是调用了findViewById然后在进行类型转换,赋值给MainActivity的myTextView属性

ButterKnife的bind方法



public static Unbinder bind(@NonNull Activity target) {
return getViewBinder(target).bind(Finder.ACTIVITY, target, target);
}

ButterKnife的​​getViewBinder​​​和​​findViewBinderForClass​



@NonNull @CheckResult @UiThread
static ViewBinder<Object> getViewBinder(@NonNull Object target) {
Class<?> targetClass = target.getClass();
if (debug) Log.d(TAG, "Looking up view binder for " + targetClass.getName());
return findViewBinderForClass(targetClass);
}

@NonNull @CheckResult @UiThread
private static ViewBinder<Object> findViewBinderForClass(Class<?> cls) {
//如果内存集合BINDERS中包含,则不再查找
ViewBinder<Object> viewBinder = BINDERS.get(cls);
if (viewBinder != null) {
if (debug) Log.d(TAG, "HIT: Cached in view binder map.");
return viewBinder;
}
String clsName = cls.getName();
if (clsName.startsWith("android.") || clsName.startsWith("java.")) {
if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
return NOP_VIEW_BINDER;
}
//noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
try {
//使用反射创建实例
Class<?> viewBindingClass = Class.forName(clsName + "_ViewBinder");
//noinspection unchecked
viewBinder = (ViewBinder<Object>) viewBindingClass.newInstance();
if (debug) Log.d(TAG, "HIT: Loaded view binder class.");
} catch (ClassNotFoundException e) {
//如果没有找到,对父类进行查找
if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());
viewBinder = findViewBinderForClass(cls.getSuperclass());
} catch (InstantiationException e) {
throw new RuntimeException("Unable to create view binder for " + clsName, e);
} catch (IllegalAccessException e) {
throw new RuntimeException("Unable to create view binder for " + clsName, e);
}
//加入内存集合,便于后续的查找
BINDERS.put(cls, viewBinder);
return viewBinder;
}

MainActivity_ViewBinder的反编译源码



➜  androidannotationsample javap -c MainActivity_ViewBinder
Warning: Binary file MainActivity_ViewBinder contains com.example.admin.androidannotationsample.MainActivity_ViewBinder
Compiled from "MainActivity_ViewBinder.java"
public final class com.example.admin.androidannotationsample.MainActivity_ViewBinder implements butterknife.internal.ViewBinder<com.example.admin.androidannotationsample.MainActivity> {
public com.example.admin.androidannotationsample.MainActivity_ViewBinder();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return

public butterknife.Unbinder bind(butterknife.internal.Finder, com.example.admin.androidannotationsample.MainActivity, java.lang.Object);
Code:
0: new #2 // class com/example/admin/androidannotationsample/MainActivity_ViewBinding
3: dup
4: aload_2
5: aload_1
6: aload_3 // 创建ViewBinding实例
7: invokespecial #3 // Method com/example/admin/androidannotationsample/MainActivity_ViewBinding."<init>":(Lcom/example/admin/androidannotationsample/MainActivity;Lbutterknife/internal/Finder;Ljava/lang/Object;)V
10: areturn

public butterknife.Unbinder bind(butterknife.internal.Finder, java.lang.Object, java.lang.Object);
Code:
0: aload_0
1: aload_1
2: aload_2
3: checkcast #4 // class com/example/admin/androidannotationsample/MainActivity
6: aload_3 //调用上面的重载方法
7: invokevirtual #5 // Method bind:(Lbutterknife/internal/Finder;Lcom/example/admin/androidannotationsample/MainActivity;Ljava/lang/Object;)Lbutterknife/Unbinder;
10: areturn
}

MainActivity_ViewBinding的反编译源码



➜  androidannotationsample javap -c MainActivity_ViewBinding
Warning: Binary file MainActivity_ViewBinding contains com.example.admin.androidannotationsample.MainActivity_ViewBinding
Compiled from "MainActivity_ViewBinding.java"
public class com.example.admin.androidannotationsample.MainActivity_ViewBinding<T extends com.example.admin.androidannotationsample.MainActivity> implements butterknife.Unbinder {
protected T target;

public com.example.admin.androidannotationsample.MainActivity_ViewBinding(T, butterknife.internal.Finder, java.lang.Object);
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: aload_1
6: putfield #2 // Field target:Lcom/example/admin/androidannotationsample/MainActivity;
9: aload_1
10: aload_2
11: aload_3 //调用Finder.findRequireViewAsType找到View,并进行类型转换,并复制给MainActivity中对一个的变量
12: ldc #4 // int 2131427412
14: ldc #5 // String field 'myTextView'
16: ldc #6 // class android/widget/TextView
// 内部实际调用了findViewById
18: invokevirtual #7 // Method butterknife/internal/Finder.findRequiredViewAsType:(Ljava/lang/Object;ILjava/lang/String;Ljava/lang/Class;)Ljava/lang/Object;
21: checkcast #6 // class android/widget/TextView
24: putfield #8 // Field com/example/admin/androidannotationsample/MainActivity.myTextView:Landroid/widget/TextView;
27: return

public void unbind();
Code:
0: aload_0
1: getfield #2 // Field target:Lcom/example/admin/androidannotationsample/MainActivity;
4: astore_1
5: aload_1
6: ifnonnull 19
9: new #9 // class java/lang/IllegalStateException
12: dup
13: ldc #10 // String Bindings already cleared.
15: invokespecial #11 // Method java/lang/IllegalStateException."<init>":(Ljava/lang/String;)V
18: athrow
19: aload_1
20: aconst_null // 解除绑定,设置对应的变量为null
21: putfield #8 // Field com/example/admin/androidannotationsample/MainActivity.myTextView:Landroid/widget/TextView;
24: aload_0
25: aconst_null
26: putfield #2 // Field target:Lcom/example/admin/androidannotationsample/MainActivity;
29: return
}

Finder的源码



package butterknife.internal;

import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.support.annotation.IdRes;
import android.view.View;

@SuppressWarnings("UnusedDeclaration") // Used by generated code.
public enum Finder {
VIEW {
@Override public View findOptionalView(Object source, @IdRes int id) {
return ((View) source).findViewById(id);
}

@Override public Context getContext(Object source) {
return ((View) source).getContext();
}

@Override protected String getResourceEntryName(Object source, @IdRes int id) {
final View view = (View) source;
// In edit mode, getResourceEntryName() is unsupported due to use of BridgeResources
if (view.isInEditMode()) {
return "<unavailable while editing>";
}
return super.getResourceEntryName(source, id);
}
},
ACTIVITY {
@Override public View findOptionalView(Object source, @IdRes int id) {
return ((Activity) source).findViewById(id);
}

@Override public Context getContext(Object source) {
return (Activity) source;
}
},
DIALOG {
@Override public View findOptionalView(Object source, @IdRes int id) {
return ((Dialog) source).findViewById(id);
}

@Override public Context getContext(Object source) {
return ((Dialog) source).getContext();
}
};

//查找对应的Finder,如上面的ACTIVITY, DIALOG, VIEW
public abstract View findOptionalView(Object source, @IdRes int id);


public final <T> T findOptionalViewAsType(Object source, @IdRes int id, String who,
Class<T> cls) {
View view = findOptionalView(source, id);
return castView(view, id, who, cls);
}

public final View findRequiredView(Object source, @IdRes int id, String who) {
View view = findOptionalView(source, id);
if (view != null) {
return view;
}
String name = getResourceEntryName(source, id);
throw new IllegalStateException("Required view '"
+ name
+ "' with ID "
+ id
+ " for "
+ who
+ " was not found. If this view is optional add '@Nullable' (fields) or '@Optional'"
+ " (methods) annotation.");
}

//来自ViewBinding的调用
public final <T> T findRequiredViewAsType(Object source, @IdRes int id, String who,
Class<T> cls) {
View view = findRequiredView(source, id, who);
return castView(view, id, who, cls);
}

public final <T> T castView(View view, @IdRes int id, String who, Class<T> cls) {
try {
return cls.cast(view);
} catch (ClassCastException e) {
String name = getResourceEntryName(view, id);
throw new IllegalStateException("View '"
+ name
+ "' with ID "
+ id
+ " for "
+ who
+ " was of the wrong type. See cause for more info.", e);
}
}

@SuppressWarnings("unchecked") // That's the point.
public final <T> T castParam(Object value, String from, int fromPos, String to, int toPos) {
try {
return (T) value;
} catch (ClassCastException e) {
throw new IllegalStateException("Parameter #"
+ (fromPos + 1)
+ " of method '"
+ from
+ "' was of the wrong type for parameter #"
+ (toPos + 1)
+ " of method '"
+ to
+ "'. See cause for more info.", e);
}
}

protected String getResourceEntryName(Object source, @IdRes int id) {
return getContext(source).getResources().getResourceEntryName(id);
}

public abstract Context getContext(Object source);
}

Otto

Otto Bus 是一个专为Android改装的Event Bus,在很多项目中都有应用.由Square开源共享.



public class EventBusTest {
private static final String LOGTAG = "EventBusTest";
Bus mBus = new Bus();

public void test() {
mBus.register(this);
}

class NetworkChangedEvent {

}

@Produce
public NetworkChangedEvent sendNetworkChangedEvent() {
return new NetworkChangedEvent();
}


@Subscribe
public void onNetworkChanged(NetworkChangedEvent event) {
Log.i(LOGTAG, "onNetworkChanged event=" + event);
}
}

Otto 的工作原理

  • 使用@Produce和@Subscribe标记方法
  • 当调用bus.register方法,去检索注册对象的标记方法,并cache映射关系
  • 当post事件时,将事件与handler方法对应加入事件队列
  • 抽取事件队列,然后调用handler处理

如下为对Otto如何利用注解的分析

register的源码



public void register(Object object) {
if (object == null) {
throw new NullPointerException("Object to register must not be null.");
}
enforcer.enforce(this);
//查找object中的Subscriber
Map<Class<?>, Set<EventHandler>> foundHandlersMap = handlerFinder.findAllSubscribers(object);
for (Class<?> type : foundHandlersMap.keySet()) {
Set<EventHandler> handlers = handlersByType.get(type);
if (handlers == null) {
//concurrent put if absent
Set<EventHandler> handlersCreation = new CopyOnWriteArraySet<EventHandler>();
handlers = handlersByType.putIfAbsent(type, handlersCreation);
if (handlers == null) {
handlers = handlersCreation;
}
}
final Set<EventHandler> foundHandlers = foundHandlersMap.get(type);
if (!handlers.addAll(foundHandlers)) {
throw new IllegalArgumentException("Object already registered.");
}
}

for (Map.Entry<Class<?>, Set<EventHandler>> entry : foundHandlersMap.entrySet()) {
Class<?> type = entry.getKey();
EventProducer producer = producersByType.get(type);
if (producer != null && producer.isValid()) {
Set<EventHandler> foundHandlers = entry.getValue();
for (EventHandler foundHandler : foundHandlers) {
if (!producer.isValid()) {
break;
}
if (foundHandler.isValid()) {
dispatchProducerResultToHandler(foundHandler, producer);
}
}
}
}
}

HandlerFinder源码



interface HandlerFinder {

Map<Class<?>, EventProducer> findAllProducers(Object listener);

Map<Class<?>, Set<EventHandler>> findAllSubscribers(Object listener);

//Otto注解查找器
HandlerFinder ANNOTATED = new HandlerFinder() {
@Override
public Map<Class<?>, EventProducer> findAllProducers(Object listener) {
return AnnotatedHandlerFinder.findAllProducers(listener);
}

@Override
public Map<Class<?>, Set<EventHandler>> findAllSubscribers(Object listener) {
return AnnotatedHandlerFinder.findAllSubscribers(listener);
}
};

具体查找实现



/** This implementation finds all methods marked with a {@link Subscribe} annotation. */
static Map<Class<?>, Set<EventHandler>> findAllSubscribers(Object listener) {
Class<?> listenerClass = listener.getClass();
Map<Class<?>, Set<EventHandler>> handlersInMethod = new HashMap<Class<?>, Set<EventHandler>>();

Map<Class<?>, Set<Method>> methods = SUBSCRIBERS_CACHE.get(listenerClass);
if (null == methods) {
methods = new HashMap<Class<?>, Set<Method>>();
loadAnnotatedSubscriberMethods(listenerClass, methods);
}
if (!methods.isEmpty()) {
for (Map.Entry<Class<?>, Set<Method>> e : methods.entrySet()) {
Set<EventHandler> handlers = new HashSet<EventHandler>();
for (Method m : e.getValue()) {
handlers.add(new EventHandler(listener, m));
}
handlersInMethod.put(e.getKey(), handlers);
}
}

return handlersInMethod;
}

以上就是关于Android中注解的一些总结,文章部分内容参考自 ​​Support Annotations​​ ,希望能帮助大家对注解有基础的认识,并运用到实际的日常开发之中.

参考文章:

标签:return,Object,id,MainActivity,探究,注解,Android,public
From: https://blog.51cto.com/u_3987305/5842148

相关文章

  • 一些快速提高Android开发的脚本与技巧(终端篇)
    正所谓“工欲善其事必先利其器”,一个好的工具或者技巧能让提升工作效率,起到事半功倍的效果。在这里斗胆列出一些窃以为一些可能快速提高Android日常开发的脚本,希望可以为大......
  • Android基于坐标对View进行模拟点击事件
    在Android中,我们对于View进行模拟点击事件,很容易,比如调用​​View.performClick​​即可。但是有些时候,我们想要更加精细的点击,比如View的某一区域或者某一点进行点击。比如......
  • java 自定义注解
    packagecom.tedu.in;importjava.lang.reflect.Field;publicclassTest{publicstaticvoidmain(String[]args){Class<User>user=User.class;......
  • 一个简单实用的Android调试应用技巧
    在应用开发中,我们常常会进行日志打印或者debug调试,以此来分析运行时的一些信息,便于发现bug和问题。AndroidStudio的Debug功能很好用,但是有时候有些情况下,就显得不是那么快......
  • 关于 Android 应用多进程的整理
    在计算机操作系统中,进程是进行资源分配和调度的基本单位。这对于基于Linux内核的Android系统也不例外。在Android的设计中,一个应用默认有一个(主)进程。但是我们通过配置可......
  • Android中使用ViewStub提高布局性能
    在Android开发中,View是我们必须要接触的用来展示的技术.通常情况下随着View视图的越来越复杂,整体布局的性能也会随之下降.这里介绍一个在某些场景下提升布局性能的View,它......
  • 小姿势 之 Android Studio 3.5 Retry 问题解决
    总会有那么一个人,让你觉得这个世界一切都是值得的。纵使结果不尽人意,曾经拥有即是最好。前言家里的MBP静静地躺了一段时间,某天心血来潮想嗨起来,其实就是瞎折腾一把,结果......
  • Android代码规范利器: Checkstyle
    程序代码向来都不仅仅是用来运行的,写的一手好代码,易读,可维护应该是每个程序员所追求的。每个团队都(应该)有一套优良统一的代码规范,而规范的检查依赖于人工检测就不太现实,好在......
  • 技巧篇 - Android Studio Build Failure 错误直观展示
    LZ-Says:爱走了,心痛了,一切,又将如何?前言技术,以原点为中心,周围不断辐射,形成独特圈子。AndroidStudio作为谷歌Baba亲儿子,继承了其优秀的血统之外,还增加很多nice的小tips......
  • 探究Kotlin的局部方法
    在Kotlin中,定义方法很有趣,不仅仅因为方法的关键字是fun(function前几个字符),还是因为你会惊奇的发现,它允许我们在方法中定义方法。如下funmethodA(){funmethodB(){......