Android從上車到漂移之ButterKnife完全解析
一、前言
ButterKnife——通過註解的方式生成View欄位、資源繫結和方法繫結的樣板程式碼,是一款老司機書寫UI佈局的必備神器!自從有了ButterKnife,媽媽再也不用擔心我findViewbyid(),find到手抽筋。
本文基於最新的8.7.0版本進行分析,不同版本可能實現方式有所差異,請知悉。
二、上車
下載Android studio 外掛Android ButterKnife Zelezny,一鍵生成模板程式碼。更多姿勢,請參考官方文件。作為一個老司機,上車不是我們的重點,漂移才是我們的目標!
三、漂移
checkout下來原始碼,工程結構如圖,核心實現模組是butterknife、butterknife-annotations、butterknife-compiler。
首先從入口開始,以activity的bind為例,程式碼如下:
@NonNull @UiThread
public static Unbinder bind(@NonNull Activity target) {
View sourceView = target.getWindow().getDecorView();
return createBinding(target, sourceView);
}
先獲取activity的根佈局DecorView,然後作為引數傳遞給createBinding()方法,該方法就是通過建構函式new出一個“clsName + “_ViewBinding”的class物件。看一下createBinding()方法的關鍵程式碼:
private static Unbinder createBinding(@NonNull Object target, @NonNull View source) {
Class<?> targetClass = target.getClass();
if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());
Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
if (constructor == null) {
return Unbinder.EMPTY;
}
//noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
try {
return constructor.newInstance(target, source);
} catch (IllegalAccessException e) {
throw new RuntimeException("Unable to invoke " + constructor, e);
}
繼續追蹤 findBindingConstructorForClass()的原始碼:
@Nullable @CheckResult @UiThread
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
if (bindingCtor != null) {
if (debug) Log.d(TAG, "HIT: Cached in binding map.");
return bindingCtor;
}
String clsName = cls.getName();
if (clsName.startsWith("android.") || clsName.startsWith("java.")) {
if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
return null;
}
try {
Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
//noinspection unchecked
bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
if (debug) Log.d(TAG, "HIT: Loaded binding class and constructor.");
} catch (ClassNotFoundException e) {
if (debug) Log.d(TAG, "Not found. Trying superclass " + cls.getSuperclass().getName());
bindingCtor = findBindingConstructorForClass(cls.getSuperclass());
} catch (NoSuchMethodException e) {
throw new RuntimeException("Unable to find binding constructor for " + clsName, e);
}
BINDINGS.put(cls, bindingCtor);
return bindingCtor;
}
該方法首先從BINDINGS這個LinkedHashMap中查詢快取,如果命中,直接返回bindingCtor,bindingCtor是實現了Unbinder介面的子類的建構函式;如果為null,則通過ClassLoade載入一個”clsName + “_ViewBinding”“的類,然後返回其建構函式。其中clsName 就是我們上文呼叫bind()的activity,最後把它put到BINDINGS這個集合中。
那麼”clsName + “_ViewBinding”“的這個類在哪?裡面實現了什麼邏輯?又是怎麼生成的?這些才是今天的重點!接下來我們一個個解答:
3.1 “clsName + “_ViewBinding”“的這個類在哪?:
大家根據截圖一層一層追進去就可以找到相應程式碼。大致是:app->build->generated->source->apt->”打包渠道”->clsName類在工程中所在目錄
3.2 “clsName + “_ViewBinding”“的這個類實現了什麼邏輯?
如上所述,該類實現了Unbinder介面,有兩個過載的建構函式,和一個unbind()方法,unbind()方法很簡單,顧名思義就是解除繫結,釋放資源,沒啥好說的。
public class BusinessmenListActivity_ViewBinding implements Unbinder {
private BusinessmenListActivity target;
@UiThread
public BusinessmenListActivity_ViewBinding(BusinessmenListActivity target) {
this(target, target.getWindow().getDecorView());
}
@UiThread
public BusinessmenListActivity_ViewBinding(BusinessmenListActivity target, View source) {
this.target = target;
target.mRecyclerView = Utils.findRequiredViewAsType(source, R.id.rv_bus, "field 'mRecyclerView'", RecyclerView.class);
target.mRefreshLayout = Utils.findRequiredViewAsType(source, R.id.refresh_layout, "field 'mRefreshLayout'", SwipeRefreshLayout.class);
}
@Override
@CallSuper
public void unbind() {
BusinessmenListActivity target = this.target;
if (target == null) throw new IllegalStateException("Bindings already cleared.");
this.target = null;
target.mRecyclerView = null;
target.mRefreshLayout = null;
}
}
我們重點看一下Utils.findRequiredViewAsType()的程式碼,就是這個方法幫我們實現了繫結View相關邏輯。先呼叫findRequiredView()方法返回Viwe物件,然後再castView()成具體的子View。比較簡單,直接看程式碼相信都能看懂:
public static <T> T findRequiredViewAsType(View source, @IdRes int id, String who,
Class<T> cls) {
View view = findRequiredView(source, id, who);
return castView(view, id, who, cls);
}
findRequiredView()程式碼:
public static View findRequiredView(View source, @IdRes int id, String who) {
View view = source.findViewById(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.");
}
castView()程式碼:
public static <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);
}
}
至此,就完成了View繫結的所有邏輯。
3.3 “clsName + “_ViewBinding”“.java檔案是如何生成的?
該類的生成主要依賴一個叫做APT的工具。APT(Annotation Processing Tool)是一種處理註解的工具,它對原始碼檔案進行檢測找出其中的Annotation,使用Annotation進行額外的處理。
使用apt需要繼承AbstractProcessor類,同時有幾個核心方法需要實現,分別是:
init()主要做一些初始化操作;
getSupportedAnnotationTypes(),顧名思義,獲取所有支援的註解型別;
process()處理註解相關邏輯,”clsName + “_ViewBinding”“.java檔案生成的核心邏輯就在這裡!
在ButterKnife中,有一個ButterKnifeProcessor類,該類就是處理ButterKnife註解相關邏輯的類。
3.3.1 我們先從init()方法開始:初始化。
@Override public synchronized void init(ProcessingEnvironment env) {
super.init(env);
String sdk = env.getOptions().get(OPTION_SDK_INT);
if (sdk != null) {
try {
this.sdk = Integer.parseInt(sdk);
} catch (NumberFormatException e) {
env.getMessager()
.printMessage(Kind.WARNING, "Unable to parse supplied minSdk option '"
+ sdk
+ "'. Falling back to API 1 support.");
}
}
debuggable = !"false".equals(env.getOptions().get(OPTION_DEBUGGABLE));
elementUtils = env.getElementUtils();
typeUtils = env.getTypeUtils();
filer = env.getFiler();
try {
trees = Trees.instance(processingEnv);
} catch (IllegalArgumentException ignored) {
}
}
init()方法中,沒有過多邏輯,只是有幾個變數需要說明一下,這幾個主要是註解處理時用到的工具類。
private Elements elementUtils;
private Types typeUtils;
private Filer filer;
private Trees trees;
Elements:一個用來處理Element的工具類,原始碼的每一個部分都是一個特定型別的Element。例如,包名、欄位、方法等等。
Types:一個用來處理TypeMirror的工具類,比如判斷該元素是class還是interface;
Filer:生成檔案;
Trees :樹,遍歷檔案用到。
3.3.2 接下來看一下getSupportedAnnotationTypes()方法:獲取所有支援的註解型別。
@Override public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new LinkedHashSet<>();
for (Class<? extends Annotation> annotation : getSupportedAnnotations()) {
types.add(annotation.getCanonicalName());
}
return types;
}
private Set<Class<? extends Annotation>> getSupportedAnnotations() {
Set<Class<? extends Annotation>> annotations = new LinkedHashSet<>();
annotations.add(BindAnim.class);
annotations.add(BindArray.class);
annotations.add(BindBitmap.class);
annotations.add(BindBool.class);
annotations.add(BindColor.class);
annotations.add(BindDimen.class);
annotations.add(BindDrawable.class);
annotations.add(BindFloat.class);
annotations.add(BindFont.class);
annotations.add(BindInt.class);
annotations.add(BindString.class);
annotations.add(BindView.class);
annotations.add(BindViews.class);
annotations.addAll(LISTENERS);
return annotations;
}
該方法的大概意思就是,將所有ButterKnife用到的註解全部新增到支援的註解集合中。
我們來瞄一眼最熟悉的BindView.class
@Retention(CLASS) @Target(FIELD)
public @interface BindView {
/** View ID to which the field will be bound. */
@IdRes int value();
}
程式碼很簡單,但是有幾個註解相關的點需要說明一下,
元註解:元註解的作用就是負責註解其他註解。相關元註解:
[email protected]:
表示註解型別所適用的程式元素的種類。
[email protected]:
表示該註解型別的註解保留的時長。
SOURCE 僅存在Java原始檔,經過編譯器後便丟棄相應的註解;
CLASS 存在Java原始檔,以及經編譯器後生成的Class位元組碼檔案,但在執行時VM不再保留註釋;
RUNTIME 存在原始檔、編譯生成的Class位元組碼檔案,以及保留在執行時VM中,可通過反射性地讀取註解。
對應到BindView這個註解中,我們可以知道,該註解適用的型別為欄位,且會打包到Class位元組碼檔案中,該註解接收的值型別為@IdRes int型別。
3.3.3 最後process()方法:遍歷所有註解->根據註解生成相應的程式碼->生成java檔案。
@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingSet binding = entry.getValue();
JavaFile javaFile = binding.brewJava(sdk, debuggable);
try {
javaFile.writeTo(filer);
} catch (IOException e) {
error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
}
}
return false;
}
先瞄一眼findAndParseTargets()方法核心程式碼:
private Map<TypeElement, BindingSet> findAndParseTargets(RoundEnvironment env) {
Map<TypeElement, BindingSet.Builder> builderMap = new LinkedHashMap<>();
Set<TypeElement> erasedTargetNames = new LinkedHashSet<>();
// ...... 此處省略若干行程式碼
// Process each @BindView element.
for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
// we don't SuperficialValidation.validateElement(element)
// so that an unresolved View type can be generated by later processing rounds
try {
parseBindView(element, builderMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindView.class, e);
}
}
}
// Associate superclass binders with their subclass binders. This is a queue-based tree walk
// which starts at the roots (superclasses) and walks to the leafs (subclasses).
Deque<Map.Entry<TypeElement, BindingSet.Builder>> entries =
new ArrayDeque<>(builderMap.entrySet());
Map<TypeElement, BindingSet> bindingMap = new LinkedHashMap<>();
while (!entries.isEmpty()) {
Map.Entry<TypeElement, BindingSet.Builder> entry = entries.removeFirst();
TypeElement type = entry.getKey();
BindingSet.Builder builder = entry.getValue();
TypeElement parentType = findParentType(type, erasedTargetNames);
if (parentType == null) {
bindingMap.put(type, builder.build());
} else {
BindingSet parentBinding = bindingMap.get(parentType);
if (parentBinding != null) {
builder.setParent(parentBinding);
bindingMap.put(type, builder.build());
} else {
// Has a superclass binding but we haven't built it yet. Re-enqueue for later.
entries.addLast(entry);
}
}
}
return bindingMap;
還是以@BindView 為例,其他的類似,不再一一贅述。關鍵程式碼:
// Process each @BindView element.
for (Element element : env.getElementsAnnotatedWith(BindView.class)) {
// we don't SuperficialValidation.validateElement(element)
// so that an unresolved View type can be generated by later processing rounds
try {
parseBindView(element, builderMap, erasedTargetNames);
} catch (Exception e) {
logParsingError(element, BindView.class, e);
}
}
}
遍歷所有使用@BindView註解的Element,繼續看parseBindView()程式碼:
private void parseBindView(Element element, Map<TypeElement, BindingSet.Builder> builderMap,
Set<TypeElement> erasedTargetNames) {
TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
// Assemble information on the field.
//獲取繫結的View的id,即:R.id.xx.
int id = element.getAnnotation(BindView.class).value();
//判斷該元素是否已經繫結過,如果繫結過,返回錯誤,否則,呼叫getOrCreateBindingBuilder()方法
BindingSet.Builder builder = builderMap.get(enclosingElement);
QualifiedId qualifiedId = elementToQualifiedId(element, id);
if (builder != null) {
String existingBindingName = builder.findExistingBindingName(getId(qualifiedId));
if (existingBindingName != null) {
error(element, "Attempt to use @%s for an already bound ID %d on '%s'. (%s.%s)",
BindView.class.getSimpleName(), id, existingBindingName,
enclosingElement.getQualifiedName(), element.getSimpleName());
return;
}
} else {
builder = getOrCreateBindingBuilder(builderMap, enclosingElement);
}
String name = simpleName.toString();
TypeName type = TypeName.get(elementType);
boolean required = isFieldRequired(element);
builder.addField(getId(qualifiedId), new FieldViewBinding(name, type, required));
// Add the type-erased version to the valid binding targets set.
erasedTargetNames.add(enclosingElement);
}
首先,通過 int id = element.getAnnotation(BindView.class).value();
獲取繫結的View的id,即:R.id.xx.;
再通過elementToQualifiedId()方法,生成一個合格標識:
private QualifiedId elementToQualifiedId(Element element, int id) {
return new QualifiedId(elementUtils.getPackageOf(element).getQualifiedName().toString(), id);
}
第三步,從已經繫結的元素中查詢該元素是否存在,如果存在,返回錯誤,不允許重複繫結,否則呼叫getOrCreateBindingBuilder()方法生成一個繫結物件:
private BindingSet.Builder getOrCreateBindingBuilder(
Map<TypeElement, BindingSet.Builder> builderMap, TypeElement enclosingElement) {
BindingSet.Builder builder = builderMap.get(enclosingElement);
if (builder == null) {
builder = BindingSet.newBuilder(enclosingElement);
builderMap.put(enclosingElement, builder);
}
return builder;
}
進一步看看 BindingSet.newBuilder(enclosingElement)方法
static Builder newBuilder(TypeElement enclosingElement) {
TypeMirror typeMirror = enclosingElement.asType();
boolean isView = isSubtypeOfType(typeMirror, VIEW_TYPE);
boolean isActivity = isSubtypeOfType(typeMirror, ACTIVITY_TYPE);
boolean isDialog = isSubtypeOfType(typeMirror, DIALOG_TYPE);
TypeName targetType = TypeName.get(typeMirror);
if (targetType instanceof ParameterizedTypeName) {
targetType = ((ParameterizedTypeName) targetType).rawType;
}
String packageName = getPackage(enclosingElement).getQualifiedName().toString();
String className = enclosingElement.getQualifiedName().toString().substring(
packageName.length() + 1).replace('.', '$');
ClassName bindingClassName = ClassName.get(packageName, className + "_ViewBinding");
boolean isFinal = enclosingElement.getModifiers().contains(Modifier.FINAL);
return new Builder(targetType, bindingClassName, isFinal, isView, isActivity, isDialog);
}
就是這裡將生成的java檔案類名定義為“className + “_ViewBinding””
第四步,回到parseBindView()方法,為繫結物件新增繫結的View欄位:
builder.addField(getId(qualifiedId), new FieldViewBinding(name, type, required));
第五步,關聯父類繫結的資源(view、string、listener等),並把她們新增到 Map
// Associate superclass binders with their subclass binders. This is a queue-based tree walk
// which starts at the roots (superclasses) and walks to the leafs (subclasses).
Deque<Map.Entry<TypeElement, BindingSet.Builder>> entries =
new ArrayDeque<>(builderMap.entrySet());
Map<TypeElement, BindingSet> bindingMap = new LinkedHashMap<>();
while (!entries.isEmpty()) {
Map.Entry<TypeElement, BindingSet.Builder> entry = entries.removeFirst();
TypeElement type = entry.getKey();
BindingSet.Builder builder = entry.getValue();
TypeElement parentType = findParentType(type, erasedTargetNames);
if (parentType == null) {
bindingMap.put(type, builder.build());
} else {
BindingSet parentBinding = bindingMap.get(parentType);
if (parentBinding != null) {
builder.setParent(parentBinding);
bindingMap.put(type, builder.build());
} else {
// Has a superclass binding but we haven't built it yet. Re-enqueue for later.
entries.addLast(entry);
}
}
主要看一下關鍵程式碼 BindingSet build()程式碼,以及BindingSet的建構函式:
BindingSet build() {
ImmutableList.Builder<ViewBinding> viewBindings = ImmutableList.builder();
for (ViewBinding.Builder builder : viewIdMap.values()) {
viewBindings.add(builder.build());
}
return new BindingSet(targetTypeName, bindingClassName, isFinal, isView, isActivity, isDialog,
viewBindings.build(), collectionBindings.build(), resourceBindings.build(),
parentBinding);
}
private BindingSet(TypeName targetTypeName, ClassName bindingClassName, boolean isFinal,
boolean isView, boolean isActivity, boolean isDialog, ImmutableList<ViewBinding> viewBindings,
ImmutableList<FieldCollectionViewBinding> collectionBindings,
ImmutableList<ResourceBinding> resourceBindings, BindingSet parentBinding) {
this.isFinal = isFinal;
this.targetTypeName = targetTypeName;
this.bindingClassName = bindingClassName;
this.isView = isView;
this.isActivity = isActivity;
this.isDialog = isDialog;
this.viewBindings = viewBindings;
this.collectionBindings = collectionBindings;
this.resourceBindings = resourceBindings;
this.parentBinding = parentBinding;
}
至此,所有需要繫結的資源已經新增到集合當中,只差生成程式碼,可謂萬事俱備只欠東風!
回到process()程式碼:
@Override public boolean process(Set<? extends TypeElement> elements, RoundEnvironment env) {
Map<TypeElement, BindingSet> bindingMap = findAndParseTargets(env);
for (Map.Entry<TypeElement, BindingSet> entry : bindingMap.entrySet()) {
TypeElement typeElement = entry.getKey();
BindingSet binding = entry.getValue();
JavaFile javaFile = binding.brewJava(sdk, debuggable);
try {
javaFile.writeTo(filer);
} catch (IOException e) {
error(typeElement, "Unable to write binding for type %s: %s", typeElement, e.getMessage());
}
}
return false;
}
通過第一行程式碼,我們幾經周折終於掌握了她的來龍去脈,還剩一個for迴圈。for迴圈無非就是遍歷生成相應的”clsName + “_ViewBinding”“.java檔案,具體怎麼生成的,我們跟進去瞄一眼:
JavaFile brewJava(int sdk, boolean debuggable) {
return JavaFile.builder(bindingClassName.packageName(), createType(sdk, debuggable))
.addFileComment("Generated code from Butter Knife. Do not modify!")
.build();
}
這裡涉及一個重要的知識點:JavaPoet。此為何物? 套用官方的簡介就是:“JavaPoet is a Java API for generating .java source files.” 簡直精闢得不能再精闢!
簡單理解——就是用來生成java檔案的,這不正是我們所要的東風嗎?具體的使用姿勢,不是本文的重點,可以檢視官方文件,文件寫得相當詳細,在此不再一一贅述,本文只對用到的地方做一些解析。
看一下createType()方法:
private TypeSpec createType(int sdk, boolean debuggable) {
TypeSpec.Builder result = TypeSpec.classBuilder(bindingClassName.simpleName())
.addModifiers(PUBLIC);
if (isFinal) {
result.addModifiers(FINAL);
}
if (parentBinding != null) {
result.superclass(parentBinding.bindingClassName);
} else {
result.addSuperinterface(UNBINDER);
}
if (hasTargetField()) {
result.addField(targetTypeName, "target", PRIVATE);
}
if (isView) {
result.addMethod(createBindingConstructorForView());
} else if (isActivity) {
result.addMethod(createBindingConstructorForActivity());
} else if (isDialog) {
result.addMethod(createBindingConstructorForDialog());
}
if (!constructorNeedsView()) {
// Add a delegating constructor with a target type + view signature for reflective use.
result.addMethod(createBindingViewDelegateConstructor());
}
result.addMethod(createBindingConstructor(sdk, debuggable));
if (hasViewBindings() || parentBinding == null) {
result.addMethod(createBindingUnbindMethod(result));
}
return result.build();
}
如果你已經瞭解了JavaPoet之後再來看這個程式碼,其實可以一目瞭然,這裡不做過多解釋,僅僅驗證一下,我們的設想與生成的”clsName + “_ViewBinding”“.java檔案內容是否一致即可。
主要看一下createBindingConstructor()方法,該方法是生成”clsName + “_ViewBinding”“.java程式碼的核心:
private MethodSpec createBindingConstructor(int sdk, boolean debuggable) {
MethodSpec.Builder constructor = MethodSpec.constructorBuilder()
.addAnnotation(UI_THREAD)
.addModifiers(PUBLIC);
if (hasMethodBindings()) {
constructor.addParameter(targetTypeName, "target", FINAL);
} else {
constructor.addParameter(targetTypeName, "target");
}
if (constructorNeedsView()) {
constructor.addParameter(VIEW, "source");
} else {
constructor.addParameter(CONTEXT, "context");
}
if (hasUnqualifiedResourceBindings()) {
// Aapt can change IDs out from underneath us, just suppress since all will work at runtime.
constructor.addAnnotation(AnnotationSpec.builder(SuppressWarnings.class)
.addMember("value", "$S", "ResourceType")
.build());
}
if (hasOnTouchMethodBindings()) {
constructor.addAnnotation(AnnotationSpec.builder(SUPPRESS_LINT)
.addMember("value", "$S", "ClickableViewAccessibility")
.build());
}
if (parentBinding != null) {
if (parentBinding.constructorNeedsView()) {
constructor.addStatement("super(target, source)");
} else if (constructorNeedsView()) {
constructor.addStatement("super(target, source.getContext())");
} else {
constructor.addStatement("super(target, context)");
}
constructor.addCode("\n");
}
if (hasTargetField()) {
constructor.addStatement("this.target = target");
constructor.addCode("\n");
}
if (hasViewBindings()) {
if (hasViewLocal()) {
// Local variable in which all views will be temporarily stored.
constructor.addStatement("$T view", VIEW);
}
for (ViewBinding binding : viewBindings) {
addViewBinding(constructor, binding, debuggable);
}
for (FieldCollectionViewBinding binding : collectionBindings) {
constructor.addStatement("$L", binding.render(debuggable));
}
if (!resourceBindings.isEmpty()) {
constructor.addCode("\n");
}
}
if (!resourceBindings.isEmpty()) {
if (constructorNeedsView()) {
constructor.addStatement("$T context = source.getContext()", CONTEXT);
}
if (hasResourceBindingsNeedingResource(sdk)) {
constructor.addStatement("$T res = context.getResources()", RESOURCES);
}
for (ResourceBinding binding : resourceBindings) {
constructor.addStatement("$L", binding.render(sdk));
}
}
return constructor.build();
}
大功告成!所有的邏輯執行完畢之後,就生成是我們3.2節對應的程式碼。
4、總結
祝各位司機漂移成功!
相關推薦
Android從上車到漂移之ButterKnife完全解析
一、前言 ButterKnife——通過註解的方式生成View欄位、資源繫結和方法繫結的樣板程式碼,是一款老司機書寫UI佈局的必備神器!自從有了ButterKnife,媽媽再也不用擔心我findViewbyid(),find到手抽筋。 本文基於
Android 外掛化入手指南之classLoader完全解析
原文:https://blog.csdn.net/u012124438/article/details/53235848 Java程式碼都是寫在Class裡面的,程式執行在虛擬機器上時,虛擬機器需要把需要的Class載入進來才能建立例項物件並工作,而完成這一個載入工作的角色就是Cla
Android外掛化學習之路(二)之ClassLoader完全解析
Java程式碼都是寫在Class裡面的,程式執行在虛擬機器上時,虛擬機器需要把需要的Class載入進來才能建立例項物件並工作,而完成這一個載入工作的角色就是ClassLoader。 類載入器ClassLoader介紹 Android的Dalvik/ART虛擬
Android 多線程之IntentService 完全詳解
required xmlns 抽象 bitmap 圖片 on() 使用 ecif ati 關聯文章: Android 多線程之HandlerThread 完全詳解 Android 多線程之IntentService 完全詳解 android多線程-AsyncTask之
Android新特性介紹,ConstraintLayout完全解析
mat 區別 界面 -s 自己 解決 match roo pre 本篇文章的主題是ConstraintLayout。其實ConstraintLayout是Android Studio 2.2中主要的新增功能之一,也是Google在去年的I/O大會上重點宣傳的一個功能。我們都
【Android自助餐】Handler訊息機制完全解析(二)MessageQueue的佇列管理
Android自助餐Handler訊息機制完全解析(二)MessageQueue的佇列管理 Android自助餐Handler訊息機制完全解析二MessageQueue的佇列管理 新增到訊息佇列enqueueMessage 從佇
【Android自助餐】Handler訊息機制完全解析(五)鳥瞰與總結
Android自助餐Handler訊息機制完全解析(五)鳥瞰與總結 Android自助餐Handler訊息機制完全解析五鳥瞰與總結 Message MessageQueue Handler Looper
【Android自助餐】Handler訊息機制完全解析(四)Looper解析
Android自助餐Handler訊息機制完全解析(四)Looper解析 Android自助餐Handler訊息機制完全解析四Looper解析 Looper 初始化prepare 提供loope
Android 多執行緒之HandlerThread 完全詳解
之前對執行緒也寫過幾篇文章,不過倒是沒有針對android,因為java與android線上程方面大部分還是相同,不過本篇我們要介紹的是android的專屬類HandlerThread,因為HandlerThread在設定思想上還是挺值得我們學習的,那麼我們下面來
Android 開源框架Universal-Image-Loader完全解析(一)--- 基本介紹及使用
大家好!差不多兩個來月沒有寫文章了,前段時間也是在忙換工作的事,準備筆試面試什麼的事情,現在新工作找好了,新工作自己也比較滿意,唯一遺憾的就是自己要去一個新的城市,新的環境新的開始,希望自己能儘快的適應新環境,現在在準備交接的事情,自己也有一些時間了,所以就繼續給大家分享And
Android FM模組學習之四原始碼解析(三)
由於最近一直忙專案,沒有時間來更新文件,今天抽空來寫一點,希望大家可以學習使用! 這一章當然還是來分析FM模組的原始碼。FmReceiver.java publicFmReceiver(String devicePath,FmRxEvCallbacks
Android 開源框架Universal-Image-Loader完全解析(二)--- 圖片快取策略詳解
本篇文章繼續為大家介紹Universal-Image-Loader這個開源的圖片載入框架,介紹的是圖片快取策略方面的,如果大家對這個開源框架的使用還不瞭解,大家可以看看我之前寫的一篇文章Android 開源框架Universal-Image-Loader完全解析(一)---
android從零單排之手機通訊錄的讀取、新增、刪除、查詢
工作中牽扯到了對android手機通訊錄的系列操作,網上查找了好多資料,大多數都介紹的是模模糊糊,終於找到了一篇靠譜的,於是摘抄下來,大家共同學習:</span> Android聯絡人資料庫檔案(contact2.db) 有研究過手機通訊錄資料的童鞋肯定
Android N App分屏模式完全解析(下)
在上篇中,介紹了什麼是App分屏模式,以及如何設定我們的App來進入分屏模式。這次我們看一下,作為開發者,我們應該如何讓自己的App進入分屏模式,當App進入分屏模式時,我們注意哪些問題。 簡單地說,我認為除了保證分屏時App功能、效能正常以外,我們需要重點學習 如何在分屏模式下開啟新的Activity 以
Android從零單排之免費簡訊驗證
介紹 簡訊驗證功能大家都很熟悉了。在很多地方都能見到,註冊新使用者或者短息驗證支付等。簡訊驗證利用簡訊驗證碼來註冊會員,大大降低了非法註冊,很大程度上提高了使用者賬戶的安全性。 目前市面上已經
Android FM模組學習之四原始碼解析(四)
我今天想分享的是FM模組的儲存方法,即FmSharedPreferences.java FmSharedPreferences(Context context)在構造方法中載入Load()方法, public void Load(){ Log.d(
Android FM模組學習之四原始碼解析(一)
前一章我們瞭解了FM手動調頻,接下來我們要分析FM模組用到的原始碼。此原始碼是基於高通平臺的,別的平臺都大同小異,只不過是平臺自己作了些小改動而已。 首先要看的當然是主activity,
C語言重點——指標篇(一文讓你完全搞懂指標)| 從記憶體理解指標 | 指標完全解析
> 有乾貨、更有故事,微信搜尋【**程式設計指北**】關注這個不一樣的程式設計師,等你來撩~ **注:這篇文章好好看完一定會讓你掌握好指標的本質** C語言最核心的知識就是指標,所以,這一篇的文章主題是「指標與記憶體模型」 說到指標,就不可能脫離開記憶體,學會指標的人分為兩種,一種是不瞭解記憶體
一篇好文之Android資料庫GreenDao的完全解析
今日科技快訊 小米近日稱,財政部此次公告的檢查為2017年財政部會計監督檢查,是針對2016年的會計資訊質量進行的檢查。公司存在部分費用攤銷核算錯誤、對外贈送商品未作為視同銷售行為申報繳稅、報銷發票管理不規範、費用管理制度不完善等問題。以上問題均已整改完成,並獲得財政部
Android進階——效能優化之程序拉活原理及手段完全解析(二)
引言 上一篇文章Android進階——效能優化之程序保活原理及手段完全解析(一)總結了Android程序和執行緒的相關知識,主要介紹了幾種提升程序優先順序的手段,通常僅僅是提高優先順序只能讓你的程序存活時間久一點,但是真正的被殺死之後就不會自動拉活的,如果你的程