世界公認最高效的學習方法 :
-
選擇一個你要學習的內容
-
想象如果你要將這些內容教授給一名新人,該如何講解
-
如果過程中出了問題,重新回顧這個內容
-
簡化:容你的講解越來越簡單易懂
————理查德費曼學習法
一、前言
以前一直在寫一些常用的功能模塊,如何使用及封裝。後來發現網上一搜一大堆,關鍵是寫得比我好也就算了,更過分的是字數比我還多,簡直不讓人活了。後來自己就搞了個工具Demo項目,把常用的功能都弄上去。也不打算寫這類文章,如果朋友們在開發功能上遇到障礙可以去項目(文章最下面)找找,說不定就有了。
如今只能被逼改行給大家介紹一些Android目前比較火的開源庫。可以添加到項目當中,提高開發效率,並讓項目開發起來更輕鬆方便,易與維護。提高逼格.... (又在胡扯)
二、簡介
本篇文章要給大家介紹的是最容易使用,也是最簡單的ButterKnife。用過的人都知道爲什麼好用,沒有過的也不用後悔,現在學了也不喫虧,花10分鐘看完本篇文章就懂了。 (10分鐘你買不了喫虧,也買不了上當)
在開始講之前給大家看看大綱,也許一眼你就學會了也說不一定
本篇圍繞幾個問題展開:
- 是什麼?
- 有什麼用?
- 爲什麼要用?
- 怎麼用?
- 原理是什麼?
等明白這幾個問題後(就可以渡劫了),在給大家介紹個好用的輔助插件。
三、ButterKnife入門
(1)是什麼?
一句話:是出自JakeWharton大神開源的一個依賴注入庫,通過註解的方式來替代android中view的相關操作。
那麼問題來了:什麼是依賴注入? 這裏簡單介紹介紹一下,忘了是哪個博客CP過來的了 。
什麼是依賴注入?
依賴注入通俗的理解就是,當A類需要引用B類的對象時,將B類的對象傳入A類的過程就是依賴注入。依賴注入最重要的作用就是解耦,降低類之間的耦合,保證代碼的健壯性、可維護性、可擴展性。
常用的實現方式有構造函數、set方法、實現接口等。例如:
// 通過構造函數 public class A { B b; public A(B b) { this.b = b; } } // 通過set方法 public class A { B b; public void setB(B b) { this.b = b; } }
(2)有什麼用?
一句話:減少大量的findViewById以及setOnClickListener代碼,且對性能的影響較小。
(3)爲什麼要用?
一句話:使用簡單,容易上手、學習成本低、對性能影響小
四、ButterKnife渡劫
(4)怎麼用?
1.添加依賴
android {
...
// Butterknife requires Java 8.
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation 'com.jakewharton:butterknife:10.2.0'
annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.0'
//若是kotlin 使用kapt 代替annotationProcessor
}
如果要在庫中使用,將插件添加到buildscript
buildscript {
repositories {
mavenCentral()
google()
}
dependencies {
//classpath 'com.android.tools.build:gradle:3.3.0'
classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.0'
}
}
並在moudle添加這些代碼
apply plugin: 'com.android.library'
apply plugin: 'com.jakewharton.butterknife'
在庫中使用R2代替R
class ExampleActivity extends Activity {
@BindView(R2.id.user) EditText username;
@BindView(R2.id.pass) EditText password;
...
}
如果不需要再庫中使用 ,直接依賴第一塊代碼既可
2.綁定佈局
①Activity
//Activity中的使用
class ExampleActivity extends Activity {
@BindView(R.id.title) TextView title;
@BindView(R.id.subtitle) TextView subtitle;
@BindView(R.id.footer) TextView footer;
@Override public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.simple_activity);
ButterKnife.bind(this);
// TODO Use fields...
}
}
②Fragment
//Fragment使用
public class FancyFragment extends Fragment {
@BindView(R.id.button1) Button button1;
@BindView(R.id.button2) Button button2;
@Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fancy_fragment, container, false);
ButterKnife.bind(this, view);
// TODO Use fields...
return view;
}
}
③適配器
//適配器中使用
public class MyAdapter extends BaseAdapter {
@Override public View getView(int position, View view, ViewGroup parent) {
ViewHolder holder;
if (view != null) {
holder = (ViewHolder) view.getTag();
} else {
view = inflater.inflate(R.layout.whatever, parent, false);
holder = new ViewHolder(view);
view.setTag(holder);
}
holder.name.setText("John Doe");
// etc...
return view;
}
static class ViewHolder {
@BindView(R.id.title) TextView name;
@BindView(R.id.job_title) TextView jobTitle;
public ViewHolder(View view) {
ButterKnife.bind(this, view);
}
}
}
④其他對象
//其他類中可以通過
View view = inflater.inflate(R.layout.fancy_fragment, null);
ButterKnife.bind(this, view);
通過ButterKnife.bind()方法直接跟對象綁定在一起,之後給控件添加註釋 表示這個控件已經和對象綁定。就可以直接使用了
3.聲明並使用
①綁定控件
//第一種方式:@BindView
@BindView(R.id.tv_hell_world)
TextView tvHellWorld;
//使用方式:
tvHellWorld.setText("Hello world");
//第二種方式:@@BindViews
@BindViews({ R.id.first_name, R.id.middle_name, R.id.last_name })
List nameViews;
//很遺憾的是最新版已經找不到該方法了 只能當List使用
ButterKnife.apply(nameViews, DISABLE);
ButterKnife.apply(nameViews, ENABLED, false);
static final ButterKnife.Action<View> DISABLE = new ButterKnife.Action<View>() {
@Override public void apply(View view, int index) {
view.setEnabled(false);
}
};
static final ButterKnife.Setter<View, Boolean> ENABLED = new ButterKnife.Setter<View, Boolean>() {
@Override public void set(View view, Boolean value, int index) {
view.setEnabled(value);
}
};
ButterKnife.apply(nameViews, View.ALPHA, 0.0f);
②綁定資源
//values文件裏面的資源綁定
@BindString 聲明:@BindString(R.string.title) String title;
@BindDrawable 聲明: @BindDrawable(R.drawable.graphic) Drawable graphic
@BindColor 聲明:@BindColor(R.color.red) int red;
@BindDimen 聲明:@BindDimen(R.dimen.spacer) float spacer;
③綁定監聽事件
@OnClick
//單個點擊事件
@OnClick(R.id.submit)
public void submit(View view) {
// TODO submit data to server...
}
//多個點擊事件
@OnClick({R.id.btn_send,R.id.btn_close,R.id.btn_canle})
public void onViewClicked(View view) {
switch (view.getId()){
case R.id.btn_send:
break;
case R.id.btn_close:
break;
case R.id.btn_canle:
break;
}
}
方法參數可變
//無參數情況
@OnClick(R.id.submit)
public void submit() {
// TODO submit data to server...
}
//指定特定類型參數情況
@OnClick(R.id.submit)
public void sayHi(Button button) {
button.setText("Hello!");
}
④多種監聽器
//選中與未選中監聽
@OnItemSelected(R.id.list_view)
void onItemSelected(int position) {
// TODO ...
}
@OnItemSelected(value = R.id.maybe_missing, callback = NOTHING_SELECTED)
void onNothingSelected() {
// TODO ...
}
這裏只介紹比較常用的幾個註解,更多註解可以查看源碼。 使用方法大同小異
4.解綁
//Fragment具有不同於activity的生命週期,需要在合適的生命週期中進行解綁
public class FancyFragment extends Fragment {
@BindView(R.id.button1) Button button1;
@BindView(R.id.button2) Button button2;
private Unbinder unbinder;
@Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fancy_fragment, container, false);
unbinder = ButterKnife.bind(this, view);
// TODO Use fields...
return view;
}
@Override public void onDestroyView() {
super.onDestroyView();
unbinder.unbind();
}
}
根據文檔提示Fragment具有不同於Activity的生命週期。在onCreateView中綁定一個Fragment時,在onDestroyView中將視圖設置爲null。當您爲您調用bind時,Butter Knife返回一個Unbinder實例。在適當的生命週期回調中調用它的unbind方法
5.可選綁定
①默認情況下,@Bind和listener綁定都是必需的。如果找不到目標視圖,將引發異常
②要禁止這種行爲並創建可選綁定,請向字段添加@Nullable註釋或向方法添加@Optional註釋
③使用方式
@Nullable @BindView(R.id.might_not_be_there) TextView mightNotBeThere;
@Optional @OnClick(R.id.maybe_missing) void onMaybeMissingClicked() {
// TODO ...
}
5.注意事項
①ButterKnife.bind(this);必須在setContentView();之後調用;且父類綁定後,子類不需要再綁定
②在非Activity 類(eg:Fragment、ViewHold)中綁定: ButterKnife.bind(this,view);這裏的this不能替換成getActivity()
③使用ButterKnife修飾的方法和控件,不能用private or static 修飾,否則會報錯。
④文檔提示:Fragment中使用綁定時 ,需要再onDestroyView()中進行解綁
6.混淆
最後,別忘了在 proguard-rules.pro 文件中加入混淆代碼,確保在混淆後仍可以繼續運行。
-keep public class * implements butterknife.Unbinder { public <init>(**, android.view.View); }
-keep class butterknife.*
-keepclasseswithmembernames class * { @butterknife.* <methods>; }
-keepclasseswithmembernames class * { @butterknife.* <fields>; }
看到這裏,若還有不會的。建個項目照着步驟來一次,5分鐘就可搞定。也是最容易集成的庫之一。不試一試怎麼知道好不好用。
當學會了使用之後一起來研究一下ButterKnife是怎麼實現的。怎麼做出來 。
五、ButterKnife封神之路
(5)原理是什麼?
1.從開始調用的方法說起,ButterKnife都是從bind()開始執行。先看下ButterKnife裏面的bind方法,再來說說bind的作用
// butterKnife 有許多bind重載方法 最終都會傳遞到 bind(@NonNull Object target, @NonNull View source)方法中
//綁定Actvity
@NonNull @UiThread
public static Unbinder bind(@NonNull Activity target) {
//根據targert獲取Activity的根視圖
View sourceView = target.getWindow().getDecorView();
return bind(target, sourceView);
}
//綁定視圖
@NonNull @UiThread
public static Unbinder bind(@NonNull View target) {
return bind(target, target);
}
//綁定Dialog
@NonNull @UiThread
public static Unbinder bind(@NonNull Dialog target) {
View sourceView = target.getWindow().getDecorView();
return bind(target, sourceView);
}
//傳入的對象與Activity視圖綁定一起
@NonNull @UiThread
public static Unbinder bind(@NonNull Object target, @NonNull Activity source) {
View sourceView = source.getWindow().getDecorView();
return bind(target, sourceView);
}
//傳入的對象與Dialog視圖綁定一起
@NonNull @UiThread
public static Unbinder bind(@NonNull Object target, @NonNull Dialog source) {
View sourceView = source.getWindow().getDecorView();
return bind(target, sourceView);
}
//調用viewBinding構造函數傳入綁定對象與視圖最終獲取Unbinder對象
@NonNull @UiThread
public static Unbinder bind(@NonNull Object target, @NonNull View source) {
//獲取綁定對象的類
Class<?> targetClass = target.getClass();
//打印類名
if (debug) Log.d(TAG, "Looking up binding for " + targetClass.getName());
//通過綁定類獲取對應的viewBinding類的構造函數
Constructor<? extends Unbinder> constructor = findBindingConstructorForClass(targetClass);
//判斷爲空時返回 Unbinder的空實例
if (constructor == null) {
return Unbinder.EMPTY;
}
//noinspection TryWithIdenticalCatches Resolves to API 19+ only type.
try {
// 通過反射調用了viewBinding的構造函數創建了一個實例
return constructor.newInstance(target, source);
} catch (IllegalAccessException e) {
throw new RuntimeException("Unable to invoke " + constructor, e);
} catch (InstantiationException e) {
throw new RuntimeException("Unable to invoke " + constructor, e);
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
}
if (cause instanceof Error) {
throw (Error) cause;
}
throw new RuntimeException("Unable to create binding instance.", cause);
}
}
——通過bind()方法傳入一個對象與視圖最終獲取Unbinder的一個實例。那Unbinder是什麼,用來幹嘛呢。我們看下源碼
public interface Unbinder {
@UiThread void unbind();
Unbinder EMPTY = () -> { };
}
——Unbinder其實是個接口,裏面有個unbind方法。就是用來在Fragment中進行解綁時用的。還有個空實現,java 8寫法。通過上面的註釋,在未找到對應的ViewBinding構造函數時調用。
2.bind()方法中有個重要的方法,通過它獲取ViewBinding的構造函數。從而實現對ViewBinding類的調用。源碼如下
@VisibleForTesting
static final Map<Class<?>, Constructor<? extends Unbinder>> BINDINGS = new LinkedHashMap<>();
//通過綁定的類 查找對應的ViewBinding類 並獲取ViewBinding類的構造函數
@Nullable @CheckResult @UiThread
private static Constructor<? extends Unbinder> findBindingConstructorForClass(Class<?> cls) {
//ButterKnife定義了一個LinkedHashMap用來存儲綁定類和對應的viewBinding構造函數
//根據指定類查找對應的構造函數
Constructor<? extends Unbinder> bindingCtor = BINDINGS.get(cls);
//構造函數不爲空或者存儲對象的key含有這個類則返回構造函數
if (bindingCtor != null || BINDINGS.containsKey(cls)) {
if (debug) Log.d(TAG, "HIT: Cached in binding map.");
return bindingCtor;
}
//未得到構造函數後 繼續向下執行 通過反射獲取類名
String clsName = cls.getName();
//判斷是不是框架類 是則返回null
if (clsName.startsWith("android.") || clsName.startsWith("java.")
|| clsName.startsWith("androidx.")) {
if (debug) Log.d(TAG, "MISS: Reached framework class. Abandoning search.");
return null;
}
try {
//通過類加載器 獲取 ?_viewBinding類 (這個viewBind類是在項目編譯ButterKnife通過APT根據生成的 命名是根據綁定的類名+ViewBinding)
Class<?> bindingClass = cls.getClassLoader().loadClass(clsName + "_ViewBinding");
//noinspection unchecked
//通過反射獲取?_ViewBinding類的構造方法
bindingCtor = (Constructor<? extends Unbinder>) bindingClass.getConstructor(cls, View.class);
if (debug) Log.d(TAG, "HIT: Loaded binding class and constructor.");
} catch (ClassNotFoundException e) {
//如果未發現?_ViewBinding類 則獲取父類傳到該方法裏面繼續尋找_ViewBinding
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);
}
// 把?_ViewBinding的構造函數存儲在LinkedHashMap中
BINDINGS.put(cls, bindingCtor);
// 並返回?_ViewBinding構造函數
return bindingCtor;
}
//butterKnife其他的方法
//設置是否打印日誌
public static void setDebug(boolean debug) {
ButterKnife.debug = debug;
}
——通過綁定的類利用反射得到類的加載方法,根據指定的名字去查找對應的ViewBinding類。
——通過獲取到的ViewBinding類利用反射得到它的一個構造函數,最後通過調用構造函數實例化ViewBinding的一個實現。
通過上面註釋,我們可以看到幾個方法都是通過反射機制實現的。那麼什麼是反射呢? 這裏簡單介紹一下:
反射機制:反射機制允許程序在執行期藉助於Reflection API取得任何類的內部信息,並能直接操作任意對象的內部屬性及方法
優點:可以實現動態創建對象和編譯,體現出很大的靈活性
缺點:對性能有影響,此類操作總是慢於直接執行相同的操作
總結:
- 通過bind()方法傳入一個對象與視圖
- 通過傳入的綁定對象利用反射原理找到對應的viewBinding類並獲取到構造函數
- 構造函數通過反射實例化了ViewBinding類並把綁定的對象與視圖傳到ViewBinding類中
那麼問題來了,綁定對象的ViewBinding類是怎麼來的呢?(如何生成的?)。這裏在簡單介紹一下。
首先得了解這幾個知識
APT(Android註解處理器):APT(Annotation Processing Tool) 即註解處理器,是一種註解處理工具,用來在編譯期掃描和處理註解,通過註解來生成 Java 文件。即以註解作爲橋樑,通過預先規定好的代碼生成規則來自動生成 Java 文件
原理:在註解了某些代碼元素(如字段、函數、類等)後,在編譯時編譯器會檢查 AbstractProcessor 的子類,並且自動調用其 process() 方法,然後將添加了指定註解的所有代碼元素作爲參數傳遞給該方法,開發者再根據註解元素獲取相應的對象信息。根據這些信息通過 javapoet 生成我們所需要的代碼。
通過上面描述總結:
- 當編譯代碼的時候,APThi掃描和處理註解
- 把所有的註解傳遞到AbstractProcessor 子類的process()方法中(也就是我們需要自定義一個類繼承AbstractProcessor 類並重寫process()方法
- 在process()中獲取註解信息並使用javapoet技術生成我們需要的文件
那麼可能有人會問什麼是註解呢:
java註解:又稱 Java 標註,是 JDK5.0 引入的一種註釋機制。Java 語言中的類、方法、變量、參數和包等都可以被標註。和 Javadoc 不同,Java 標註可以通過反射獲取標註內容。在編譯器生成類文件時,標註可以被嵌入到字節碼中。Java 虛擬機可以保留標註內容,在運行時可以獲取到標註內容。
看起來有點抽象:不是很瞭解的這裏給大家在推薦個 註解視頻資源
JavaPoet:JavaPoet是一個用於生成. Java源文件的Java API。
如果大家理解了這幾個知識再來看這篇文章就輕鬆多了,一眼掃過,就會發現都是廢話。作者還挺囉嗦的有木有,覺得是待會點個贊讓作者知道下。
寫太多篇幅太長,看得也累。所以具體如何生成的還是要靠大家去看。
繼續分析生成的ViewBinding類,這裏以MainActivity爲例
3.MainActivity_ViewBinding源碼及註釋如下
//1.ButterKnife類最終執行constructor.newInstance(target, source)通過反射調用了viewBinding構造函數方法,這裏以MainActivity的ViewBinding的源碼進行分析
@UiThread
public MainActivity_ViewBinding(final MainActivity target, View source) {
//把綁定對象賦給viewBinding類的屬性,在解綁的時候釋放資源
this.target = target;
View view;
// 這裏調用了Utils.findRequiredViewAsType()方法來獲取控件
target.tvHellWorld = Utils.findRequiredViewAsType(source, R.id.tv_hell_world, "field 'tvHellWorld'", TextView.class);
target.etInput = Utils.findRequiredViewAsType(source, R.id.et_input, "field 'etInput'", EditText.class);
//通過@OnClick註釋得到id 找到對應的View 並實現點擊事件 並執行MainActivity的onViewClicked方法(onViewClicked是在MainActivity中我們自己定義的名字)
view = source.findViewById(R.id.btn_send);
if (view != null) {
view7f070024 = view;
view.setOnClickListener(new DebouncingOnClickListener() {
@Override
public void doClick(View p0) {
target.onViewClicked(p0);
}
});
}
//獲取資源對象賦值給MainActivity對象
Context context = source.getContext();
Resources res = context.getResources();
target.colorAccent = ContextCompat.getColor(context, R.color.colorAccent);
target.helloWorld = res.getString(R.string.helloWorld);
}
//2.獲取控件的方法
public static <T> T findRequiredViewAsType(View source, @IdRes int id, String who,
Class<T> cls) {
//根據id找到對應的View
View view = findRequiredView(source, id, who);
//把veiw類型強轉成傳進來的cls
return castView(view, id, who, cls);
}
//3.內部通過findViewById 獲取到View
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.");
}
//4.把View轉成對應的Class類型
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);
}
}
// ViewBinding 實現Unbinder接口的unBind方法 在對象銷燬的時候把對應的ViewBinding類裏面的對象進行釋放
@Override
@CallSuper
public void unbind() {
//釋放ViewBinding中的對象
MainActivity target = this.target;
if (target == null) throw new IllegalStateException("Bindings already cleared.");
this.target = null;
target.tvHellWorld = null;
target.etInput = null;
if (view7f070024 != null) {
view7f070024.setOnClickListener(null);
view7f070024 = null;
}
}
上面的代碼都是通過註解獲取來的信息再根據JavaPoet按照butterKnife定的規則生成的。
——通過viewBinding構造函數獲取到控件、資源和點擊事件。賦值給bind()傳遞過來的綁定對象,實現控件/資源/點擊事件的綁定。
——viewBinding類實現Unbinder接口的unbind()方法。
通過源碼發現這裏釋放掉了ViewBinding類的target對象和綁定對象的一些屬性與監聽。Fragment的實現也基本一樣。
那麼這裏有個問題,爲什麼在Fragment中需要進行解綁。而在其他對象中不需要解綁?
ButterKnife文檔中指出:Fragment具有不同於Activity的生命週期。在onCreateView中綁定一個Fragment時,在onDestroyView中將Fragment設置爲null。
可據源碼來看解綁顯然是把Fragment的一些屬性都清空,並且把對應ViewBinding類引用Fragment對象也清空。
如果不進行解綁Fragment ->onDestrory的時候這些就不被回收了嗎?
這也是唯一疑惑的一個地方?也許是我對Fragment生命週期仍存在一些不瞭解地方,很遺憾沒有在其他博客和文檔中找到我想要的答案。所以在這裏提出自己的疑惑。 若有哪位朋友看到,請留言指教一二 。
注:源碼就說到這裏了,看了別人分析10次源碼。不如自己看一次源碼的效果好。這是後面自己看源碼體會最深的。懂得了原理,使用起來就更得心應手了。
總結:
- 項目編譯的時候,ButterKnife使用APT根據註解獲取到的信息採用JavaPoet生成ViewBinding類。
- 我們通過ButterKnife.bind()方法傳入一個對象與視圖
- 通過傳入的綁定對象利用反射原理找到對應的viewBinding類並獲取到構造函數
- 構造函數通過反射實例化了ViewBinding類並把綁定的對象與視圖傳到ViewBinding類中
- 在ViewBinding類的構造函數中分別獲取到控件與資源再賦值給傳遞過來的綁定對象(屬性與對象綁在一起)
(6)輔助插件
這裏推薦一個ButterKnife 輔助插件:Android Butterknife Zelezny (可以自動幫生成所需要綁定的控件)
1.添加插件並重啓(這一步就不演示了)
2.右擊佈局選擇Generate...
3.選擇需要綁定的控件和點擊監聽或修改要生成的控件名
六、總結
放棄:
(1)在Goole的大力支持下 ,越來越多人採用kotlin開發,而kotlin有更簡便的方式替代了findViewById()方法。使得ButterKnife的作用少了一些,但不是完全沒作用。ButterKnife仍可以綁定資源與方法。
(2)DataBinding:使用數據綁定,也可以代替ButterKnife。是不是完全替代就看到家怎麼使用了。
或許還有許多我不知道方式。這裏就舉個例子,也不補充了。
相關鏈接:
七、內容推薦
《Android Rxjava+Retrofit網絡請求框架封裝(一)》
八、項目參考
自己整理的一個工具演示項目,有興趣可以看下
Github:https://github.com/DayorNight/BLCS
apk下載體驗地址:https://www.pgyer.com/BLCS
若您發現文章中存在錯誤或不足的地方,希望您能指出!