当前位置:网站首页>Wechat's crazy use of glide - life cycle learning
Wechat's crazy use of glide - life cycle learning
2022-07-29 03:02:00 【The procedural ape will point north】
Glide Life cycle action
When there are a lot of requests for pictures , Will cause network congestion , The pictures that are not displayed are still being requested , This is not reasonable , therefore Glide You need to monitor the lifecycle to maintain Request, Another use of life cycle is to monitor the network changes of each page .
Glide characteristic
- Easy to use
- High configurability , Highly adaptive
- Support common picture formats (jpg、png、gif、webp)
- Support multiple data sources ( The Internet 、 Local 、 resources 、Assets etc. )
- Efficient caching strategy ( Support Memory and Disk Image caching , Default Bitmap Format adopted RGB_565 Small memory )
- Lifecycle Integration ( according to Activity/Fragment Lifecycle automatically manages requests )
- Efficient processing Bitmap( Use BitmapPool Reuse Bitmap, Active call recycle Recycle what needs to be recycled Bitmap)
Glide flow chart
[ Failed to transfer the external chain picture , The origin station may have anti-theft chain mechanism , It is suggested to save the pictures and upload them directly (img-H6npo4tS-1658998499622)(https://upload-images.jianshu.io/upload_images/6957639-112786ec47bb95b6.png?imageMogr2/auto-orient/strip|imageView2/2/w/1200/format/webp)]
Glide analysis
One 、Glide Life cycle delivery
First look with Execution of a function , Can structure glide Single case , and
RequestManagerRetriever stay initializeGlide Construction will be carried out in .
// Glide.java
public static RequestManager with(@NonNull Activity activity) {
return getRetriever(activity).get(activity);
}
@NonNull
private static RequestManagerRetriever getRetriever(@Nullable Context context) {
// Context could be null for other reasons (ie the user passes in null), but in practice it will
// only occur due to errors with the Fragment lifecycle.
Preconditions.checkNotNull(
context,
"You cannot start a load on a not yet attached View or a Fragment where getActivity() "
+ "returns null (which usually occurs when getActivity() is called before the Fragment "
+ "is attached or after the Fragment is destroyed).");
return Glide.get(context).getRequestManagerRetriever();
}
@NonNull
public static Glide get(@NonNull Context context) {
if (glide == null) {
synchronized (Glide.class) {
if (glide == null) {
checkAndInitializeGlide(context);
}
}
}
return glide;
}
private static void checkAndInitializeGlide(@NonNull Context context) {
// In the thread running initGlide(), one or more classes may call Glide.get(context).
// Without this check, those calls could trigger infinite recursion.
if (isInitializing) {
throw new IllegalStateException("You cannot call Glide.get() in registerComponents(),"
+ " use the provided Glide instance instead");
}
isInitializing = true;
initializeGlide(context);
isInitializing = false;
}
constructed RequestManagerRetriever adopt get Return to one RequestManager, If not in the main thread , Default will be passed in getApplicationContext, That is, no Lifecycle Management :
- stay getRequestManagerFragment First check the current Activity Is there a FRAGMENT_TAG This label corresponds to Fragment, Go back if you have
- without , Will judge pendingRequestManagerFragments Is there a , If there is, go back
- without , Will rewrite new One , Then put it into the pendingRequestManagerFragments in , Then add to the current Activity, Give again Handler Send a removed message
// RequestManagerRetriever.java
@NonNull
public RequestManager get(@NonNull Activity activity) {
if (Util.isOnBackgroundThread()) {
return get(activity.getApplicationContext());
} else {
assertNotDestroyed(activity);
android.app.FragmentManager fm = activity.getFragmentManager();
return fragmentGet(
activity, fm, /*parentHint=*/ null, isActivityVisible(activity));
}
}
private RequestManager fragmentGet(@NonNull Context context, @NonNull android.app.FragmentManager fm, @Nullable android.app.Fragment parentHint, boolean isParentVisible) {
RequestManagerFragment current = getRequestManagerFragment(fm, parentHint, isParentVisible);
RequestManager requestManager = current.getRequestManager();
if (requestManager == null) {
// TODO(b/27524013): Factor out this Glide.get() call.
Glide glide = Glide.get(context);
requestManager =
factory.build(
glide, current.getGlideLifecycle(), current.getRequestManagerTreeNode(), context);
current.setRequestManager(requestManager);
}
return requestManager;
}
private RequestManagerFragment getRequestManagerFragment(
@NonNull final android.app.FragmentManager fm, @Nullable android.app.Fragment parentHint, boolean isParentVisible) {
RequestManagerFragment current = (RequestManagerFragment) fm.findFragmentByTag(FRAGMENT_TAG);
if (current == null) {
current = pendingRequestManagerFragments.get(fm);
if (current == null) {
current = new RequestManagerFragment();
current.setParentFragmentHint(parentHint);
if (isParentVisible) {
current.getGlideLifecycle().onStart();
}
pendingRequestManagerFragments.put(fm, current);
fm.beginTransaction().add(current, FRAGMENT_TAG).commitAllowingStateLoss();
handler.obtainMessage(ID_REMOVE_FRAGMENT_MANAGER, fm).sendToTarget();
}
}
return current;
}
public boolean handleMessage(Message message) {
...
switch (message.what) {
case ID_REMOVE_FRAGMENT_MANAGER:
android.app.FragmentManager fm = (android.app.FragmentManager) message.obj;
key = fm;
removed = pendingRequestManagerFragments.remove(fm);
break;
...
}
...
}
There is a problem that needs to be paid attention to , Is that if with() What's coming in the function is not Activity, It is Fragment, Then I will create a new one without interface RequestManagerFragment, And its father Fragment That's what came in Fragment.
Why does it need pendingRequestManagerFragments Cache first ? This is explained in the second question below . First, let's look down at the transmission of life cycle .
RequestManagerFragment Is a very important class ,Glide It's through it as a distribution portal for the lifecycle ,RequestManagerFragment The default constructor of will instantiate a ActivityFragmentLifecycle, In every life cycle onStart/onStop/onDestroy Will call ActivityFragmentLifecycle:
// RequestManagerFragment.java
public class RequestManagerFragment extends Fragment {
private static final String TAG = "RMFragment";
private final ActivityFragmentLifecycle lifecycle;
@Nullable private RequestManager requestManager;
public RequestManagerFragment() {
this(new ActivityFragmentLifecycle());
}
RequestManagerFragment(@NonNull ActivityFragmentLifecycle lifecycle) {
this.lifecycle = lifecycle;
}
@Override
public void onStart() {
super.onStart();
lifecycle.onStart();
}
@Override
public void onStop() {
super.onStop();
lifecycle.onStop();
}
@Override
public void onDestroy() {
super.onDestroy();
lifecycle.onDestroy();
unregisterFragmentWithRoot();
}
...
}
RequestManagerFragment There is an example RequestManager, In front of fragmentGet,RequestManagerFragment I will try to get it after I get it RequestManager, The first acquisition is definitely not , It's going to reconstruct a , adopt RequestManagerRetriever It was passed in during construction RequestManagerFactory The factory class instantiates a RequestManager, hold RequestManagerFragment Medium ActivityFragmentLifecycle Pass in :
// RequestManagerRetriever.java
public interface RequestManagerFactory {
@NonNull
RequestManager build(
@NonNull Glide glide,
@NonNull Lifecycle lifecycle,
@NonNull RequestManagerTreeNode requestManagerTreeNode,
@NonNull Context context);
}
private static final RequestManagerFactory DEFAULT_FACTORY = new RequestManagerFactory() {
@NonNull
@Override
public RequestManager build(@NonNull Glide glide, @NonNull Lifecycle lifecycle, @NonNull RequestManagerTreeNode requestManagerTreeNode, @NonNull Context context) {
return new RequestManager(glide, lifecycle, requestManagerTreeNode, context);
}
};
Obviously the key to life cycle is ActivityFragmentLifecycle, stay RequestManagerFragment It will be recalled in the corresponding life cycle , So guess it's maintaining a list of observers in it , Notify when the event occurs , Look at the source code :
// ActivityFragmentLifecycle.java
class ActivityFragmentLifecycle implements Lifecycle {
private final Set<LifecycleListener> lifecycleListeners =
Collections.newSetFromMap(new WeakHashMap<LifecycleListener, Boolean>());
private boolean isStarted;
private boolean isDestroyed;
@Override
public void addListener(@NonNull LifecycleListener listener) {
lifecycleListeners.add(listener);
if (isDestroyed) {
listener.onDestroy();
} else if (isStarted) {
listener.onStart();
} else {
listener.onStop();
}
}
@Override
public void removeListener(@NonNull LifecycleListener listener) {
lifecycleListeners.remove(listener);
}
void onStart() {
isStarted = true;
for (LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) {
lifecycleListener.onStart();
}
}
void onStop() {
isStarted = false;
for (LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) {
lifecycleListener.onStop();
}
}
void onDestroy() {
isDestroyed = true;
for (LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) {
lifecycleListener.onDestroy();
}
}
}
therefore RequestManagerFragment Pass this on to RequestManager after , I'm sure to register observers , to glance at RequestManager Related code , In the constructor lifecycle.addListener(this);, Register yourself as an observer :
// RequestManager.java
public class RequestManager implements LifecycleListener,
ModelTypes<RequestBuilder<Drawable>> {
...
protected final Glide glide;
protected final Context context;
@Synthetic final Lifecycle lifecycle;
private final RequestTracker requestTracker;
private final RequestManagerTreeNode treeNode;
private final TargetTracker targetTracker = new TargetTracker();
private final Runnable addSelfToLifecycle = new Runnable() {
@Override
public void run() {
lifecycle.addListener(RequestManager.this);
}
};
private final Handler mainHandler = new Handler(Looper.getMainLooper());
private final ConnectivityMonitor connectivityMonitor;
private RequestOptions requestOptions;
public RequestManager(
@NonNull Glide glide, @NonNull Lifecycle lifecycle, @NonNull RequestManagerTreeNode treeNode, @NonNull Context context) {
this(
glide,
lifecycle,
treeNode,
new RequestTracker(),
glide.getConnectivityMonitorFactory(),
context);
}
// Our usage is safe here.
@SuppressWarnings("PMD.ConstructorCallsOverridableMethod")
RequestManager(
Glide glide, Lifecycle lifecycle, RequestManagerTreeNode treeNode, RequestTracker requestTracker, ConnectivityMonitorFactory factory, Context context) {
this.glide = glide;
this.lifecycle = lifecycle;
this.treeNode = treeNode;
this.requestTracker = requestTracker;
this.context = context;
connectivityMonitor =
factory.build(
context.getApplicationContext(),
new RequestManagerConnectivityListener(requestTracker));
if (Util.isOnBackgroundThread()) {
mainHandler.post(addSelfToLifecycle);
} else {
lifecycle.addListener(this);
}
lifecycle.addListener(connectivityMonitor);
setRequestOptions(glide.getGlideContext().getDefaultRequestOptions());
glide.registerRequestManager(this);
}
Looking at the RequestManager In the corresponding life cycle , Start separately here , Stop and destroy requests :
// RequestManager
@Override
public void onStart() {
resumeRequests();
targetTracker.onStart();
}
@Override
public void onStop() {
pauseRequests();
targetTracker.onStop();
}
@Override
public void onDestroy() {
targetTracker.onDestroy();
for (Target<?> target : targetTracker.getAll()) {
clear(target);
}
targetTracker.clear();
requestTracker.clearRequests();
lifecycle.removeListener(this);
lifecycle.removeListener(connectivityMonitor);
mainHandler.removeCallbacks(addSelfToLifecycle);
glide.unregisterRequestManager(this);
}
3、 ... and 、Glide Why is it right Fragment Do the cache ?
Stick it again RequestManagerRetriever In order to get Fragment Code for , There's a question ahead , Why do we need a pendingRequestManagerFragments Yes Fragment Cache .
// RequestManagerRetriever.java
/** * Pending adds for RequestManagerFragments. */
@SuppressWarnings("deprecation")
@VisibleForTesting
final Map<android.app.FragmentManager, RequestManagerFragment> pendingRequestManagerFragments = new HashMap<>();
private RequestManagerFragment getRequestManagerFragment(
@NonNull final android.app.FragmentManager fm, @Nullable android.app.Fragment parentHint, boolean isParentVisible) {
RequestManagerFragment current = (RequestManagerFragment) fm.findFragmentByTag(FRAGMENT_TAG);
if (current == null) {
current = pendingRequestManagerFragments.get(fm);
if (current == null) {
current = new RequestManagerFragment();
current.setParentFragmentHint(parentHint);
if (isParentVisible) {
current.getGlideLifecycle().onStart();
}
pendingRequestManagerFragments.put(fm, current);
fm.beginTransaction().add(current, FRAGMENT_TAG).commitAllowingStateLoss();
handler.obtainMessage(ID_REMOVE_FRAGMENT_MANAGER, fm).sendToTarget();
}
}
return current;
}
Let's see a situation :
Glide.with(Context).load(ImageUrl1).into(imageview1); // task1
Glide.with(Context).load(ImageUrl2).into(imageview2); // task2
Android Development should know that the main thread has a Handler Mechanism , Will turn out for the Message queue Intermediate message , adopt Looper Take it out in order to execute . What is the relationship between the execution order in the main thread and the execution order in the message queue ? Look at a chestnut :
private void start() {
mHandler = new Handler(getMainLooper());
VLog.i("HandlerRunT", "=========Begin!============");
mHandler.post(new Runnable() {
@Override
public void run() {
VLog.i("HandlerRunT", "=========First!============");
}
});
VLog.i("HandlerRunT", "=========Middle!============");
mHandler.sendMessage(Message.obtain(mHandler, new Runnable() {
@Override
public void run() {
VLog.i("HandlerRunT", "=========Second!============");
}
}));
VLog.i("HandlerRunT", "=========End!============");
Next();
}
private void Next() {
VLog.i("HandlerRunT", "=========Next Begin!============");
mHandler.post(new Runnable() {
@Override
public void run() {
VLog.i("HandlerRunT", "=========Next First!============");
}
});
VLog.i("HandlerRunT", "=========Next Middle!============");
mHandler.sendMessage(Message.obtain(mHandler, new Runnable() {
@Override
public void run() {
VLog.i("HandlerRunT", "=========Next Second!============");
}
}));
VLog.i("HandlerRunT", "=========Next End!============");
}
stay start The order of printing in and Handler Which of the information in print first ?start in handler Information and Next What is the printing order of the information in the function ? Look at the printout :
HandlerRunT: =========Begin!============
HandlerRunT: =========Middle!============
HandlerRunT: =========End!============
HandlerRunT: =========Next Begin!============
HandlerRunT: =========Next Middle!============
HandlerRunT: =========Next End!============
HandlerRunT: =========First!============
HandlerRunT: =========Second!============
HandlerRunT: =========Next First!============
HandlerRunT: =========Next Second!============
Handler The order in will be after the main thread ,Handler The message execution order in is queue first in first out .
It goes to task1 When , Here are two lines of code ,add The operation puts a message on the message queue , It's marked here as msg1:
fm.beginTransaction().add(current, FRAGMENT_TAG).commitAllowingStateLoss();
// FragmentManager.java
public void enqueueAction(OpGenerator action, boolean allowStateLoss) {
if (!allowStateLoss) {
checkStateLoss();
}
synchronized (this) {
if (mDestroyed || mHost == null) {
if (allowStateLoss) {
// This FragmentManager isn't attached, so drop the entire transaction.
return;
}
throw new IllegalStateException("Activity has been destroyed");
}
if (mPendingActions == null) {
mPendingActions = new ArrayList<>();
}
mPendingActions.add(action);
scheduleCommit();
}
}
private void scheduleCommit() {
synchronized (this) {
boolean postponeReady =
mPostponedTransactions != null && !mPostponedTransactions.isEmpty();
boolean pendingReady = mPendingActions != null && mPendingActions.size() == 1;
if (postponeReady || pendingReady) {
mHost.getHandler().removeCallbacks(mExecCommit);
mHost.getHandler().post(mExecCommit);
}
}
}
Then if you don't put task1 Constructed in RequestManagerFragment Put it in pendingRequestManagerFragments in , Then in execution task2 It will also reconstruct another RequestManagerFragment, And put a message in the main thread msg2, There will be repetition at this time add The situation of .
So in front new Out of a RequestManagerFragment, Then put it in pendingRequestManagerFragments in , that task2 You can get... From the cache when you come in again , There will be no more new and add 了 .
So the next question is , Why does this line of code appear ,add Then I need to send a message immediately remove fall ? Stop at the front task2 repeat new and add After operation , Just delete the cache , Can avoid memory leaks and memory pressure :
// RequestManagerRetriever.java
pendingRequestManagerFragments.put(fm, current);
fm.beginTransaction().add(current, FRAGMENT_TAG).commitAllowingStateLoss();
handler.obtainMessage(ID_REMOVE_FRAGMENT_MANAGER, fm).sendToTarget();
Four 、Glide How to monitor network changes
From the analysis section of the page life cycle above , Control of tasks is through RequestManager, Or go to see it , To realize network change monitoring is ConnectivityMonitor:
// RequestManager.java
public class RequestManager implements LifecycleListener,
ModelTypes<RequestBuilder<Drawable>> {
...
protected final Glide glide;
protected final Context context;
@Synthetic final Lifecycle lifecycle;
private final RequestTracker requestTracker;
private final RequestManagerTreeNode treeNode;
private final TargetTracker targetTracker = new TargetTracker();
private final Handler mainHandler = new Handler(Looper.getMainLooper());
private final ConnectivityMonitor connectivityMonitor;
...
RequestManager(
Glide glide, Lifecycle lifecycle, RequestManagerTreeNode treeNode, RequestTracker requestTracker, ConnectivityMonitorFactory factory, Context context) {
this.glide = glide;
this.lifecycle = lifecycle;
this.treeNode = treeNode;
this.requestTracker = requestTracker;
this.context = context;
connectivityMonitor =
factory.build(
context.getApplicationContext(),
new RequestManagerConnectivityListener(requestTracker));
if (Util.isOnBackgroundThread()) {
mainHandler.post(addSelfToLifecycle);
} else {
lifecycle.addListener(this);
}
lifecycle.addListener(connectivityMonitor);
...
}
So also register it as ActivityFragmentLifecycle The observer ,ConnectivityMonitor adopt ConnectivityMonitorFactory Construct , Provides a default implementation class DefaultConnectivityMonitorFactory:
// DefaultConnectivityMonitorFactory.java
public class DefaultConnectivityMonitorFactory implements ConnectivityMonitorFactory {
private static final String TAG = "ConnectivityMonitor";
private static final String NETWORK_PERMISSION = "android.permission.ACCESS_NETWORK_STATE";
@NonNull
@Override
public ConnectivityMonitor build(
@NonNull Context context, @NonNull ConnectivityMonitor.ConnectivityListener listener) {
int permissionResult = ContextCompat.checkSelfPermission(context, NETWORK_PERMISSION);
boolean hasPermission = permissionResult == PackageManager.PERMISSION_GRANTED;
return hasPermission
? new DefaultConnectivityMonitor(context, listener) : new NullConnectivityMonitor();
}
}
Then look down DefaultConnectivityMonitor, stay onStart in registerReceiver Monitor the broadcast of mobile network status changes , And then in connectivityReceiver Call in isConnect Confirm network status , According to whether the network state changes , If there is any change, call back to monitor ConnectivityMonitor.ConnectivityListener:
final class DefaultConnectivityMonitor implements ConnectivityMonitor {
private static final String TAG = "ConnectivityMonitor";
private final Context context;
@SuppressWarnings("WeakerAccess") @Synthetic final ConnectivityListener listener;
@SuppressWarnings("WeakerAccess") @Synthetic boolean isConnected;
private boolean isRegistered;
private final BroadcastReceiver connectivityReceiver = new BroadcastReceiver() {
@Override
public void onReceive(@NonNull Context context, Intent intent) {
boolean wasConnected = isConnected;
isConnected = isConnected(context);
if (wasConnected != isConnected) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "connectivity changed, isConnected: " + isConnected);
}
listener.onConnectivityChanged(isConnected);
}
}
};
DefaultConnectivityMonitor(@NonNull Context context, @NonNull ConnectivityListener listener) {
this.context = context.getApplicationContext();
this.listener = listener;
}
private void register() {
if (isRegistered) {
return;
}
// Initialize isConnected.
isConnected = isConnected(context);
try {
// See #1405
context.registerReceiver(connectivityReceiver,
new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
isRegistered = true;
} catch (SecurityException e) {
// See #1417, registering the receiver can throw SecurityException.
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "Failed to register", e);
}
}
}
private void unregister() {
if (!isRegistered) {
return;
}
context.unregisterReceiver(connectivityReceiver);
isRegistered = false;
}
@SuppressWarnings("WeakerAccess")
@Synthetic
// Permissions are checked in the factory instead.
@SuppressLint("MissingPermission")
boolean isConnected(@NonNull Context context) {
ConnectivityManager connectivityManager =
Preconditions.checkNotNull(
(ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE));
NetworkInfo networkInfo;
try {
networkInfo = connectivityManager.getActiveNetworkInfo();
} catch (RuntimeException e) {
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "Failed to determine connectivity status when connectivity changed", e);
}
// Default to true;
return true;
}
return networkInfo != null && networkInfo.isConnected();
}
@Override
public void onStart() {
register();
}
@Override
public void onStop() {
unregister();
}
@Override
public void onDestroy() {
// Do nothing.
}
}
ConnectivityMonitor.ConnectivityListener Is in RequestManager In the middle of , There is a restart request after the network reconnects :
// RequestManager.java
private static class RequestManagerConnectivityListener implements ConnectivityMonitor
.ConnectivityListener {
private final RequestTracker requestTracker;
RequestManagerConnectivityListener(@NonNull RequestTracker requestTracker) {
this.requestTracker = requestTracker;
}
@Override
public void onConnectivityChanged(boolean isConnected) {
if (isConnected) {
requestTracker.restartRequests();
}
}
}
5、 ... and 、Glide How to monitor memory
stay Glide It will call registerComponentCallbacks Register globally , The system calls back when memory is tight onTrimMemory, Then according to the system memory tension level memoryCache/bitmapPool/arrayPool The recycling of :
// Glide.java
public static Glide get(@NonNull Context context) {
if (glide == null) {
synchronized (Glide.class) {
if (glide == null) {
checkAndInitializeGlide(context);
}
}
}
return glide;
}
private static void initializeGlide(@NonNull Context context, @NonNull GlideBuilder builder) {
Context applicationContext = context.getApplicationContext();
...
applicationContext.registerComponentCallbacks(glide);
Glide.glide = glide;
}
@Override
public void onTrimMemory(int level) {
trimMemory(level);
}
public void trimMemory(int level) {
Util.assertMainThread();
memoryCache.trimMemory(level);
bitmapPool.trimMemory(level);
arrayPool.trimMemory(level);
}
【 obtain 】 paving Android Architect technical notes
6、 ... and 、 summary
Glide The source code of is relatively large and of high quality , So one or two articles are not clear , The back for Glide Source code analysis will be followed by articles , Welcome to your attention .
边栏推荐
- MySql的安装配置超详细教程与简单的建库建表方法
- Day 5 experiment
- SOA(面向服务架构)是什么?
- C语言基础知识点汇总
- Engineering boy: under 20 years old, ordinary but not mediocre
- Self organization is the two-way rush of managers and members
- Restfulapi - C - add header username and password authentication
- Chapter 2 VRP command line
- 多线程实现多用例文件并发读取执行+Selenium Grid4实现测试框架分布式部署
- 百度副总裁李硕:数字技术加持下中国劳动力成本上升是好事
猜你喜欢
随机推荐
【luogu P8352】小 N 的独立集(DP套DP)(性质)
Thirty years of MPEG audio coding
Zone --- line segment tree lazy marking board sub problem
C陷阱与缺陷 第3章 语义“陷阱” 3.8 运算符&&、||和!
解析机器人与人类情感共鸣的主观意识
Groupby method
mysql大表联合查询优化,大事务优化,规避事务超时,锁等待超时与锁表
会议OA项目之我的审批功能
Plug in --- line segment sloth marking board + simple mathematical reasoning
万字详解 Google Play 上架应用标准包格式 AAB
数仓中概念术语解析
C和指针 第3章 语义“陷阱” 3.5 空指针并非字符串
Summary of common hooks
Verilog的时间系统任务----$time、$stime、$realtime
【机器人学习】机械臂抓手matlab运动学与admas动力学分析
FTP protocol details
MySQL large table joint query optimization, large transaction optimization, avoiding transaction timeout, lock wait timeout and lock table
Redis配置缓存过期监听事件触发
Pgzero飞机大战
混淆矩阵学习笔记