在进阶Android的路上,了解理解一个应用根Activity启动流程可以作为一个切入点,由此展开进阶之路。平时我们开发的应用都是展示在Android系统桌面上,这个系统桌面其实也是一个Android应用,它叫Launcher。所以本文通过源码层面从Launcher调用ATMS,ATMS调用ApplicationThread,最后ActivityThread启动Activity三个过程了解Activity启动流程(文中源码基于Android 10 )。
首先来个脑图,对于整体模块在大脑中形成一个整体印象
packages/apps/Launcher3/src/com/android/launcher3/Launcher.java
1 | public boolean startActivitySafely(View v, Intent intent, ItemInfo item, |
packages/apps/Launcher3/src/com/android/launcher3/BaseDraggingActivity.java
1 | public boolean startActivitySafely(View v, Intent intent, @Nullable ItemInfo item, |
frameworks/base/core/java/android/app/Activity.java
1 | @Override |
frameworks/base/core/java/android/app/Activity.java
1 | Activity mParent; |
frameworks/base/core/java/android/app/Instrumentation.java
1 |
|
通过以上源码看到注释1,这里获取了IApplicationThread,如果你了解Binder,第一反应就应该很清晰,目前处于Launcher应用程序进程,要启动Activity则需要请求系统服务进程(SystemServer),而Android进程间通信则可以使用Binder,而这里实现方式为AIDL,它的实现类为ActivityThread的内部类ApplicationThread,而ApplicationThread作用则为应用程序进程和系统服务进程通信的桥梁,后面还会继续提到;接着看到注释2,这里调用ActivityTaskManager.getService则可以获取ActivityTaskManagerService的代理对象,看看他的实现
frameworks/base/core/java/android/app/ActivityTaskManager.java
1
2
3
4
5
6
7
8
9
10
11
12
13 public static IActivityTaskManager getService() {
return IActivityTaskManagerSingleton.get();
}
129726065) (trackingBug =
private static final Singleton<IActivityTaskManager> IActivityTaskManagerSingleton =
new Singleton<IActivityTaskManager>() {
protected IActivityTaskManager create() {
final IBinder b = ServiceManager.getService(Context.ACTIVITY_TASK_SERVICE);//1
return IActivityTaskManager.Stub.asInterface(b); //2
}
};
由以上源码注释1,通过ServiceManager来获取远程服务ActivityTaskManagerService,ServiceManager底层最终调用的还是c++层的ServiceManager,它是Binder的守护服务,通过它能够获取在Android系统启动时注册的系统服务,这其中就包含这里提到的ATMS;接着回到注释2建立 Launcher与 ATMS的连接,这样回到execStartActivity方法,Launcher就通过调用ATMS的startActivity方法将启动Activity的数据交给ATMS服务来处理了。
为了更好理解,看看Launcher调用到ActivityTaskManagerService时序图来对上面的步骤进行回顾
通过上一小节,启动应用程序Activity已经走到ActivityTaskManagerService中,如果你熟悉前以往版本的Android源码,你肯定会知道ActivityManagerService,而在Android 10 中则将AMS用于管理Activity及其容器(任务,堆栈,显示等)的系统服务分离出来放到ATMS中,也许是谷歌不想让AMS的代码越来越膨胀吧(Android 10中AMS代码有一万九千行)。好了,接着看到ATMS的startActivity方法
frameworks/base/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
1
2
3
4
5
6
7
8
public final int startActivity(IApplicationThread caller, String callingPackage,
Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode,
int startFlags, ProfilerInfo profilerInfo, Bundle bOptions) {
return startActivityAsUser(caller, callingPackage, intent, resolvedType, resultTo,
resultWho, requestCode, startFlags, profilerInfo, bOptions,
UserHandle.getCallingUserId());//1
}
由以上代码,继续调用了startActivityAsUser方法,该方法多传入了用户的ID,接着会判断是否有权限调用,没有权限调用则抛出异常,否则获取用户id用于后续进程间Binder通信。接着继续看startActivityAsUser方法
frameworks/base/services/core/java/com/android/server/wm/ActivityTaskManagerService.java
1 | @Override |
frameworks/base/services/core/java/com/android/server/wm/ActivityStarter.java
1 | int execute() { |
frameworks/base/services/core/java/com/android/server/wm/ActivityStarter.java
1 | private int startActivityMayWait(IApplicationThread caller, int callingUid, |
由以上代码,可以看到注释1处创建了一个ActivityRecord数组,ActivityRecord代表一个Activity,接着调用了startActivity方法,
frameworks/base/services/core/java/com/android/server/wm/ActivityStarter.java
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 private int startActivity(IApplicationThread caller, Intent intent, Intent ephemeralIntent,
String resolvedType, ActivityInfo aInfo, ResolveInfo rInfo,
IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
IBinder resultTo, String resultWho, int requestCode, int callingPid, int callingUid,
String callingPackage, int realCallingPid, int realCallingUid, int startFlags,
SafeActivityOptions options,
boolean ignoreTargetSecurity, boolean componentSpecified, ActivityRecord[] outActivity,
TaskRecord inTask, boolean allowPendingRemoteAnimationRegistryLookup,
PendingIntentRecord originatingPendingIntent, boolean allowBackgroundActivityStart) {
mSupervisor.getActivityMetricsLogger().notifyActivityLaunching(intent);
int err = ActivityManager.START_SUCCESS;
// Pull the optional Ephemeral Installer-only bundle out of the options early.
final Bundle verificationBundle
= options != null ? options.popAppVerificationBundle() : null;
WindowProcessController callerApp = null;
if (caller != null) {//1
callerApp = mService.getProcessController(caller);//2
if (callerApp != null) {
callingPid = callerApp.getPid();
callingUid = callerApp.mInfo.uid;
} else {
Slog.w(TAG, "Unable to find app for caller " + caller
+ " (pid=" + callingPid + ") when starting: "
+ intent.toString());
err = ActivityManager.START_PERMISSION_DENIED;
}
}
.......
ActivityRecord r = new ActivityRecord(mService, callerApp, callingPid, callingUid,
callingPackage, intent, resolvedType, aInfo, mService.getGlobalConfiguration(),
resultRecord, resultWho, requestCode, componentSpecified, voiceSession != null,
mSupervisor, checkedOptions, sourceRecord);
if (outActivity != null) {
outActivity[0] = r;//3
}
......
final int res = startActivity(r, sourceRecord, voiceSession, voiceInteractor, startFlags,
true /* doResume */, checkedOptions, inTask, outActivity, restrictedBgActivity);//4
.....
return res;
}
由以上代码,startActivity里面有很多的逻辑代码,这里只看一些重点的逻辑代码,主要做了两个事情:
(1)注释1处判断IApplicationThread是否为空,前面第一小节我们就已经提到过,它代表的就是Launcher进程的ApplicationThread,注释2通过与即将要启动的应用程序进程建立联系,应用程序进程的是fork到Zyote进程,这里先不进行展开了,先专注Activity启动流程。接着注释3创建ActivityRecord代表即将要启动的Activity,包含了Activity的所有信息,并赋值给上一步骤中创建的ActivityRecord类型的outActivity,注释4则继续调用startActivity方法
frameworks/base/services/core/java/com/android/server/wm/ActivityStarter.java
1 | private int startActivity(final ActivityRecord r, ActivityRecord sourceRecord, |
frameworks/base/services/core/java/com/android/server/wm/ActivityStarter.java
1 | // Note: This method should only be called from {@link startActivity}. |
frameworks/base/services/core/java/com/android/server/wm/RootActivityContainer.java
1 | boolean resumeFocusedStacksTopActivities( |
frameworks/base/services/core/java/com/android/server/wm/ActivityStack.java
1 | //确保栈顶 activity 为Resume |
frameworks/base/services/core/java/com/android/server/wm/ActivityStack.java
1 | @GuardedBy("mService") |
frameworks/base/services/core/java/com/android/server/wm/ActivityStackSupervisor.java
1 | void startSpecificActivityLocked(ActivityRecord r, boolean andResume, boolean checkConfig) { |
frameworks/base/services/core/java/com/android/server/wm/ActivityStackSupervisor.java
1 | boolean realStartActivityLocked(ActivityRecord r, WindowProcessController proc, |
frameworks/base/services/core/java/com/android/server/wm/ClientLifecycleManager.java
1 | void scheduleTransaction(ClientTransaction transaction) throws RemoteException { |
到此,基本上已经比较清晰了,注释1处获取了要启动的应用程序进程的IApplicationThread,上一步中创建ClientTransaction对象时已经将其赋值给ClientTransaction的变量mClient,随后scheduleTransaction判断是否支持进程间通信;注释二处则调用了ClientTransaction的schedule方法,
frameworks/base/core/java/android/app/servertransaction/ClientTransaction.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 /** Target client. */
private IApplicationThread mClient;
/** Schedule the transaction after it was initialized. It will be send to client and all its
* individual parts will be applied in the following sequence:
* 1. The client calls {@link #preExecute(ClientTransactionHandler)}, which triggers all work
* that needs to be done before actually scheduling the transaction for callbacks and
* lifecycle state request.
* 2. The transaction message is scheduled.
* 3. The client calls {@link TransactionExecutor#execute(ClientTransaction)}, which executes
* all callbacks and necessary lifecycle transitions.
*/
public void schedule() throws RemoteException {
mClient.scheduleTransaction(this); //1
}
通过以上代码,注释1处mClient则代表要启动的应用程序进程的IApplicationThread,而当前还处于ATMS服务的进程,也就是SystemServer进程,这时ATMS要与即将启动的应用程序进程通信则通过IApplicationThread来执行AIDL,IApplicationThread实现为ApplicationThread,它是ActivityThread的内部类,所以前面也说过ApplicationThread为进程间通信的桥梁,注释1处则相当于是IApplicationThread.scheduleTransaction,并将包含要启动Activity信息的ClientTransaction传递到了应用程序进程,下一节就从IApplicationThread讲起。
为了更好理解,看看AMTS调用到ApplicationThread时序图来对上面的步骤进行回顾
frameworks/base/core/java/android/app/ActivityThread.java
1 | private class ApplicationThread extends IApplicationThread.Stub { |
frameworks/base/core/java/android/app/ClientTransactionHandler.java
1 | /** Prepare and schedule transaction for execution. */ |
1 | class H extends Handler { |
frameworks/base/core/java/android/app/servertransaction/TransactionExecutor.java
1 | public void execute(ClientTransaction transaction) { |
frameworks/base/core/java/android/app/servertransaction/TransactionExecutor.java
1 | /** Cycle through all states requested by callbacks and execute them at proper times. */ |
frameworks/base/core/java/android/app/servertransaction/LaunchActivityItem.java
1 | @Override |
frameworks/base/core/java/android/app/ActivityThread.java
1 | /** |
frameworks/base/core/java/android/app/ActivityThread.java
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 /** Core implementation of activity launch. */
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
ActivityInfo aInfo = r.activityInfo;//1
if (r.packageInfo == null) {
r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo,
Context.CONTEXT_INCLUDE_CODE);//2
}
ComponentName component = r.intent.getComponent();//3
if (component == null) {
component = r.intent.resolveActivity(
mInitialApplication.getPackageManager());
r.intent.setComponent(component);
}
if (r.activityInfo.targetActivity != null) {
component = new ComponentName(r.activityInfo.packageName,
r.activityInfo.targetActivity);
}
//应用程序Context的创建
ContextImpl appContext = createBaseContextForActivity(r);//4
Activity activity = null;
try {
java.lang.ClassLoader cl = appContext.getClassLoader();
//创建Activity的实例
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);//5
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
r.intent.prepareToEnterProcess();
if (r.state != null) {
r.state.setClassLoader(cl);
}
} catch (Exception e) {
if (!mInstrumentation.onException(activity, e)) {
throw new RuntimeException(
"Unable to instantiate activity " + component
+ ": " + e.toString(), e);
}
}
try {
//应用程序Application的创建
Application app = r.packageInfo.makeApplication(false, mInstrumentation);//6
......
if (activity != null) {
CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
Configuration config = new Configuration(mCompatConfiguration);
if (r.overrideConfig != null) {
config.updateFrom(r.overrideConfig);
}
if (DEBUG_CONFIGURATION) Slog.v(TAG, "Launching activity "
+ r.activityInfo.name + " with config " + config);
Window window = null;
if (r.mPendingRemoveWindow != null && r.mPreserveWindow) {
window = r.mPendingRemoveWindow;
r.mPendingRemoveWindow = null;
r.mPendingRemoveWindowManager = null;
}
appContext.setOuterContext(activity);
// 通过Activity的 attach 方法将 context等各种数据与Activity绑定,初始化Activity
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window, r.configCallback,
r.assistToken); //7
......
if (r.isPersistable()) {
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);//8
} else {
mInstrumentation.callActivityOnCreate(activity, r.state);
}
......
r.activity = activity;
}
r.setState(ON_CREATE);
.......
}
return activity;
}
frameworks/base/core/java/android/app/Instrumentation.java
1 | public void callActivityOnCreate(Activity activity, Bundle icicle,PersistableBundle persistentState) { |
frameworks/base/core/java/android/app/Activity.java
1 | final void performCreate(Bundle icicle, PersistableBundle persistentState) { |
1 | Doctor summary (to see all details, run flutter doctor -v): |
类型 | 二维码 |
---|---|
Apk 下载二维码 | |
ios 下载 | 暂无下载,可以自行clone项目编译体验 |
点击头像进入个人中心,仿B站个人中心效果
第三方库 | 功能 |
---|---|
fluro | 页面跳转路由框架 |
shared_preferences | 本地存储 |
dio | 网络 |
json_annotation | json 序列化 |
flutter_webview | webview |
fluttertoast | Toast |
provider | 跨组件数据共享 |
event_bus | 事件总线 |
flutter_spinkit | 加载中指示器动画 |
extended_nested_scroll_view | NestedScrollView 扩展 |
flutter_easyrefresh | 配合NestedScrollView扩展下拉刷新以及上拉加载 |
flutter_staggered_grid_view | 瀑布流 |
package_info | 方便获取应用信息 |
flutter_html | 加载html 字符串 |
expandable | 扩展显示隐藏 |
date_format | 日期转换 |
share | 分享 |
项目中的 API 均来自于 wanandroid.com 网站,纯属学习交流使用,不得用于商业用途。
]]>谈起Android 消息机制,相信各位会首先想到Handler,Handler是Android 提供给给开发者实现线程间通信的工具。Android的消息机制包含四大内容,ThreadLocal保证每个线程都有自己的消息轮询器Looper,MessageQueue用来存放消息,Looper负责取消息,最后Handler负责消息的发送与消息的处理。
1 | /**Handler 构造方法*/ |
ThreadLocal是一个线程内部数据存储类,但存放数据并不是它实现的,它只是帮助类,真正存放数据的是ThreadLocalMap。
先看一个简单的例子
1 | public class Test { |
上面例子当中,两个线程访问的都是一个ThreadLocal对象,但是第二个线程没有设置初始值,则获取为null,也就可以说明每个线程操作的是自己对应的一份数据,虽然都是从ThreadLocal的get方法获取,但是get方法则是获取对应线程的ThreadLocal.ThreadLocalMap来获取值。
1 | /** |
1 | /** |
Thread类有一个类型为ThreadLocal.ThreadLocalMap的成员变量threadLocals,如果你了解Java内存模型,threadLocals的值都是new出来的话,很容易明白threadLocals是存放在堆内存中的,而每一个线程只是在堆内存中存放了自己的threadLocals,也就是每个线程本地内存(逻辑上),物理上本地内存只是在堆内存中占有一块区域,每个线程只玩自己对应的threadLocals,各个线程的对应ThreadLocal互不干扰,这也就实现了各个线程间数据的隔离,也就是每个Handler所在线程都有其对应的Looper对象。
Thread类中 threadLocals 声明
1 | /* ThreadLocal values pertaining to this thread. This map is maintained |
简单来说就是数据复制很多份存放在堆内存,各个线程获取自己对应的那份数据。
这个可以举一个共享汽车的例子,假如刚开始共享汽车试运行,大街上只有一辆,大家都抢着去开,这就会出现问题,而后来发展普及,每辆车复制迅速生产,满大街都是共享汽车,每个人都可以通过专属二维码开对应共享汽车,这里开车人就对应线程,大家互不干扰,共享汽车就对应ThreadLocals,而大街就相当于堆内存。
1 | /** Handler的sendMessageAtTime方法*/ |
1 | /** MessageQueue的enqueueMessage方法*/ |
1 | Message next() { |
1 | private native static void nativeWake(long ptr); |
到此,next方法的逻辑就很清晰了,开始nextPollTimeoutMillis的值是等于零的,获取消息过程就不会受到nativePollOnce方法的阻塞,然后判断取出的消息是否延时,有延时则计算nextPollTimeoutMillis进入下一循环进入nativePollOnce方法阻塞,否则返回取出的消息,有阻塞肯定就有唤醒,这个唤醒的方法就是nativeWake(long ptr)方法,它的实现也在native层,它的调用在我们前面分析enqueueMessage方法逻辑有出现,当有消息进入消息队列,如果当前线程正在被阻塞,调用nativeWake方法,nativePollOnce就会立即返回,取消阻塞,这样循环取到没有延时的消息,则直接返回消息;如果没有消息,nextPollTimeoutMillis等于 -1,继续阻塞状态。
经过前面的分析,消息插入链表是sendMessageAtTime方法触发的,而接下来就会有一个疑问,那又是谁调用 next() 方法取消息呢?没错,就是接下来要了解的Looper
1 | /* This is a typical example of the implementation of a Looper thread, |
1 | public static void prepare() { |
1 | /** |
1 | /**ActivityThread 的 main 方法*/ |
1 | /** |
基础页面实现
1 | List <String>_titles=['湖人','勇士','雄鹿','快船','凯尔特人','马刺','76人','猛龙']; |
1 | const RefreshIndicator({ |
1 | @override |
1 | ///根据配置状态返回实际列表数量 |
1 | ///根据配置状态返回实际列表渲染Item |
该方法中,如果没有设置头部,并且数据不为0,当index等于数据长度时,渲染加载更多页面(因为index是从0开始);如果设置了头部页面,并且数据不为0,当index等于实际渲染长度 - 1时,渲染加载更多页面(在该方法判断是否已经加载到底);接着如果设置了头部widget,并且数据不为0,当index = 0 ,渲染头部widget;如果没设置头部,并且数据为0,如果当前正在刷新,渲染Loading页面,否则渲染空页面或者Error页面;同理,如果设置头部,并且数据为0,并且当前正在刷新,渲染Loading页面,否则渲染空页面或者Error页面;如果不是上面情况,则渲染正常渲染Item,如果这里有需要,可以直接返回相对位置的index,如果有头部 index 减一 ,保持不会忽略 index = 0 的数据。
接着封装一个统一网络请求方法,外部请求安装固定格式的 Map 将数据返回给下拉刷新上拉加载更多widget,达到通用的目的。
1 | //网络请求获取数据 isRefresh 是否为下拉刷新 |
1 | Widget _buildIsLoading() { |
1 | // 模块item |
Flutter 中Widget 多种多样,有UI的,当然也有功能型的组件InheritedWidget 组件就是Flutter 中的一个功能组件,它可以实现Flutter 组件之间的数据共享,他的数据传递方向在Widget树传递是从上到下的。
1 | /// Created with Android Studio. |
1 | /// Created with Android Studio. |
1 | /// Created with Android Studio. |
代码很简单,创建一个按钮,每点击一次,就将ShareDataWidget的data值加一,而前面创建的TestShareDataWidget中依赖了ShareDataWidget的data值,如果数据共享则它的值就会跟随变化。
运行效果
1 | I/flutter ( 7082): didChangeDependencies |
1 | // 子树中的widget获取共享数据 方法 |
1 | /** |
1 | /** |
1 | /// Created with Android Studio. |
1 | /// Created with Android Studio. |
1 | class _ChangeNotifierProviderState<T extends ChangeNotifier> extends State<ChangeNotifierProvider<T>>{ |
1 | /// Created with Android Studio. |
上一节中手写了一个非常简单基于InheritedWidget的Provider数据共享组件,接下来通过一个切换主题的例子来使用刚刚写好的ChangeNotifierProvider。
主题切换这里简单的改变主题颜色,所以共享数据就是颜色值,Demo 思路为使用Dialog,提供可选择的主题颜色,然后点击对应颜色则切换应用主题颜色,接下来一起实现。
1 | /// Created with Android Studio. |
1 | class _MyHomePageState extends State<MyHomePage> { |
1 | class SingleThemeColor extends StatelessWidget { |
1 | bash, mkdir, rm, git, curl, unzip, which |
flutter官网下载其最新可用的安装包
安装包下载完成则可以进行解压
1 | unzip /指定解压目录/flutter_macos_v1.9.1+hotfix.4-stable.zip |
1 | # 使用pwd 命令查看目录路径 |
设置环境变量目的是以便我们可以运行flutter命令在任何终端会话中
确定Flutter SDK的目录,上一步我们解压获取了flutter的路径/Users/XXXXX/development/flutter
打开(或创建) $HOME/.bash_profile. 文件路径和文件名可能在您的机器上不同(注意$HOME 指的是 路径是 /Users/用户名XX/ )
1 | vim $HOME/.bash_profile |
1 | export PUB_HOSTED_URL=https://pub.flutter-io.cn //国内用户需要设置 |
1 | source $HOME/.bash_profile |
1 | flutter doctor |
1 | flutter upgrade |
1 | var name = 'maoqitian'; |
1 | final List _suggestions = new List<WordPair>(); |
1 | //如下定义一个字体大小的值一直都是 18 ,不会改变 |
1 | //定义一个返回 bool(布尔)类型的方法 |
1 | //调用有可选命名参数方法 playGames |
1 | // 定义可选位置参数方法 |
1 | // 定义可选位置参数方法 |
1 | // Android studio 创建Demo 项目 main.dart 文件开头 |
1 | static Future<ArticleListData> getArticleData(int pageNum) async{ |
先了解这么多,更多Dart 相关内容可以查看Dart语言官网
在开始Flutter Hello world程序之前,作为一名Android 开发者,首先我们要认识到Flutter中没有原生开发的XML,所有界面和逻辑代码都在.dart文件中,Flutter给我提供了一套视觉、结构、平台、和交互式的Widgets,所以在Flutter中一构架的一切界面都是Widgets。接下来我们先看一个简单的Hello World Flutter应用。
Android Studio 新建Flutter demo
1 | import 'package:flutter/material.dart'; |
上个小例子中我们提到无状态 StatelessWidget,想必也能猜到,肯定会有一个有状态的widget,这个widget就是StatefulWidget,该widget为何说是有状态的呢,主要是在其管理的State中我们可以调用setState来动态改变页面显示。接着我们看一个显示数据列表的例子,并加入一个可以点击收藏的按钮。
1 | import 'package:flutter/material.dart'; |
运行效果
如上代码,将原来的无状态Widget改成了StatefulWidget,并在build中构建ListView,到此你可能有疑惑,不是说有状态的Widget,怎么还是创建Widget,有状态如何体现呢? 别急,我们看到_buildRow方法,方法中构建了ListTile 这个Widget,它响应点击事件回到为onTap方法,也就是当我们点击ListTile,我们在onTap方法中就可以调用setState方法来动态改变页面显示,也就是改变桃心收藏按钮变化(注意setState方法需要在State类中才能调用)。
State 是有周期的,其中包括三个函数:
布局名称 | 特点描述 |
---|---|
Container | 拥有单个子元素的布局widget,可以灵活设置 |
Padding | 拥有单个子元素,给其子widget添加指定的填充 |
Center | 将其子widget居中显示 |
Align | 将其子widget对齐,并可以根据子widget的大小自动调整大小。 |
Row | 可以拥有多个子元素,在水平方向上排列子widget的列表,和原生控件 LinerLayout orientation=”horizontal” 类似 |
Column | 可以拥有多个子元素,在竖直方向上排列子widget的列表,和原生控件 LinerLayout orientation=”vertical” 类似 |
Stack | 可以拥有多个子元素,允许其子widget简单的堆叠在一起 |
Flow | 实现流式布局算法的widget |
ListView | 可滚动的列表控件 |
Widget名称 | 特点描述 |
---|---|
MaterialApp | 封装了应用程序实现Material Design所需要的一些widget,由前面demo可以发现它一般为应用顶层入口widget |
Scaffold | Material Design布局结构的基本实现。此类提供了用于显示drawer、snackbar和底部sheet的API。 |
Appbar | 一般和Scaffold结合使用,可以设置页面标题和各种按钮等(Toolbar) |
BottomNavigationBar | 底部导航条,可以很容易地在tap之间切换和浏览顶级视图 |
Drawer | 和Scaffold结合使用,从Scaffold边缘水平滑动以显示应用程序中导航链接的Material Design面板 |
RaisedButton | Material Design中的button,响应点击事件(button) |
IconButton | 一个Material图标按钮,可以设置icon,点击时会有水波动画 |
TextField | 文本输入框 (EditText) |
image | 显示图片的widget(ImageView) |
Text | 单一格式的文本 (TextView) |
1 | import 'package:flutter/material.dart'; |
之前的文章中,我们使用docker run 命令来启动一个容器,而作为真正的线上业务环境,我们服务肯定不止一个,也就说明容器肯定不止一个,而如果还是手动的一个个来启动容器这未免会让人头皮发麻,幸好有Docker Compose,用于定义和运行多容器Docker应用程序的工具,有了它我们可以一次启动多个容器,这也非常适合与持续集成工具(Jenkins)来配合。
1 | sudo curl -L "https://github.com/docker/compose/releases/download/1.24.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose |
1 | chmod +x /usr/local/bin/docker-compose |
1 | sudo ln -s local docker-compose bin/docker-compose |
1 | # docker-compose --version |
1 | sudo curl -L https://raw.githubusercontent.com/docker/compose/1.24.0/contrib/completion/bash/docker-compose -o /etc/bash_completion.d/docker-compose |
命令 | 含义 | 示例 |
---|---|---|
build | 构建或重新构建服务 | build [options] [–build-arg key=val…] [SERVICE…](使用 docker-compose help build 查看详细使用) |
help | 查看docker-compose命令帮助文档 | docker-compose help COMMAND(标识要看的命令) |
up | 构建、创建、重新创建、启动,连接服务的相关容器。所有连接的服务都会启动,除非它们已经运行(前提该目录下已经存在docker-compose.yml文件) | docker-compose up(直接启动,该命令退出,所有容器停止) docker-compose up -d (后台运行所有容器) |
kill | 发送SIGKILL 信号停止指定服务的容器 | docker-compose kill api-feign(注意该名称为docker-compose.yml中定义的服务名称) |
start | 启动指定服务已存在的容器 | docker-compose start api-feign |
stop | 停止指定服务已存在的容器 | docker-compose stop api-feign |
logs | 查看服务的日志输出 | docker-compose logs –tail=”all” api-feign(查看api-feign 全部日志输出) |
ps | 列出所有容器 | docker-compose ps (和docker ps -a 一样可以查看容器,显示信息不一样) |
rm | 删除指定服务的容器 | docker-compose rm api-feign |
服务器任意目录编写文件 docker-compose.yml
1 | version: '3.4' |
在docker-compose.yml目录下执行命令启动多个服务(安装好Docker Compose前提下)
1 |
|
首先我们可以将脚本打包到我们的镜像中,修改Dockerfile文件
1 | #Dockerfile中 |
再次使用docker-maven-plugin打包镜像
1 | version: '3.7' |
1 | docker-compose up -d |
Docker 镜像构建一般使用Dockerfile,首先我们需要了解Dockerfile语法(Dockerfile官方文档),然后我们编写好Dockerfile文件之后就可以开始构建我们的项目对应Docker镜像,如果构建呢?我们可以手动使用docker命令构建,也可以使用开源插件帮助构建,请往下看。
1 | mvn install |
将jar包复制到服务器我们想要的位置,使用docker命令构建docker镜像
1 | # 格式:docker build -t 标签名称 Dockerfile的相对位置 |
Dockerfile 内容
1 | # 基于哪个镜像 |
启动构建好的镜像
1 | docker run -p 8666:8666 configserver |
在/usr/lib/systemd/system/docker.service,配置远程访问。主要是在[Service]这个部分,加上下面两个参数
1 | # vim /usr/lib/systemd/system/docker.service |
以上配置完成之后我们需要重新读取docker配置文件,重新启动docker服务
1 | systemctl daemon-reload |
看进程docker是否已经监听2375端口,命令为 ps aux | grep docker
1 | root 8902 0.5 0.1 637948 37388 ? Ssl 10:34 1:30 /usr/bin/dockerd -H tcp://0.0.0.0:2375 -H unix://var/run/docker.sock |
此时docker已经开启远程访问地址为 服务器地址:2375(别忘了服务器防火墙需要开放2375端口)
配置POM build 模块,使用docker 镜像私服(nexus)作为镜像仓库
1 | <build> |
创建并上传镜像执行
1 | mvn clean package docker:build -DpushImage |
1 | <build> |
1 | mvn clean package docker:build -DpushImageTag |
平常打包构建命令为mvn clean package docker:build,而对应maven的命令格式为mvn phase:goal,所以打包构建命令中package docker对应为phase,build则对应goal,这样根据官方文档提示将POM改造得出
1 | <build> |
如此我们只要在项目根目录使用命令,则项目就会自动打包构建镜像并上传到nexus私服
1 | ## -DskipTests 表示 跳过Test |
1 |
|
1 | docker login xxx.xxx.xxx.xxx:9290 |
上一篇文章我们了解如何在CentOs安装Docker,接下来我们学习Docker 命令
命令 | 解释 |
---|---|
docker images 或者 docker image ls | 列表本地所有镜像 |
docker search 关键词 | 在Docker Hub中搜索镜像 |
docker pull 镜像名称 | 下载Docker镜像 |
docker rmi 镜像id | 删除Docker镜像。加参数-f表示强制删除。 |
docker run 镜像名称称 | 下载Docker镜像 |
docker build -t 标签名称 目录 | 构建Docker镜像,-t 表示指定一个标签 |
docker tag | 为镜像打标签 |
命令 | 解释 |
---|---|
docker ps | 列表所有运行中的Docker容器(包括已停止的容器)。该命令参数比较多,-a:列表所有容器;-f:过滤;-q 只列表容器的id。 |
docker version | 查看docker 版本信息 |
docker –version | 查看docker 版本 |
docker info | 查看Docker系统信息,例如:CPU、内存、容器个数等等 |
docker kill 容器id | 杀死id对应容器 |
docker start / stop / restart 容器id | 启动、停止、重启指定容器 |
1 | # 搜索镜像 nginx |
1 | #下载镜像 |
docker images 或者 docker image ls
1 | REPOSITORY TAG IMAGE ID CREATED SIZE |
REPOSITORY:镜像所属仓库名称。
TAG:镜像标签。默认是latest,表示最新。
IMAGE ID:镜像ID,表示镜像唯一标识。
CREATED:镜像创建时间。
SIZE:镜像大小。
1 |
|
选项 | 含义 |
---|---|
-d | 表示后台运行2 |
-P | 随机端口映射(指定端口映射) |
1 | # 指定端口映射,有以下四种格式。 |
1 |
|
docker ps -a
1 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES |
CONTAINER_ID:表示容器ID。
IMAGE:表示镜像名称。
COMMAND:表示启动容器时运行的命令。
CREATED:表示容器的创建时间。
STATUS:表示容器运行的状态。Up表示运行中,Exited表示已停止。
PORTS:表示容器对外的端口号。
NAMES:表示容器名称。该名称默认由Docker自动生成,也可使用docker run命令的–name选项自行指定。
1 |
|
使用nsenter工具进入容器(nsenter工具包含在util-linux 2.23或更高版本中)
1 | # 找到容器第一个进程的PID,可通过以下命令获取 |
实战例子
1 | [root@gxst_docker_76_16 ~] |
1 | root@ded3613de77d:/# exit |
1 | # 新建脚本文件 |
脚本代码
1 |
|
使用脚本进入容器
1 | sh docker-enter.sh ded3613de77d(容器名称或者ID) |
首先我们了解什么是Docker
首先查看系统自带java,并卸载
1 | # 如果有结果出来,则说明自带了java |
安装JDK
1 | cd /usr |
1 | #编辑/etc/profile文件 |
1 | java -version |
下载 maven 3.6.1
1 | # 执行以下命令 |
测试
1 | mvn -v |
1 | <localRepository> maven/repo</localRepository> |
1 | <!--私服账号配置--> |
1 | 安装依赖 |
1 | yum remove docker \ |
1 | # 安裝所需的包。 yum-utils提供yum-config-manager實用程序,devicemapper存儲驅動程序需要device-mapper-persistent-data和lvm2。 |
1 | # 安装wget 网络工具 |
1 | yum install docker-ce docker-ce-cli containerd.io |
1 | yum list docker-ce --showduplicates | sort -r |
1 | # 官方方法 |
1 | # 直接安装 |
1 | systemctl start docker |
1 | docker pull library/hello-world |
1 | docker run hello-world |
1 | Hello from Docker! |
平时撸代码避免不了在有些功能会使用到别人已经写好的轮子,别人的轮子开源库一般都已经上传了 jcenter仓库,我只需要比如 implementation ‘com.mao:xxxxxxx:1.0.0’一句话就能引入别人的开源库,这是怎么弄的呢?一般可以使用bintray-release插件和gradle-bintray-plugin插件,gradle-bintray-plugin插件不够简便(想了解可以看这篇文章https://www.cnblogs.com/mingfeng002/p/10255486.html),所以接下来我们就了解一下如何使用bintray-release插件将自己的开源库上传到jcenter。
1 | buildscript { |
1 | apply plugin: 'com.android.library' |
1 | gradlew clean build bintrayUpload //根命令 |
1 | # window 下执行 |
1 | publish { |
开发一个App,和起房子应该有异曲同工之处,起房子需要画好设计图纸,而我们开发App则需要先设计好App整个架构模式。目前Android一般有MVC、MVP和MVVM,本文则先来说说MVP架构。在了解MVP架构之前,有人可能会说,MVP架构是不是有点落后了,但是我想说,如果你公司有老项目,他就是用MVP架构写的,这时候我们MVP知识是不是就派上用场了;任何架构都有它存在的理由,学习架构的思想才是关键。MVP分别代表Model、View、Presenter三个英文字母,和传统的MVC 相比,C替换成了P。Presenter英文单词有主持人意思,也就是说Presenter是View 和 Model 的主持人,按照惯例我们先来看两张图。
1 | /** |
1 | /** |
1 | /** |
1 | /** |
1 | /** |
1 | public interface MainContract { |
1 | /** |
1 | dependencies { |
1 | public class MainPresenter extends RxBasePresenter<MainContract.MainView> implements MainContract.MainActivityPresenter { |
1 | /** |
1 | /** |
1 | /** |
1 | @Module |
1 | @Singleton |
按照如下改造MyApplication之后我们从新编译编译一下代码,如果编译通过,dagger就会帮我们生成对应DaggerAppComponent.create()方法,将其返回在applicationInjector()方法中。
1 | public class MyApplication extends DaggerApplication { |
项目编译通过dagger会在build目录生成对应对象注入类,具体源码以后再出文章分析,这里就先告一段落了。到此,使用dagger2优化MVP 架构基本完成了,但是还有其他细节这里没有提及,比如每个Presenter之间该如何通信,可以使用EventBus,也可以Rxbus等等,具体细节可以看接下架构实践中我写的开源项目的代码。
第三方库 | 功能 |
---|---|
Dagger2 | 依赖注入 |
Retrofit2 | 网络 |
OKHttp3 | 网络 |
RxJava2 | 异步事件处理 |
greenDAO | 数据库 |
SmartRefreshLayout | 下拉刷新 |
Glide4 | 图片加载 |
Android-ConvenientBanner | Banner |
BaseRecyclerViewAdapterHelper | 数据适配器帮助类 |
butterknife | 控件绑定 |
FlowLayout | tag 显示 |
verticalTabLayout | 纵向导航 |
项目中的 API 均来自于 wanandroid.com 网站,纯属学习交流使用,不得用于商业用途。
Copyright 2019 maoqitian
Licensed under the Apache License, Version 2.0 (the “License”);
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an “AS IS” BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
kubernetes来源于希腊语,意为舵手或领航员,从k8s的logo也能看出这个船舵图标与其名称对应。而我们常说的k8s中的8代表的就是ubernete这个八个字符。这里引用k8s中文社区文档对k8s的描述:Kubernetes是一个开源的,用于管理云平台中多个主机上的容器化的应用,Kubernetes的目标是让部署容器化的应用简单并且高效(powerful),Kubernetes提供了应用部署,规划,更新,维护的一种机制。
IP地址 | 角色 | 服务器系统 |
---|---|---|
172.31.76.16 | k8s从节点 | CentOS 7.6 |
172.31.76.17 | k8s从节点 | CentOS 7.6 |
172.31.76.18 | k8s主节点 | CentOS 7.6 |
软件名称 | 版本 | 作用 |
---|---|---|
Docker | 18.09.6 | 容器 |
Kubernetes | 1.14.2 | 管理容器 |
组件名称 | 版本 | 作用 |
---|---|---|
kubeadm | 1.14.2-0 | 初始化k8s集群工具 |
kubectl | 1.14.2-0 | k8s命令行工具,命令控制部署管理应用,CRUD各种资源 |
kubelet | 1.14.2-0 | 运行于所有节点上,负责启动容器和 Pod |
1 | # 主节点主机名对应 172.31.76.18 |
1 | # 使用hostnamectl命令 显示信息 |
1 | # 编辑每台机器的 /etc/hosts文件,写入下面内容 |
1 |
|
1 | setenforce 0 ##设置SELinux 成为permissive模式 (不用重启机器) |
1 | swapoff -a |
安装这几个组件前先准备repo
1 | cat <<EOF > /etc/yum.repos.d/kubernetes.repo |
接着直接安装 kubeadm、kubectl、kubelet这个三个组件
1 | yum install -y kubelet kubeadm kubectl |
1 | systemctl enable kubelet && systemctl start kubelet |
1 | docker pull mirrorgooglecontainers/kube-apiserver:v1 |
1 | docker tag mirrorgooglecontainers/kube-apiserver:v1 k8s.gcr.io/kube-apiserver:v1 |
1 | docker rmi mirrorgooglecontainers/kube-apiserver:v1 |
输入以下命令开始安装kubernetes
1 | # --kubernetes-version=v1 指定安装的k8s版本 |
如下为kubernetes初始化日志打印
1 | [init] Using Kubernetes version: v1.14.2 |
注意: 看到上面Kubernetes初始化信息,我们需要注意最后一句话,等会我们子节点加入Kubernetes集群就是使用这一句话
1 | kubeadm join 172.31.76.18:6443 --token y6awgp.6bvxt8l3rie2du5s \ |
1 | # root 模式下导入环境变量 |
1 | sysctl net.bridge.bridge-nf-call-iptables=1 |
然后在k8s-master节点上执行kube-flannel.yaml配置,也可根据官方文档来操作下载kube-flannel.yaml文件,下文也给出kube-flannel.yaml文件内容
1 | kubectl apply -f kube-flannel.yaml |
kube-flannel.yaml 文件
1 |
|
查看Kubernetes的Pod 是否正常运行
1 | kubectl get pods --all-namespaces -o wide |
1 | kubectl get nodes |
1 | mkdir -p $HOME/.kube |
1 | # 基础命令示例 kubeadm join --token <token> <master-ip>:<master-port> --discovery-token-ca-cert-hash sha256:<hash> |
我们可以在另一台node节点机器再次重复该操作
查看刚刚加入集群的子节点
1 | # 查询k8s集群所以节点 |
1 | # 不论主节点 还是 子节点该命令都能重置节点 |
1 | # 拉取国内镜像 |
1 |
|
1 | #使用如下命令 |
1 | # 使用如下地址格式访问 |
1 | { |
1 | grep 'client-certificate-data' /etc/kubernetes/admin.conf | head -n 1 | awk '{print $2}' | base64 -d >> kubecfg.crt |
1 | grep 'client-key-data' /etc/kubernetes/admin.conf | head -n 1 | awk '{print $2}' | base64 -d >> kubecfg.key |
1 | openssl pkcs12 -export -clcerts -inkey kubecfg.key -in kubecfg.crt -out kubecfg.p12 -name "kubernetes-client" |
1 | https://172.31.76.18:6443/api/v1/namespaces/kube-system/services/https:kubernetes-dashboard:/proxy/ |
这里我们使用token认证,使用token认证前先创建dashboard用户,
1 | cat <<EOF | kubectl create -f - |
创建ClusterRoleBinding
1 | cat <<EOF | kubectl create -f - |
1 | kubectl -n kube-system describe secret $(kubectl -n kube-system get secret | grep admin-user | awk '{print $1}') |
1 | kubectl delete -f kubernetes-dashboard.yaml |
1 | mkdir -p $HOME/.kube |
1 | # 创建新token |
1 | [kubelet-check] It seems like the kubelet isn't running or healthy. |
1 | sudo swapoff -a |
1 | ## 启动 docker 服务 |
1 |
|
查看有问题服务的日志1
2
3kubectl --namespace kube-system logs kube-flannel-ds-amd64-g997s
错误日志:Error from server: Get https://172.31.76.17:10250/containerLogs/kube-system/kube-flannel-ds-amd64-g997s/kube-flannel: dial tcp 172.31.76.17:10250: connect: no route to host
1 | error execution phase preflight: [preflight] Some fatal errors occurred: |
1 | # 执行以下命令 |
Maven的原理就是将jar从远程中央仓库下载到PC磁盘的本地仓库,当本地仓库没有发现需要的jar就会去Maven默认的远程中央仓库Maven Central(由Apache维护)中寻找,每次需要新的jar后都要从远程中央仓库上下载。那么问题来了?这个远程的中央仓库一定有很多人使用那下载速度一定很慢,这个暂且不用考虑。 重要的是万一哪天公司外网连不上了咋办?而Nexus私服恰好可以解决这个问题。搭建私服的好处是Nexus有效解决了Maven对Apache的远程中央仓库的依赖,当项目需要新的jar时会先在nexus私服下载好以后才会下载到本地。如果发现私服已经存在这个jar包,则会直接从私服下载到本地Maven库,如果没有再去网络上下载。同时,我们也可打包自己的代码变成jar包上传到私服中供公司其他同事下载使用。
解压
1 | tar -zvxf nexus-3.13.0-01-unix.tar.gz -C /opt/ |
环境变量配置
1 | vim /opt/nexus-3.13.0-01/bin/nexus |
启动Nexus
1 | /opt/nexus-3.13.0-01/bin/nexus start |
1 | //加入9190端口的监听 |
1 | <repositories> |
1 | 1.group(仓库组类型):又叫组仓库,用于方便开发人员自己设定的仓库; |
Hosted 有三种方式,Releases、SNAPSHOT、Mixed
]]>到此,Nexus搭建Maven私服服务已经完成.
在日常开发的过程中,对于代码版本的控制已经是是一个习以为常的功能了,接下就记录一下使用Git来作为版本控制的一些常用操作命名,方便自己查看回顾。
git 提交流程
1 | git init (初始化本地仓库) |
master 分支链接成功之后,拉取分支代码
1 | git fetch origin wiki 把远程分支拉到本地 |
1 | git config user.name |
1 | git config --global user.name "your name" |
创建新分支
1 | git checkout -b dev |
1 | git checkout master |
1 | git merge dev |
1 | #拉取 |
清除对应文件夹的提交记录缓存
1 | git rm --cached --force -r gxxmt-admin/target/ |
清除所有文件夹的缓存记录
1 | git rm -r --cached . |
清除完成之后重复 add 和 commit操作则能使用新的忽略规则
删除GitLab 上的文件,首先克隆代码
1 | git rm -r --cached target |
1 | git remote -v(查看链接库情况) |
1 | git clone git@172.31.116.11:maoqitian/gxxmt.git |
1 | //查看设置的邮箱 |
git tag 打标签(漫长版本迭代中比较重要)
正常提交一个版本流程
1 | git checkout master (切换主分支) |
对以前提交记录加入 tag
1 | git log --pretty=oneline (显示提交历史) |
1 | git tag (查看tag ) |
关于注解首先引入官方文档的一句话:Java 注解用于为 Java 代码提供元数据。作为元数据,注解不直接影响你的代码执行,但也有一些类型的注解实际上可以用于这一目的。Java 注解是从 Java5 开始添加到 Java 的。看完这句话也许你还是一脸懵逼,接下我将从注解的定义、元注解、注解属性、自定义注解、注解解析JDK 提供的注解这几个方面再次了解注解(Annotation)
我们新建一个注解MyTestAnnotation
1 | public @interface MyTestAnnotation { |
接着我们就可以在类或者方法上作用我们刚刚新建的注解
1 |
|
以上我们只是了解了注解的写法,但是我们定义的注解中还没写任何代码,现在这个注解毫无意义,要如何使注解工作呢?接下来我们接着了解元注解。
1 | @Retention(RetentionPolicy.RUNTIME) |
1 | @Retention(RetentionPolicy.RUNTIME) |
1 | /**自定义注解*/ |
1 | /**一个人喜欢玩游戏,他喜欢玩英雄联盟,绝地求生,极品飞车,尘埃4等,则我们需要定义一个人的注解,他属性代表喜欢玩游戏集合,一个游戏注解,游戏属性代表游戏名称*/ |
1 | /**注解Repeatable源码*/ |
1 | /**Annotation接口源码*/ |
1 |
|
如果获取注解属性,当然是反射啦,主要有三个基本的方法
1 | /**是否存在对应 Annotation 对象*/ |
下面结合前面的例子,我们来获取一下注解属性,在获取之前我们自定义的注解必须使用元注解@Retention(RetentionPolicy.RUNTIME)
1 | public class test { |
运行结果:
注解 | 作用 | 注意事项 |
---|---|---|
@Override | 它是用来描述当前方法是一个重写的方法,在编译阶段对方法进行检查 | jdk1.5中它只能描述继承中的重写,jdk1.6中它可以描述接口实现的重写,也能描述类的继承的重写 |
@Deprecated | 它是用于描述当前方法是一个过时的方法 | 无 |
@SuppressWarnings | 对程序中的警告去除。 | 无 |
现在我们再次回头看看开头官方文档的那句描述
Java 注解用于为 Java 代码提供元数据。作为元数据,注解不直接影响你的代码执行,但也有一些类型的注解实际上可以用于这一目的。
经过我们前面的了解,注解其实是个很方便的东西,它存活的时间,作用的区域都可以由你方便设置,只是你用注解来干嘛的问题
1 | /**定义限额注解*/ |
运行结果:
到此,对于Java中注解的解析就结束了。最后,也非常感谢您阅读我的文章,文章中如果有错误,请大家给我提出来,大家一起学习进步,如果觉得我的文章给予你帮助,也请给我一个喜欢和关注,同时也欢迎访问我的个人博客。
]]>个人博客对于我们知识的积累过程中起到温故而知新的作用,并且也能达到展示自我的目的。接下来就大致介绍一下以hexo为基础搭建个人博客的过程。
安装 Hexo
1 | npm install -g hexo-cli |
选择一个目录初始化hexo
1 | hexo init |
创建 hexo
1 | npm install |
开启hexo本地服务
1 | hexo s |
其次创建github仓库,仓库名称为<用户名>.github.io
安装hexo-deployer-git插件。在命令行(即Git Bash)运行以下命令即可
1 | npm install hexo-deployer-git --save |
修改_config.yml(在站点目录下)。文件末尾修改为(注意冒号之后必须添一个空格)
1 |
|
推送到GithubPages。在命令行(即Git Bash)依次输入以下命令, 返回INFO Deploy done: git即成功推送:
1 | $ hexo g |
访问我们刚刚搭建好的githubPages博客
域名解析(需要到购买域名的网站设置)
1 | 类型选择为 CNAME; |
绑定域名可能出现https连接不安全,可以获取免费证书,参照链接: Hexo绑定自定义Https域名
https://github.com/iissnan/hexo-theme-next
1 | # Canvas-nest |
1 | # 找到themes\next\layout\_layout.swig文件,添加内容: |
1 | # 在themes\next\source\css\_custom\custom.styl文件末尾添加内容: |
/themes/next/source/css/_common/components/sidebar/sidebar-author.styl
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
.site-author-image {
display: block;
margin: 0 auto;
padding: $site-author-image-padding;
max-width: $site-author-image-width;
height: $site-author-image-height;
border: $site-author-image-border-width solid $site-author-image-border-color;
border-radius: 60%;
transition: 2.5s all;
}
.site-author-image:hover {
transform: rotate(360deg);
}
.site-author-name {
margin: $site-author-name-margin;
text-align: $site-author-name-align;
color: $site-author-name-color;
font-weight: $site-author-name-weight;
}
.site-description {
margin-top: $site-description-margin-top;
text-align: $site-description-align;
font-size: $site-description-font-size;
color: $site-description-color;
}
themes\next\source\images 路径下替换favicon-16x16-next.png
themes\next\source\images路径修改头像替换avatar.jpg,并且在主题配置文件中开启头像设置
1 | avatar: avatar.jpg |
themes\next\source\css_common\components\header路径下
1 | .header {background-image: url(图片地址 或者图片路径images\xxx.jpg);} |
在themes/*/source/css/_custom/custom.styl中添加如下代码:
1 | // Custom styles. |
去掉logo字体背景图
在theme/next/source/css/_common/components/header文件夹下打开site-meta.styl文件,找到.brand{},去掉background: $brand-bg 字段
修改menu样式(\blog\themes\next\source\css_common\components\header\menu.styl文件)
1 | // Menu |
在博客文件夹下面 blog/ 使用git bash 下载插件
1 | npm install --save hexo-generator-feed |
打开主题配置文件搜索rss并修改为如下
1 | rss: /atom.xml |
重新启动发布博客hexo clean清除缓存后$ hexo g 生成静态文件,在文件夹(public)下看到 atom.xml 文件
打开主题配置文件搜索social,把#去掉就可以启用,如需新增在图标库找自己喜欢的小图标,并将名字复制按social格式修改即可
1 | social: |
打开主题配置文件搜索 footer 并按如下对应项修改
1 | footer: |
底部添加访客数和总访问量(编辑主题配置文件)
1 | busuanzi_count: |
编辑主题配置文件
1 |
|
样式地址
在上面样式连接中 挑选自己喜欢的样式,并复制代码。然后把刚才复制的代码粘贴到 themes\next\layout_layout.swig 文件中(放在
的下面),并把href改为你的github地址
1 | <a href="https://your-url" class="github-corner" aria-label="View source on GitHub"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:#151513; color:#fff; position: absolute; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a><style>.github-corner:hover .octo-arm{animation:octocat-wave 560ms ease-in-out}@keyframes octocat-wave{0%,100%{transform:rotate(0)}20%,60%{transform:rotate(-25deg)}40%,80%{transform:rotate(10deg)}}@media (max-width:500px){.github-corner:hover .octo-arm{animation:none}.github-corner .octo-arm{animation:octocat-wave 560ms ease-in-out}}</style> |
打开主题配置文件搜索 post_copyright 并按如下对应项修改:
1 | post_copyright: |
经过上面配置底部版权部分只能出现文章作者,文章链接和版权声明,我们可以修改next\layout_macro\post-copyright.swig 文件,添加文章标题和文章发布日期
1 | <script src="https://cdn.bootcss.com/jquery/2.0.0/jquery.min.js"></script> |
经过如上配置,文章标题和发布日期都显示出来了,但是只能显示英文,中文配置文件没有对应的中文,打开 themes\next\languages\zh-Hans.yml 搜索 copyright: 自定义修改类别名称如下
1 | copyright: |
根据官方说明,编辑博客配置文件
1 | 方式一:(不管是方式一还是方式二,都是必须的): |
hexo新建文章后的目录结构
1 | _posts |
正确的引用图片方式是使用下列的标签插件
1 | 方式一: |
比如新建标签页
1 | # 新建页面 tags |
修主题配置文件config.yml,把 false 改为 true
1 | # Scroll percent label in b2t button. |
1 | npm install hexo-wordcount --save |
1 | post_meta: |
1 | <span title="{{ __('post.wordcount') }}"> |
获取我们前面新建项目的app_id和app_key,主题配置文件中设置对应信息
1 | leancloud_visitors: |
Web安全
因为AppID以及AppKey是暴露在外的,因此如果一些别用用心之人知道了之后用于其它目的是得不偿失的,为了确保只用于我们自己的博客,建议开启Web安全选项,这样就只能通过我们自己的域名才有权访问后台的数据了,可以进一步提升安全性。选择应用的设置的安全中心选项卡,加入我们域名保存。
显示站点统计
1 | busuanzi_count: |
如果无法显示字数统计,原因为不蒜子域名过期的问题
1 | <script async src="//dn-lbstatics.qbox.me/busuanzi/2.3/busuanzi.pure.mini.js"></script> |
改为:
1 | <script async src="//busuanzi.ibruce.info/busuanzi/2.3/busuanzi.pure.mini.js"></script> |
安装插件searchdb
1 | npm install hexo-generator-searchdb --save |
hexo博客配置文件中添加如下配置
1 | search: |
Next主题配置_config.yml文件中更改如下配置(enable设置为true)
1 | local_search: |
方法一:文章属性中加入图片链接
1 |
|
方法二:由于markdown是支持原生html的,所以我们可以在正文引用img来为我们的文章设置摘要配图。
1 | //在“<!-- more -->”之前的内容都会展示到摘要中(同时与你主题文件中配置的摘要字数有关). |
1 | livere_uid: you uid |
1 | highlight: |
1 |
|
1 | //文章添加阴影效果 1 |
1 | # Blog rolls |
1 | baidushare: |
1 | # Warning: Baidu Share does not support https. |
1 | 文件末尾 讲静态资源路径改为我刚刚下载好的静态资源 |
创建clicklove.js文件,并写入如下内容代码
1 | !function(e,t,a){function n(){c(".heart{width: 10px;height: 10px;position: fixed;background: #f00;transform: rotate(45deg);-webkit-transform: rotate(45deg);-moz-transform: rotate(45deg);}.heart:after,.heart:before{content: '';width: inherit;height: inherit;background: inherit;border-radius: 50%;-webkit-border-radius: 50%;-moz-border-radius: 50%;position: fixed;}.heart:after{top: -5px;}.heart:before{left: -5px;}"),o(),r()}function r(){for(var e=0;e<d.length;e++)d[e].alpha<=0?(t.body.removeChild(d[e].el),d.splice(e,1)):(d[e].y--,d[e].scale+=.004,d[e].alpha-=.013,d[e].el.style.cssText="left:"+d[e].x+"px;top:"+d[e].y+"px;opacity:"+d[e].alpha+";transform:scale("+d[e].scale+","+d[e].scale+") rotate(45deg);background:"+d[e].color+";z-index:99999");requestAnimationFrame(r)}function o(){var t="function"==typeof e.onclick&&e.onclick;e.onclick=function(e){t&&t(),i(e)}}function i(e){var a=t.createElement("div");a.className="heart",d.push({el:a,x:e.clientX-5,y:e.clientY-5,scale:1,alpha:1,color:s()}),t.body.appendChild(a)}function c(e){var a=t.createElement("style");a.type="text/css";try{a.appendChild(t.createTextNode(e))}catch(t){a.styleSheet.cssText=e}t.getElementsByTagName("head")[0].appendChild(a)}function s(){return"rgb("+~~(255*Math.random())+","+~~(255*Math.random())+","+~~(255*Math.random())+")"}var d=[];e.requestAnimationFrame=function(){return e.requestAnimationFrame||e.webkitRequestAnimationFrame||e.mozRequestAnimationFrame||e.oRequestAnimationFrame||e.msRequestAnimationFrame||function(e){setTimeout(e,1e3/60)}}(),n()}(window,document); |
在\themes\next\layout_layout.swig文件末尾添加:
1 | <!-- 页面点击小红心 --> |
1 | {% if theme.daovoice %} |
1 | # Online contact |
在\themes\next\layout_macro下新建 passage-end-tag.swig 文件,并添加以下代码(新建文件格式必须是utf-8):
1 | <div> |
打开\themes\next\layout_macro\post.swig文件,在post-body 之后, post-footer (post-footer之前有两个DIV)之前添加如下代码:
1 | <div> |
1 | # 文章末尾添加“本文结束”标记 |
打开模板文件/themes/next/layout/_macro/post.swig,找到rel=”tag”>#字段,
将# 换成,其中tag是你选择标签图标的名字,也是可以自定义的,
如下:
1 | <a href="{{ url_for(tag.path) }}" rel="tag"><i class="fa fa-tag"></i> {{ tag.name }}</a> |
经过我们各种修改美化后的博客,需要同步到github中,具体步骤为
1 | # 在博客目录下 |
新建页面命令,在命令中指定文章的布局(layout),默认为 post,可以通过修改 _config.yml 中的 default_layout 参数来指定默认布局
1 | hexo new [layout] <title> |
layout的类型
布局 | 路径 | 布局含义 |
---|---|---|
post | source/_posts | 文章 |
page | source | 页面 |
draft | source/_drafts | 草稿 |
默认三种layout模板的路径 \blog\scaffolds
如果需要删除文章,则到source/_posts目录下删除对应文章重新发布博客即可
1 | git clone git@github.com:maoqitian/maoqitian.github.io.git |
将blog源文件的目录下所有文件复制到刚刚克隆hexo分支的文件目录username.github.io层级,并下载一下插件
1 | npm install hexo-generator-index --save |
提交时考虑以下注意事项
上一篇文章从源码角度深入理解Glide(上)中,我们已经把Glide加载图片的基本流程走了一遍,想必你已经对Glide的加载原理有了新的认识并且见识到了Glide源码的复杂逻辑,在我们感叹Glide源码复杂的同时我们也忽略了Glide加载图片过程的其它细节,特别是缓存方面,我们在上一篇文章中对于缓存的处理都是跳过的,这一篇文章我们就从Glide的缓存开始再次对Glide进行深入理解。
默认情况下,Glide 会在开始一个新的图片请求之前检查以下多级的缓存:
1 | /**Key 接口*/ |
前面提到了缓存Key的接口,那这个缓存的Key实在哪里生成的,实现类又是什么呢?这我们就要看到加载发动机Engine类的load方法
1 | private final EngineKeyFactory keyFactory; |
由以上源码我们知道,通过EngineKeyFactory的buildKey方法Glide创建了缓存的Key实现类EngineKey对象,由生成EngineKey对象传入的参数我们可以明白,只要有一个参数不同,所生成的EngineKey对象都会是不同的。内存的速度是最快的,理所当然如果内存中有缓存的对应加载图片Glide会搜先从内存缓存中加载。
1 | //跳过内存缓存 |
1 | /**Engine类的load方法*/ |
通过以上Engine类load的源码,首先调用loadFromActiveResources方法来从内存中获取缓存
1 | private final ActiveResources activeResources; |
通过以上源码, 这里需要分几步来解读,首先如果是第一次加载,肯定没有内存缓存,所以如果第一次加载成功,则在加载成功之后调用了Engine对象的onEngineJobComplete方法,并在该方法中将加载成功的resource通过ActiveResources对象的activate方法保存在其内部维护的弱引用(WeakReference)HashMap中。下次再加载相同的资源,当你设置了skipMemoryCache(true),则表明你不想使用内存缓存,这时候Glide再次加载相同资源的时候则会跳过内存缓存的加载,否则可以从ActiveResources对象中获取,如果内存资源没被回收的话(关于弱引用的一下描述可以看看我以前写的一篇文章Android 学习笔记之图片三级缓存)。如果该弱引用资源被回收了(GC),则下一步就到内存中寻找是否有该资源的缓存。
接着回到Engine类的load方法,如果弱引用缓存资源已经被回收,则调用loadFromCache方法在内存缓存中查找缓存资源
1 | /**Engine类的load方法*/ |
通过以上源码,在loadFromCache同样也判断了Glide是否设置了skipMemoryCache(true)方法,没有设置则调用getEngineResourceFromCache方法,在该方法中我们可以看到cache对象就是MemoryCache对象,而该对象实际是一个接口,他的实现类是LruResourceCache,该对象我们前面在GlideBuilder的build方法中进行了新建(在第一步with方法中调用了Glide.get方法,在get方法中初始化Glide调用了在GlideBuilder的build方法),这里也就说明Glide的内存缓存还是使用LruCache来实现,这里如果获取到了内存缓存,则获取内容缓存的同时移除该缓存,并在loadFromCache方法中将该资源标记为正在使用同时加入在弱引用中。这样在ListView或者Recyclerview中加载图片则下次加载首先从弱引用Map中获取缓存资源,并且标志当前资源正在使用,可以防止该资源被LRU算法回收掉。
前面我们只是分析了如何获取内存缓存,而内存缓存又是在哪里写入的呢?根据前面分析,首先获取在弱引用Map中的缓存资源,而前面我们在分析活动资源(Active Resources)时候已经说过是在onEngineJobComplete放中往弱引用Map存放缓存资源,而
onEngineJobComplete方法是在哪里调用呢,这我们就要回想起上一篇文章中我们再网络加载图片成功后腰切换在主线程回调来显示图片,也就是EngineJob对象的handleResultOnMainThread方法
1 | /**EngineJob类的handleResultOnMainThread方法*/ |
通过以上源码,EngineJob类的handleResultOnMainThread方法首先构建了获取好的包含图片的资源,标记当前资源正在使用,通过listener.onEngineJobComplete回调,而listener就是Engine对象,也就到了Engine类的onEngineJobComplete方法,并在该方法中存入了图片资源到弱引用Map中。
上面我是分析了弱引用资源的缓存存入,接着我们看看内存缓存是在哪里存入的,在次看回handleResultOnMainThread方法,我们看到onEngineJobComplete回调前后分别调用了EngineResource对象的acquire方法和release方法
1 | /**EngineResource类的acquire方法*/ |
通过以上源码,其实我们应该能恍然大悟,Glide的内存缓存存入其实就是通过一个acquired变量来进行控制,如果当前弱引用资源不再使用,也就是acquired等于零的时候,则调用回调listener.onResourceReleased(listener就是Engine对象),在onResourceReleased方法中移除了弱引用资源资源,并且没有设置skipMemoryCache(true),则通过cache.put存入内存缓存。
说去磁盘缓存,上一篇文章我们在简单使用Glide的例子中就已经使用了Glide的磁盘缓存
1 | RequestOptions requestOptions = new RequestOptions() |
既然知道如何使用Glide的磁盘缓存,首先我们要了解Glide4中给我提供了哪几种磁盘缓存策略
不知你是否还记得上一篇文章中在加载图片的时候我们是在开启子线程任务在线程池中进行的,我们来回顾一下
1 | /**DecodeJob类的start方法*/ |
通过以上源码,可以分两个步骤来进行解读:
1 | /**GlideBuilder类的build方法*/ |
根据前面分分析,假定没有内存缓存,而是由磁盘缓存,则结合前面分析我们得到了磁盘缓存处理的线程池,也获得枚举Stage是RESOURCE_CACHE或DATA_CACHE,则在DecodeJob对象getNextGenerator方法,我们就能得到对应的Generator
1 | /**DecodeJob的getNextGenerator方法*/ |
通过getNextGenerator方法的源码,如果之前设置磁盘缓存策略为DiskCacheStrategy.RESOURCE,则应该对应的就是枚举Stage.RESOURCE_CACHE,也就是说接下来使用的资源Generator是ResourceCacheGenerator,结合上一篇文章,我们分析网络加载流程是这里获取的是SourceGenerator,我们接着来看ResourceCacheGenerator的startNext()方法
1 | /** ResourceCacheGenerator类的startNext方法*/ |
通过以上源码其实已经很清晰,首先还是获取缓存的唯一key,然后helper.getDiskCache().get(currentKey)这一句话就是获取缓存,helper对象就是DecodeHelper,它的getDiskCache方法获取的对象也就是前面提到的包含DiskLruCacheFactory对象的LazyDiskCacheProvider对象,而LazyDiskCacheProvider对象的getDiskCache方法调用了factory.build(),factory对象DiskLruCacheFactory,也就是获取了我们前面所说的DiskLruCache对象。
1 | /** ResourceCacheGenerator类的startNext方法*/ |
1 | /** DataCacheGenerator类的startNext方法*/ |
根据上一篇文章的分析,加载图片会走到DecodeJob对象的decodeFromRetrievedData方法
1 | /** DecodeJob类的decodeFromRetrievedData方法*/ |
通过以上源码可以看到DecodeJob对象的decodeFromRetrievedData方法通过调用notifyEncodeAndRelease方法,在该方法中调用了内部类DeferredEncodeManager的encode方法存入了磁盘缓存,这里存入的是转换后的磁盘缓存(Resource)。
原始数据也就是SourceGenerator第一次网络下载成功之后获取的图片数据,之后再做磁盘缓存,所以再次回到看到SourceGenerator的onDataReady方法
1 | /**SourceGenerator类的onDataReady方法**/ |
通过以上源码,其实逻辑已经很清晰,会让你有“柳暗花明又一村”的感觉,onDataReady网络请求成功并且设置了缓存策略,则将图片资源赋值给Object类型的dataToCache,执行回调cb.reschedule,cb就是DecodeJob对象,所以接着执行了DecodeJob对象的reschedule方法,该方法再次执行回调也就是执行了Engine对象的reschedule方法,该方法再次执行DecodeJob,也就会再次触发SourceGenerator类的startNext方法,该方法首先判断了Object类型的dataToCache是否有值,前面分析该对象已经赋值,所以就进入到SourceGenerator对象的cacheData方法存入了我们的原始下载图片的缓存。
我们看看如何使用
1 | RequestOptions requestOptions = new RequestOptions().onlyRetrieveFromCache(true); |
使用起来还是很方便的,只要设置onlyRetrieveFromCache(true)方法就行,而它的原理也其实也很简单,我们再次回到DecodeJob对象的getNextStage方法,如果前面获取了缓存,则相应得到对应的Generator加载图片,如果获取不到缓存,则枚举Stage.FINISHED,DecodeJob对象的getNextGenerator方法则会返回null。(如下代码所示)
1 | /**DecodeJob类的getNextStage方法**/ |
1 | /**SingleRequest类的onResourceReady方法**/ |
再次看看Glide监听(listener)的例子
1 | Glide.with(this).load(IMAGE_URL).listener(new RequestListener<Drawable>() { |
Glide监听的实现同样还是基于我们上面分析的SingleRequest对象的onResourceReady方法,使用的时候调用RequestBuilder对象的listener方法,传入的RequestListener对象加入到requestListeners,这样在SingleRequest对象的onResourceReady方法中遍历requestListeners,来回调listener.onResourceReady方法,布尔类型的anyListenerHandledUpdatingTarget则接收回调listener.onResourceReady方法的返回值,如果返回true,则不会执会往下执行,则接着的into方法就不会被触发,说明我们自己在监听中处理,返回false则不拦截。
1 | /**RequestBuilder类的listener方法**/ |
1 | //注意需要指定Glide的加载类型asBitmap,不指定Target不知道本身是是类型的Target |
1 | /** |
1 | new Thread(new Runnable() { |
如何使用
1 | Glide.with(this).load(IMAGE_URL).preload(); |
预加载其实也是属于Target的范围,只是他加载的对象为空而已,也就是没有加载目标
1 | /**RequestBuilder类的preload方法**/ |
通过以上源码,逻辑已经非常清晰,Glide的preload方法里使用的继承SimpleTarget的PreloadTarget对象来作为Target,在它的onResourceReady方法中并没有任何的加载操作,只是调用了Handler来释放资源,到这里也许你会有疑惑,不是说预加载么,怎么不加载。哈哈,其实到onResourceReady方法被调用经过前面的分析Glide已经走完缓存的所有逻辑,那就很容易理解了,预加载只是把图片加载到缓存当中,没有进行其他操作,自然是预加载,并且加载完成之后释放了资源。
1 | //在app下的gradle添加Glide注解处理器的依赖 |
1 | /**GlideApp类部分代码**/ |
GlideOption注解是用来扩展RequestOptions,扩展功能方法第一个参数必须是RequestOptions。下面我们通过设置一个扩展默认设置占位符和错误符方法的例子来说明GlideOption注解。
1 | /** |
如上代码所示,我们可以通过@GlideExtension注解设置自己功能扩展类,使用@GlideOption注解标注对赢扩展功能静态方法,重构项目后Glide注解处理器则会自动在GlideOptions对象和GlideRequest对象中生成相应的方法能够被我们调用
1 | //调用我们刚刚设置的扩展功能方法 |
GlideType注解是用于扩展RequestManager的,同理扩展的方法第一个参数必须是RequestManager,并设置类型为加载资源类型,该注解主要作用就是扩展Glide支持加载资源的类型,以下举出官方文档支持gif的一个例子,还是在我们刚刚扩展功能类中。
1 |
|
同理在我们加载Gif资源的时候可以直接使用
1 | GlideApp.with(this).asMyGif().load(IMAGE_URL) |
到此,真的很想大叫一声宣泄一下,Glide源码就像一座山,一座高峰,你必须沉住气,慢慢的解读,要不然稍不留神就会掉入代码的海洋,迷失方向。回头看看,你不得不感叹正式由于Glide源码中成千上万行的代码,才造就了这样一个强大的框架。最后,也非常感谢您阅读我的文章,文章中如果有错误,请大家给我提出来,大家一起学习进步,如果觉得我的文章给予你帮助,也请给我一个喜欢和关注,同时也欢迎访问我的个人博客。
参考链接
谈到Glide,从英文字面意思有滑行、滑动的意思;而Android从开发的角度我们知道它是一款图片加载框架,这里引用官方文档的一句话“Glide是一个快速高效的Android图片加载库,注重于平滑的滚动”,从官方文档介绍我们了解到用Glide框架来加载图片是快速并且高效的,接下来就来通过简单使用Glide和源码理解两个方面看看Glide是否是快速和高效(文中代码基于Glide 4.8版本)。
1.使用前需要添加依赖
1 | implementation 'com.github.bumptech.glide:glide:4.8.0' |
2.简单加载网络图片到ImageView,可以看到简单一句代码就能将网络图片加载到ImageView,也可以使用Generated API方式
1 | //直接使用 |
3.当加载网络图片的时候,网络请求是耗时操作,所以图片不可能马上就加载出来,网络请求这段时间ImageView是空白的,所以我们可以使用一个占位符显示图片来优化用户体验,占位符有三种
后备回调符(Fallback)
1 | //添加占位图 |
4.指定加载图片的大小(override)
1 | RequestOptions requestOptions = new RequestOptions().override(200,100); |
5.缩略图 (Thumbnail)
这个其实和占位符(placeholder)有些相似,但是占位符只能加载本地资源,而缩略图可以加载网络资源,thumbnail方法与我们的主动加载并行运行,如果主动加载已经完成,则缩略图不会显示
1 | //缩略图Options |
6.图像变化
Glide中内置了三种图片的变化操作,分别是CenterCrop(图片原图的中心区域进行裁剪显示),FitCenter(图片原始长宽铺满)和CircleCrop(圆形裁剪)
1 | //显示圆形裁剪到ImageView |
如果想要更酷炫的变化,可以使用第三方框架glide-transformations来帮助我们实现,并且变化是可以组合的
1 | //第三方框架glide-transformations引入 |
7.加载目标(Target)
Target是介于请求和请求者之间的中介者的角色,into方法的返回值就是target对象,之前我们一直使用的 into(ImageView) ,它其实是一个辅助方法,它接受一个 ImageView 参数并为其请求的资源类型包装了一个合适的 ImageViewTarget
1 | //加载 |
1 | /** |
8.回调监听
1 | Glide.with(this).load(IMAGE_URL). |
Glide还有其他的一些使用方法,这里就不继续展开了,有兴趣的可以自行继续研究。
1 | Glide.with(Context).load(IMAGE_URL).into(mImageView); |
1 | /** Glide类的with()方法*/ |
1 | /** Glide类的getRetriever()方法*/ |
Glide的get方法中通过new GlideBuilder()获取了Glide对象,并通过Glide的getRequestManagerRetriever()的方法最终得到RequestManagerRetriever对象,接下来我们看看RequestManagerRetriever对象的get方法
1 | /** RequestManagerRetriever类的get()方法*/ |
同样,RequestManagerRetriever对象的get方法也有不同类型参数的重载,分别针对Application、Activity、Fragmenet、view做了不同的处理,先看Context参数的get方法,在该方法中它把Context的参数分成了两个类型,一个Application类型的Context,另一个是非Application类型的Context。如果是Application类型的Context,则创建的Glide的生命周期则跟随ApplicationContext的生命周期,也就是下面的getApplicationManager所做的事情。
1 | /** RequestManagerRetriever类的getApplicationManager()方法*/ |
1 | /** RequestManagerRetriever类的fragmentGet()方法*/ |
1 | /** RequestManager 类的as()方法*/ |
1 | /** RequestBuilder 类的load()方法*/ |
1 | /** RequestBuilder 类的apply方法*/ |
通过上一小节的分析,经过load方法之后获取的对象是RequestBuilder,并且我们将load方法的参数赋值给了RequestBuilder对象的model参数,接下来就到了Glide最核心的方法,也就是RequestBuilder对象的into方法
1 | /** RequestBuilder 类的into方法*/ |
RequestBuilder 对象的into方法中首先获取传递进来的ImageView的ScaleType,让Glide加载出来的ImageView保持一样的ScaleType变化,然后我们看到最后一句话,该方法返回了RequestBuilder 对象的另一个into方法,先看glideContext.buildImageViewTarget()做了什么操作
1 | /** GlideContext 类的 buildImageViewTarget方法*/ |
通过以上源码,之前我们看RequestBuilder源码中as方法传入的是Drawable.class,所以以上的buildImageViewTarget方法最终返回的是DrawableImageViewTarget对象,接着我们继续看第一步into方法返回into方法中做了什么操作
1 | /** RequestBuilder 类的into方法返回的into方法*/ |
通过以上源码,我们应该先明白Request类是一个接口,他抽象了Glide加载图片请求(Request类源码这里就不贴了),它是一个非常重要的类,这里我们先看看buildRequest(target, targetListener, options)方法是如何创建Request对象的
1 | private Request buildRequest( |
通过上面的源码,我们可以看到buildRequest方法调用了buildRequestRecursive方法,在buildRequestRecursive方法中大部分代码都在处理缩略图(thumbnail),我们主流程中没有设置缩略图,这里就不进行展开分析,接着buildRequestRecursive方法又调用了obtainRequest方法,obtainRequest方法传递了非常多参数,比如有我们熟悉的RequestOptions,设置图片尺寸的 overrideWidth, overrideHeight,还有第一步into方法中的target对象,也就是DrawableImageViewTarget对象,model也就是我们load传入的图片地址,也就说明不管load方法还是apply方法传入的参数最终都给到了这里传入SingleRequest.obtain方法,我们继续看看SingleRequest类
1 | public final class SingleRequest<R> implements Request, |
通过SingleRequest对象的obtain方法,我们可以看到request = new SingleRequest<>();也就是最终我们构建的Request是SingleRequest对象,并在init方法中将上一步obtainRequest方法传递进来的各种参数进行赋值。
构建完成Request对象,接下来继续看刚刚的into方法下面的操作
1 | requestManager.clear(target); |
首先RequestManager对象清除target,此时不懂你是否还记得RequestManager,该对象是第一步with方法之后得到的,接着是将我们上一步得到的SingleRequest对象设置给target,接着又执行了RequestManager.track方法,继续跟进该方法看看
1 | /** RequestManager 类的track方法*/ |
1 | /** SingleRequest 类的begin方法*/ |
通过以上begin方法源码,如果model为空,也就是我们load传入的图片地址为空,则会调用onLoadFailed方法,而onLoadFailed方法又调用了setErrorPlaceholder方法,接着看看该方法中做了什么操作
1 | /** SingleRequest 类的setErrorPlaceholder方法*/ |
通过以上源码,如果我们传入图片地址为空,则首先查看是否有后备回调符设置,然后是错误占位符,最后是加载占位符,最终调用target.onLoadFailed方法,也就是ImageViewTarget的onLoadFailed方法
1 | public abstract class ImageViewTarget<Z> extends ViewTarget<ImageView, Z>implements Transition.ViewAdapter { |
前面分析完各种占位符实现,我们再次回到SingleRequest对象的begin方法,我们可以注意到onSizeReady()和target.getSize()这两句就是加载图片的入口,如果我们在使用glide的时候设置图片加载的大小尺寸,则会调用target.getSize()
1 | /** ViewTarget 类的etSize方法*/ |
通过以上源码,target.getSize()会根据ImageView的宽高来得出图片的加载宽高,最终target.getSize()还是会调用onSizeReady()方法,所以我们就直接来看看SingleRequest对象onSizeReady()方法中做了什么操作。
1 | /** SingleRequest 类的onSizeReady方法*/ |
在onSizeReady方法中,主要调用了engine.load()方法并返回加载状态,engine.load方法继续接收我们之前传入的各种参数,其中也有我们model对象,也就是之前load方法传入的图片地址。首先我们需要了解engine是什么,顾名思义,engine的英文意思是发动机,而在Glide框架中他就是负责启动图片加载的发动机,主要负责启动加载,我们在前面with方法获取glide对象中得到了engine对象(这里就不贴源码了),我们接着看engine.load()方法进行什么操作
1 | /** Engine 类的load方法*/ |
通过以上源码,Engine对象的load方前面一段代码都是在处理缓存问题,这里先不进行展开,继续走我们加载图片的主线,往下看我们看到构建了一个EngineJob对象,还构建了一个DecodeJob对象,构建DecodeJob对象又继续接收我们之前传入的各种参数,由DecodeJob对象的继承关系我们可以知道它是Runnable对象,接着我们看到engineJob的start()方法,它直接传入了DecodeJob对象
1 | /** EngineJob 类的start方法*/ |
通过以上源码,EngineJob对象的start方法首先还是判断缓存,最终获取的GlideExecutor就是一个线程池执行器(Executor),GlideExecutor中有各种方法获得缓存线程池,还有资源线程池(SourceExecutor),以上源码贴出资源线程池。实际上EngineJob对象的start方法就是用来在线程池中启动DecodeJob这个Runnable对象,也就是说EngineJob的主要作用是开启线程来加载图片,接着我们来看看DecodeJob对象的run方法。
1 | /** DecodeJob 类的run方法*/ |
上面我们再次贴出了一堆代码,我们来好好梳理一下逻辑,DecodeJob对象的run方法中逻辑很简单,就是调用了自身的runWrapped方法,runWrapped方法中首先判断Stage枚举,前面在创建DecodeJob对象时候设置初始状态为Stage.INITIALIZE,然后接着调用getNextStage方法,这里我们还是继续跳过缓存,所以getNextStage方法最终返回的是Stage.SOURCE状态,接着在getNextGenerator()方法中我们获取就是SourceGenerator对象,也就是run方法中的第一句话DataFetcher<?> localFetcher = currentFetcher中localFetcher就是我们刚刚获得的SourceGenerator对象,接着继续执行runGenerators()方法,在该方法的while循环判断条件执行了currentGenerator.startNext()方法,也就是SourceGenerator对象的startNext()方法
1 | /** SourceGenerator 类的startNext()方法*/ |
通过上面源码,我们接着看到loadData=helper.getLoadData().get(loadDataListIndex++)这一句代码,helper就是DecodeHelper对象,在我们前面创建DecodeJob对象的时候已经把它创建,之前我们在load步骤中传入的model是图片url地址,所以经过DecodeHelper 类的getLoadData() 方法(更细的代码这里就不进行展开了),最终获取的ModelLoader<Object, ?> modelLoader对象则为HttpGlideUrlLoader对象,也就是laodData对象,所以modelLoader.buildLoadData创建则在HttpGlideUrlLoader对象的buildLoadData中实现,上方贴出的该方法源码中把我们model赋值给GlideUrl对象,也就是将其作为URL地址来进行处理,则经过modelLoader.buildLoadData获取的loadData.fetcher则对应HttpUrlFetcher对象,所以loadData.fetcher.loadData调用的就是HttpUrlFetcher对象loadData方法,
1 | /**HttpUrlFetcher类的loadData方法 **/ |
通过以上源码,HttpUrlFetcher对象的loadData方法首先调用自身loadDataWithRedirects方法,接着我们看到该方法源码,这里使用了HttpURLConnection来执行了网络请求,看到这里内心还是有点开心的,前面看了这么多源码,终于看到Glide的网络请求了,开心之后还没完呢,还得接着往下看,执行完网络请求成功,loadDataWithRedirects方法中网络请求成功调用getStreamForSuccessfulRequest返回了一个InputStream流(记住这个InputStream,很关键),然后执行了一个callback回调,而这个回调对象就是我们之前在SourceGenerator对象中调用loadData方法传入SourceGenerator对象本身,所以callback.onDataReady()调用的就是SourceGenerator对象的onDataReady方法
1 | /**SourceGenerator类的onDataReady方法 **/ |
通过以上源码,不走缓存的情况下则调用cb.onDataFetcherReady,这个cb也就是前面我们new SourceGenerator对象传入的 DecodeJob对象,也就是调用DecodeJob对象onDataFetcherReady方法
1 | /**DecodeJob类的onDataFetcherReady方法 **/ |
通过以上源码,onDataFetcherReady方法中将之前网络请求得到的流赋值给当前的DecodeJob对象的currentData,其他数据都赋值给对应字段,最终调用的是decodeFromRetrievedData方法
1 | /**DecodeJob类的decodeFromRetrievedData方法 **/ |
通过以上源码,decodeFromRetrievedData方法调用了decodeFromFetcher方法,在该方法中首先通过decodeHelper.getLoadPath获取LoadPath对象,LoadPath对象其实是根据我们传入的处理数据来返回特定的数据解码转码处理器,我们跟进decodeHelper.getLoadPath看看
1 | /** DecodeHelper类的getLoadPath方法*/ |
通过以上源码,我们接着前面跟进DecodeHelper.getLoadPath方法,它调用了Registry对象的getLoadPath方法,Registry对象的getLoadPath方法又调用了自身的getDecodePaths方法,现在我前面提到过得我们网络请求获取的是InputStream流,所以上面源码getDecodePaths方法中Data泛型就是InputStream,在根据getDecoders方法遍历得到解码器ResourceDecoder能处理InputStream流的有StreamBitmapDecoder和StreamGifDecoder,StreamGifDecoder处理的是Gif,我们这里处理图片就之能是StreamBitmapDecoder,它将InputStream流解码成bitmap,然后能将bitmap转换成Drawable的转码器ResourceTranscoder对象则是BitmapDrawableTranscoder,最后getDecodePaths将我们刚刚分析得到的解码器和转码器传递给了新建的DecodePath对象,DecodePath对象就是用来帮助我们进行解码和转码的。
1 | /**DecodePath类的decode方法**/ |
通过以上源码,DecodePath对象的decode方法调用了decodeResource方法,decodeResource又调用了decodeResourceWithList方法,经过前面分析,decodeResourceWithList方法中获得的decoder就是前面提到的解码器StreamBitmapDecoder对象,所以我们接着看StreamBitmapDecoder的decode方法
1 | /**StreamBitmapDecoder类的decode方法**/ |
通过以上源码,StreamBitmapDecoder的decode方法中只是对InputStream进行包装(装饰模式),可以让Glide进行更多操作,最终调用了downsampler.decode,这个downsampler对象则是Downsampler对象(英文注释:Downsamples, decodes, and rotates images according to their exif orientation.),英文注释大致意思是对图像exif格式进行采样、解码和旋转。而我们这里调用了它的decode方法,也就是对我们前面包装的流进行解码
1 | /**Downsampler类的decode方法**/ |
通过以上源码,Downsampler对象的decode方法首先调用了decodeFromWrappedStreams方法,在decodeFromWrappedStreams方法中又调用了decodeStream方法,在该方法中调用用了BitmapFactory.decodeStream,到这里我们终于看到了Glide将InputStream流解析成了bitmap,而最终Downsampler对象的decode方法返回的Resource对象就是BitmapResource对象
经过前面的分析,Glide已经将InputStream解码完成,这时我们还得再次回到DecodePath对象的decode方法,解码完成还需转码,这里再次贴一下ecodePath对象的decode方法,前面已经分析了转码器为BitmapDrawableTranscoder对象,所以我们继续看BitmapDrawableTranscoder对象transcode做了什么操作
1 | /**DecodePath类的decode方法**/ |
通过以上源码,BitmapDrawableTranscoder对象transcode方法最终返回了LazyBitmapDrawableResource对象,也就是将我们解码拿到的BitmapResource对象转换成了LazyBitmapDrawableResource对象
到此,Glide整个图片解码转码已近完成,接着我们再回到DecodeJob对象的decodeFromRetrievedData方法
1 | /**DecodeJob类的decodeFromRetrievedData方法**/ |
通过以上源码,DecodeJob对象的decodeFromRetrievedData方法调用notifyEncodeAndRelease方法,将我们上一步获取的LazyBitmapDrawableResource传入notifyComplete方法中,在notifyComplete调用了callback.onResourceReady,而这个callback对象就是EngineJob对象(它实现了DecodeJob.Callback接口),也许到这里你已经忘了EngineJob对象是什么,前面我们开启线程执行加载的start方法就在EngineJob对象中,所以我们去看看EngineJob对象的onResourceReady方法
1 | private static final Handler MAIN_THREAD_HANDLER = |
通过以上源码,前面我们在Engine类的Load方法中已经将SingleRequest这个对象通过EngineJob对象的addCallback方法加入到了cbs这个List当中,EngineJob对象的onResourceReady方法中将我们加载好的图片对象通过Hanlder将数据又传递到了主线程(主线程更新UI),也就是handleResultOnMainThread方法中根据我们刚刚的分析通过cb.onResourceReady将数据回调通知,cb对象就是SingleRequest对象,我们接着看SingleRequest对象的onResourceReady回调方法
1 | /**SingleRequest类的onResourceReady回调方法**/ |
通过以上源码,这时候我们已经可以看到长征胜利的曙光了,SingleRequest对象的onResourceReady回调方法中调用了resource.get(),而这个resource就是前面我们经过解码、转码获取的LazyBitmapDrawableResource对象,然后又调用了SingleRequest对象的onResourceReady私有方法,在该方法中又调用了target.onResourceReady方法,在我们最开始进入into方法的时候我们已经分析过创建的target对象就是DrawableImageViewTarget对象,它继承了抽象类ImageViewTarget,所以我们看看抽象类ImageViewTarget的onResourceReady方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24/** LazyBitmapDrawableResource类的get()方法**/
@NonNull
@Override
public BitmapDrawable get() {
return new BitmapDrawable(resources, bitmapResource.get());
}
/** ImageViewTarget类的onResourceReady方法**/
@Override
public void onResourceReady(@NonNull Z resource, @Nullable Transition<? super Z> transition) {
if (transition == null || !transition.transition(resource, this)) {
setResourceInternal(resource);
} else {
maybeUpdateAnimatable(resource);
}
}
/** ImageViewTarget类的setResourceInternal方法**/
private void setResourceInternal(@Nullable Z resource) {
// Order matters here. Set the resource first to make sure that the Drawable has a valid and
// non-null Callback before starting it.
setResource(resource);
maybeUpdateAnimatable(resource);
}
protected abstract void setResource(@Nullable Z resource);
1 | @Override |
到此,我们的万里长征终于结束了一半,Glide的简单加载图片流程已经分析完了。
最后我还是想要把那句简单的代码给贴出来
1 | Glide.with(Context).load(IMAGE_URL).into(mImageView); |
就是这样一句简单的代码,它背后所走的逻辑却让人头皮发麻,此时我只想说一句话“read the fuck source code”。前面我们只是分析了Glide简单的加载图片流程,它的缓存使用,各种变换,回调和各种功能原理还没分析到,这只能等到下篇文章了。文章中如果有错误,请大家给我提出来,大家一起学习进步,如果觉得我的文章给予你帮助,也请给我一个喜欢和关注,同时也欢迎访问我的个人博客。
参考链接