在前面《Android启动过程》中提到了System进程启动ActivityManagerService服务,AMS是由Android提供的用于管理Activity(不仅仅指Activity,还包括其他三个组件)运行状态的系统进程,则是平时编写APK应用程序时使用得最频繁的一个系统服务。

AMS是通过ActivityStack(和其它数据结构)来记录、管理系统中的Activity(和其它组件)状态,并提供查询功能的一个系统服务,负责启动和调度应用程序组件。
一、AMS功能概述

1、回到《Android启动过程》中提到的使用SystemServer中ServerThread来启动AMS服务:
/** @path: \frameworks\base\services\java\com\android\server\SystemServer.java */
class ServerThread extends Thread {@Overridepublic void run() {......   // Critical services...boolean onlyCore = false;try {// 创建Activity Manager实例context = ActivityManagerService.main(factoryTest);.....// 将AMS注册到ServiceManager中(前面分析提到这一步是在创建PMS实例之后才进行注册)ActivityManagerService.setSystemProcess();} catch (RuntimeException e) {}}
}

2、如前所述,其是通过调用ActivityManagerService#main函数来创建AMS实例及AMS线程:

/** @path \frameworks\base\services\core\java\com\android\server\am\ActivityManagerService.java<span style="font-family: Arial, Helvetica, sans-serif;"> **/ </span>
public final class ActivityManagerService extends ActivityManagerNativeimplements Watchdog.Monitor, BatteryStatsImpl.BatteryCallback {static ActivityManagerService mSelf;  public static final Context main(int factoryTest) {/** 创建一个AThread线程 ,这里用来创建AMS实例*/AThread thr = new AThread();thr.start();   // 启动AMS线程synchronized (thr) {/** 这里运来判断AMS是否启动成功,失败则一直等待 **/while (thr.mService == null) {try {// 注意这里会wait,直至AThread中AMS创建完成,调用notiyAll方法才唤醒thr.wait();} catch (InterruptedException e) {}}}// 将AThread中创建的AMS实例赋值给m,再赋值给AMS静态变量mSelfActivityManagerService m = thr.mService;mSelf = m;/** AMS两个最重要核心——*  - ActivityStack:Activity的记录者与管理者,同时也为AMS管理系统运行情况提供了基础*  - ActivityTask**/ActivityThread at = ActivityThread.systemMain();mSystemThread = at;Context context = at.getSystemContext();context.setTheme(android.R.style.Theme_Holo);m.mContext = context;m.mFactoryTest = factoryTest;/** 创建一个ActivityStack对象 **/m.mMainStack = new ActivityStack(m, context, true); m.mBatteryStatsService.publish(context);m.mUsageStatsService.publish(context);synchronized (thr) {thr.mReady = true;// 唤醒AMS线程thr.notifyAll();}/*** 开始运行  ***/m.startRunning(null, null, null, null);return context;}static class AThread extends Thread {ActivityManagerService mService;boolean mReady = false;public AThread() {super("ActivityManager");}public void run() {/** 创建消息Loopr循环 **/Looper.prepare();android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_FOREGROUND);android.os.Process.setCanSelfBackground(false);/** 在这里创建AMS实例,用以作为系统中的Activity管理服务 **/ActivityManagerService m = new ActivityManagerService();synchronized (this) {mService = m;// 这里唤醒前面等待的线程notifyAll();}synchronized (this) {while (!mReady) {try {// 创建完成后wait等待,直至System线程将其唤醒wait();} catch (InterruptedException e) {}}}Looper.loop();}}
}

3、通过ActivityManagerService#setSystemProcess将AMS注册到ServiceManager中

/** @path \frameworks\base\services\java\com\android\server\am\ActivityManagerService.java **/
public static void setSystemProcess() {try {// mSelf是静态变量,即前面启动的AMS实例ActivityManagerService m = mSelf;/** 这里通过ServiceManager来注册各种服务,其中AMS服务的主体是第一个即"activity" **/ServiceManager.addService("activity", m, true);ServiceManager.addService("meminfo", new MemBinder(m));ServiceManager.addService("gfxinfo", new GraphicsBinder(m));ServiceManager.addService("dbinfo", new DbBinder(m));if (MONITOR_CPU_USAGE) {ServiceManager.addService("cpuinfo", new CpuBinder(m));}ServiceManager.addService("permission", new PermissionController(m));ApplicationInfo info =mSelf.mContext.getPackageManager().getApplicationInfo("android", STOCK_PM_FLAGS);mSystemThread.installSystemApplicationInfo(info);synchronized (mSelf) {ProcessRecord app = mSelf.newProcessRecordLocked(mSystemThread.getApplicationThread(), info,info.processName, false);app.persistent = true;app.pid = MY_PID;app.maxAdj = ProcessList.SYSTEM_ADJ;mSelf.mProcessNames.put(app.processName, app.uid, app);synchronized (mSelf.mPidsSelfLocked) {mSelf.mPidsSelfLocked.put(app.pid, app);}mSelf.updateLruProcessLocked(app, true);}} catch (PackageManager.NameNotFoundException e) {}
}

下面来看AMS是如何对Activity的启动产生影响的。
二、根Activity组件启动过程
1、Activity分类
Activity组件分为两种类型:一种是根Activity,一种是子Activity。
1)根Activity以快捷方式图标的形式显示在应用程序启动器上,它的启动过程代表了一个Android应用程序的启动过程。
根Activity在manifest.xml文件中的声明:
<activityandroid:name="com.loadingUI.LoadingActivity"android:label="@string/Loadinging_activity"><intent-filter><action android:name="android.intent.action.MAIN"/><category android:name="android.intent.category.LAUNCHER"/></intent-filter>
</activity>

 Launcher组件在启动时,会向PackageManagerService查询系统中所有“action”等于"action.MAIN"以及"category"等于"category.LAUNCHER"的Activity组件,为每一个Activity组件创建一个快捷图标。

(这也是为什么要将Launcher Activity的intent filter设为上述模式的原因)。
当想启动一个应用时,通过点击应用程序启动器Launcher界面上的图标来启动


2)子Activity由根Activity或者其他子Activity来启动,它们可能与启动它们的Activity运行在同一进程,也可能运行在不同的进程中。
启动方式分有显式启动与隐式启动两种。按照软件工程的角度,隐式启动可以减少Android应用程序组件之间的依赖耦合度。
2、根Activity(记为MainActivity)的总启动过程总结如下:
1)Launch组件向AMS发送启动根Activity组件的请求;
2)AMS响应Launch请求,将根Activity组件的信息保存下来,并向Launch发送一个进入中止状态的进程间通信请求。
3)Launch进入中止状态后,向AMS发送消息;
4)AMS接收到消息后,会检查启动根Activity的应用程序进程是否已经存在;若不存在,则启动该应用程序进程。
5)根Activity应用程序进程启动完成后,该进程会向AMS发送一个启动完成的进程间通信请求。
6)AMS将其保存的根Activity组件的信息发送给创建的根Activity应用程序进程,使得其继续启动根Activity组件。
3、根Activity详细启动过程
先来分析启动的前几步如下图:

启动根Activity一般是通过点击快捷图标,Launch组件启动Activity。先看其点击响应函数:
1)Launcher类:
public final class Launcher extends Activityimplements View.OnClickListener, OnLongClickListener, LauncherModel.Callbacks, View.OnTouchListener

1.2)点击响应函数Launcher#onClick:

/** \packages\apps\Launcher2\src\com\android\launcher2*/
public void onClick(View v) {...Object tag = v.getTag();if (tag instanceof ShortcutInfo) {// 这里组装启动intent的信息final Intent intent = ((ShortcutInfo) tag).intent;int[] pos = new int[2];v.getLocationOnScreen(pos);intent.setSourceBounds(new Rect(pos[0], pos[1],pos[0] + v.getWidth(), pos[1] + v.getHeight()));/** 看到通过这个函数用来启动一个应用的根Activity*/boolean success = startActivitySafely(v, intent, tag);if (success && v instanceof BubbleTextView) {mWaitingForResume = (BubbleTextView) v;mWaitingForResume.setStayPressed(true);}} .......
}

可以看到Launcher通过函数startActivitySafely来启动应用程序的根Activity;

1.2)Launcher#startActivitySafely方法:

/** \packages\apps\Launcher2\src\com\android\launcher2*/
boolean startActivitySafely(View v, Intent intent, Object tag) {boolean success = false;try {/** 函数很简单,只是对startActivity进行了安全封装*/success = startActivity(v, intent, tag);} catch (ActivityNotFoundException e) {...}return success;
}

可以看到startActivitySafely仅仅是对startActivity方法做了一个try-catch安全封装,用以安全启动,最终目的仍是调用startActivity来启动根Activity。

1.3)Launcher#startActivity函数:

boolean startActivity(View v, Intent intent, Object tag) {/*** 设置启动标志***/intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);try {// Only launch using the new animation if the shortcut has not opted out (this is a// private contract between launcher and may be ignored in the future).boolean useLaunchAnimation = (v != null) &&!intent.hasExtra(INTENT_EXTRA_IGNORE_LAUNCH_ANIMATION);UserHandle user = (UserHandle) intent.getParcelableExtra(ApplicationInfo.EXTRA_PROFILE);LauncherApps launcherApps = (LauncherApps)this.getSystemService(Context.LAUNCHER_APPS_SERVICE);// 当添加新的启动动画时,使用此启动方式if (useLaunchAnimation) {ActivityOptions opts = ActivityOptions.makeScaleUpAnimation(v, 0, 0,v.getMeasuredWidth(), v.getMeasuredHeight());if (user == null || user.equals(android.os.Process.myUserHandle())) {/** 注意这里将会调用父类的startActivity*/startActivity(intent, opts.toBundle());} else {launcherApps.startMainActivity(intent.getComponent(), user,intent.getSourceBounds(), opts.toBundle());}} else { // 默认的启动方式if (user == null || user.equals(android.os.Process.myUserHandle())) {/** 调用父类Activity的startActivity来启动 **/startActivity(intent);} else {launcherApps.startMainActivity(intent.getComponent(), user,intent.getSourceBounds(), null);}}return true;} catch (SecurityException e) {...}return false;
}

上面代码重要完成两个工作:

1> 设置Activity的启动标志位Intent.FLAG_ACTIVITY_NEW_TASK如果设置了此标志,这个activity将成为一个新task的历史堆栈中的第一个activity。这个task定义了一个原子组activities,用户可以对其进行移除。各种tasks可以移到前面或者后面;在一个特定的task中,所有的activities总是保持相同的顺序。当使用这个标志时,如果一个包含此activity的task已经运行了,新的activity不会启动;同时,当前的task将简单的被提到窗口最前面。查看FLAG_ACTIVITY_MULTIPLE_TASK可以禁止这个行为

2>  调用startActivity方法来继续启动根Activity组件;可以看到Launcher类中并未实现两个/一个参数的startActivity的方法,可知其在父类Activity中,这里将会调用Activity.startActivity()函数;

4、Activity类
熟悉的Activity类:
    public class Activity extends ContextThemeWrapperimplements LayoutInflater.Factory2,Window.Callback, KeyEvent.Callback,OnCreateContextMenuListener, ComponentCallbacks2,Window.OnWindowDismissedCallback

1、前面调用到的Activity#startActivity方法:
/** \frameworks\base\core\java\android\app\Activity.java*/
@Override
public void startActivity(Intent intent) {this.startActivity(intent, null);
}@Override
public void startActivity(Intent intent, @Nullable Bundle options) {if (options != null) {startActivityForResult(intent, -1, options);} else {// Note we want to go through this call for compatibility with// applications that may have overridden the method.startActivityForResult(intent, -1);}
}

最终将会调用startActivityForResult来执行函数,第二个参数设为-1表示不需要知道最终的执行结果。

2、来分析Activity#startActivityForResult方法:

public void startActivityForResult(Intent intent, int requestCode) {startActivityForResult(intent, requestCode, null);
}public void startActivityForResult(Intent intent, int requestCode, @Nullable Bundle options) {/** mParent在调用attach函数时传入*/if (mParent == null) {/** Instrumentation类用以监控应用程序与系统之间的交互,*  这里通过其execStartActivity来启动Activity组件**/Instrumentation.ActivityResult ar =mInstrumentation.execStartActivity(this, mMainThread.getApplicationThread(), mToken, this, intent, requestCode, options);if (ar != null) {mMainThread.sendActivityResult(mToken, mEmbeddedID, requestCode, ar.getResultCode(), ar.getResultData());}/** 这个本情况下不需要*/if (requestCode >= 0) {mStartedActivity = true;}/** 有关界面加载SurfaceFlinger*/final View decor = mWindow != null ? mWindow.peekDecorView() : null;if (decor != null) {decor.cancelPendingInputEvents();}} else {...}if (options != null && !isTopOfTask()) {mActivityTransitionState.startExitOutTransition(this, options);}
}

注意execStartActivity中的传参param:

1)mToken:
private IBinder mToken;

mToken是一个Binder代理对象,指向了ActivityManagerService中一个类型为ActivityRecord的Binder本地对象。每一个已经启动的Activity组件在AMS(ActivityManagerService)中都存在一个对应的ActivityRecord对象,用来维护对应的Activity组件的运行状态及信息。

在调用execStartActivity函数时,传入实参mToken,使得可以将其传递给AMS,以便AMS接下来可以获得Launcher组件的详细信息。

前面提到调用execStartActivity方法,该方法是Instrumentation类中的方法。
5、Instrumentation类

Instrumentation是执行application instrumentation代码的基类。当应用程序运行的时候instrumentation处于开启,Instrumentation将在任何应用程序运行前初始化,可以通过它监测系统与应用程序之间的交互。Instrumentation implementation通过的AndroidManifest.xml中的<instrumentation>标签进行描述。

Instrumentation似乎有些类似与window中的“钩子(Hook)函数”,在系统与应用程序之间安装了个“窃听器”。

1、Instrumentation#execStartActivity函数:

先看调用传参:
Instrumentation.ActivityResult ar =mInstrumentation.execStartActivity(this, mMainThread.getApplicationThread(), mToken, this, intent, requestCode, options);

函数源码:

/** \frameworks\base\core\java\android\app\Instrumentation.java*/
public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target,Intent intent, int requestCode, Bundle options) {IApplicationThread whoThread = (IApplicationThread) contextThread;/** mActivityMonitors定义:List<ActivityMonitor> mActivityMonitors;*  ActivityMoniter:有关特定的Intent的监视。*  一个ActivityMoniter类的实例通过函数addMonitor*  (Instrumentation.ActivityMonitor)添加到当前*  instrumentation中,一旦添加后,每当启动一个新的Activity,*  ActivityMoniter就会检测,如果匹配,其hit count计数更新*  等其他操作。一个ActivityMonitor也可以用来寻找一个Activity,*  通过waitForActivity()方法,这个函数将返直到匹配的活动被创建。*/// 这里与主线无关,可以不用关心if (mActivityMonitors != null) {synchronized (mSync) {final int N = mActivityMonitors.size();for (int i=0; i<N; i++) {final ActivityMonitor am = mActivityMonitors.get(i);if (am.match(who, null, intent)) {am.mHits++;if (am.isBlocking()) {return requestCode >= 0 ? am.getResult() : null;}break;}}}}/** 主要的代码从这里开始**/try {intent.migrateExtraStreamToClipData();intent.prepareToLeaveProcess();// 主要的执行函数在这里,调用了ActivityManagerNative中的startActivityint result = ActivityManagerNative.getDefault().startActivity(whoThread, who.getBasePackageName(), intent,intent.resolveTypeIfNeeded(who.getContentResolver()),token, target != null ? target.mEmbeddedID : null,requestCode, 0, null, options);checkStartActivityResult(result, intent);} catch (RemoteException e) {}return null;
}

上述代码中最重要的作用是调用了ActivityManagerNative.getDefault().startActivity方法;getDefault用以获取AMS的一个代理对象ActivityManagerNative,接着再调用它的成员函数startActivity来通知AMS将有一个Activity组件启动。


6、ActivityManagerNative类:
看一下其继承关系:
   /** \frameworks\base\core\java\android\app\ActivityManagerNative.java*/public abstract class ActivityManagerNative extends Binder implements IActivityManager

1、ActivityManagerNative#getDefault函数:

    /** \frameworks\base\core\java\android\app\ActivityManagerNative.java*/static public IActivityManager getDefault() {// 单例类Singleton中的函数,get是Singleton类中的方法,见下面附Ireturn gDefault.get();}// Singleton为单例模式的实现,是个抽象类,见附一private static final Singleton<IActivityManager> gDefault = new Singleton<IActivityManager>() {protected IActivityManager create() {// 前面AMS分析中提到的AMS的主要服务"activity",通过SM获取IBinder b = ServiceManager.getService("activity");...IActivityManager am = asInterface(b);...return am;}};static public IActivityManager asInterface(IBinder obj) {if (obj == null) {return null;}IActivityManager in = (IActivityManager)obj.queryLocalInterface(descriptor);if (in != null) {return in;}return new ActivityManagerProxy(obj);}

可以看到当调用getDefault函数时,会返回gDefault.get(),返回一个IActivityManager类;

附I、Singleton类

再看一下gDefault的定义,gDefault是个泛型类Singleton的对象实例,Singleton是个单例模式实现类,它是一个抽象类,它的定义如下:
    /*** Singleton helper class for lazily initialization.* Modeled after frameworks/base/include/utils/Singleton.h*/public abstract class Singleton<T> {private T mInstance;protected abstract T create();public final T get() {synchronized (this) {if (mInstance == null) {mInstance = create();}return mInstance;}}}

可以看到gDefault的定义是Singleton<IActivityManager>,调用get函数则是返回IActivityManager的具体实例对象。


在定义gDefault时,需要重写abstract函数create();在create函数中,可以看到它通过ServiceManager获取到一个服务名为"activity"的服务代理对象即一个引用了ActivtyManagerService的代理对象。

而后通过asInterface将其封装成一个ActivityManagerProxy代理对象
因此调用getDefault最终获取到的返回结果时ActivityManagerService的代理对象

回到前面的步骤可知,ActivityManagerNative.getDefault().startActivity方法最终调用的是ActivityManagerProxy.startActivity方法。

2)ActivityManagerProxy类:(ActivityManagerNative的内部类)
/** \frameworks\base\core\java\android\app\ActivityManagerNative.java*/
class ActivityManagerProxy implements IActivityManager {public ActivityManagerProxy(IBinder remote) {mRemote = remote;}public IBinder asBinder() {return mRemote;}public int startActivity(IApplicationThread caller,String callingPackage, Intent intent, String resolvedType,IBinder resultTo, String resultWho, int requestCode,int startFlags, ProfilerInfo profilerInfo, Bundle options)throws RemoteException {/*** 写入启动Activity的信息到Parcel对象data中 **/Parcel data = Parcel.obtain();Parcel reply = Parcel.obtain();data.writeInterfaceToken(IActivityManager.descriptor);data.writeStrongBinder(caller != null ? caller.asBinder() : null);data.writeString(callingPackage);intent.writeToParcel(data, 0);data.writeString(resolvedType);data.writeStrongBinder(resultTo);data.writeString(resultWho);data.writeInt(requestCode);data.writeInt(startFlags);if (profilerInfo != null) {data.writeInt(1);profilerInfo.writeToParcel(data,Parcelable.PARCELABLE_WRITE_RETURN_VALUE);} else {data.writeInt(0);}if (options != null) {data.writeInt(1);options.writeToParcel(data, 0);} else {data.writeInt(0);}/** 通过Binder进行进程间通信,通过mRemote来向AMS发送START_ACTIVITY_TRANSACTION类型*  的进程间请求 **/mRemote.transact(START_ACTIVITY_TRANSACTION, data, reply, 0);reply.readException();int result = reply.readInt();reply.recycle();data.recycle();return result;}
}

和通常的Binder通信机制相同,这里使用ActivityManagerProxy将Activity组件的信息封装到一个Parcel对象中,通过Binder机制传递给AMS,发起进程间通信请求。接下来的启动操作则会在AMS中进行。

Activity启动过程(一)AMS相关推荐

  1. activity启动流程_以AMS视角看Activity启动过程

    原文作者:Levi_wayne 原文地址:blog.csdn.net/u012551754/article/details/78822782 特别声明:本文转载自网络,版权归作者所有,如有侵权请联系删 ...

  2. activity 生命周期_死磕Android_App 启动过程(含 Activity 启动过程)

    1. 前言 Activity是日常开发中最常用的组件,系统给我们做了很多很多的封装,让我们平时用起来特别简单,很顺畅.但是你有没有想过,系统内部是如何启动一个Activity的呢?Activity对象 ...

  3. 【Android 插件化】Hook 插件化框架 ( 从 Hook 应用角度分析 Activity 启动流程 二 | AMS 进程相关源码 | 主进程相关源码 )

    Android 插件化系列文章目录 [Android 插件化]插件化简介 ( 组件化与插件化 ) [Android 插件化]插件化原理 ( JVM 内存数据 | 类加载流程 ) [Android 插件 ...

  4. Android系统(117)---Activity启动过程

    Activity启动过程 ###一些基本的概念 ActivityManagerServices,简称AMS,服务端对象,负责系统中所有Activity的生命周期 ActivityThread,App的 ...

  5. Android深入四大组件(五)Android8.0 根Activity启动过程(后篇)

    前言 在几个月前我写了Android深入四大组件(一)应用程序启动过程(前篇)和Android深入四大组件(一)应用程序启动过程(后篇)这两篇文章,它们都是基于Android 7.0,当我开始阅读An ...

  6. 死磕Android_App 启动过程(含 Activity 启动过程)

    1. 前言 Activity是日常开发中最常用的组件,系统给我们做了很多很多的封装,让我们平时用起来特别简单,很顺畅.但是你有没有想过,系统内部是如何启动一个Activity的呢?Activity对象 ...

  7. Activity启动过程详解(Android P)

    本章我们来分析Activity的启动过程. 我们知道,Activity可以通过两种方式启动:一种是点击应用程序图标,Launcher会启动主Activity:另一种是在应用程序内部,调用startAc ...

  8. Android 面试必备 - 系统、App、Activity 启动过程

    前言 最近准备更新 Android 面试必备基础知识系列,有兴趣的可以关注我的微信公众号 stormjun94,有更新时,第一时间会在微信公众号上面发布,同时,也会同步在 GitHub 上面更新,如果 ...

  9. Android深入四大组件(六)Android8.0 根Activity启动过程(前篇)

    相关文章 Android深入四大组件系列 Android系统启动系列 Android应用程序进程系列 Android深入解析AMS系列 前言 在几个月前我写了Android深入四大组件(一)应用程序启 ...

  10. Android深入四大组件(七)Android8.0 根Activity启动过程(后篇)

    相关文章 Android深入四大组件系列 Android系统启动系列 Android应用程序进程系列 Android深入解析AMS系列 前言 在几个月前我写了Android深入四大组件(一)应用程序启 ...

最新文章

  1. 记一次mpvue开发完整小程序相关笔记
  2. 卡地亚搜索引擎_「AF厂卡地亚猎豹」网站SEO优化新方向
  3. android 自定义模板下载,android studio 自定义模板
  4. linux 线程退出 signal,Linux signal 那些事儿 (3)
  5. WPF开发的实用小工具 - 快捷悬浮菜单
  6. 时尚排毒法可用性到底多大? - 生活至上,美容至尚!
  7. 【万字干货】OpenMetric与时序数据库存储模型分析
  8. 算法训练 字符串编辑(java)
  9. centos6.8安装telnet
  10. OSChina 周一乱弹 —— 最无法理解的程序员行为
  11. windows下python访问ipv6报错
  12. 从0开始,设计研发一个全功能通用大数据系统
  13. 软考高级《信息系统项目管理师》(简称高项)考证经验(满满的干货)
  14. matlab 卷积改变步长,转载“MATLAB卷积函数改进”
  15. 对路径“C:\Program Files (x86)\gwssi\CPC客户端\CheckWord.xml”的访问被拒绝。
  16. 入门物联网需要服务器
  17. 微信公众号开发者模式入门
  18. 开篇:内容提要 (《蓝调口琴指南》名作拙译)
  19. 【CF802O】April Fools‘ Problem (hard)(wqs二分,模拟费用流,老鼠进洞)
  20. 信息与计算机科学讲座,【创新创业 计科在行动】2015级信息与计算科学专业大学生创新讲座、专业教育讲座暨学术前沿讲座专题报道...

热门文章

  1. django+layui表格数据管理
  2. c51单片机矩阵键盘1602计算器_基于AT89C51单片机的十进制计算器系统设计
  3. cad填充密度怎么调整_CAD填充比例调好了,填充物数量怎么调,就是密度怎么调?...
  4. 互联网金融网络借贷系统架构
  5. daemontools安装和使用
  6. p6spy mysql8_P6Spy配置使用
  7. 计算机日期函数公式大全,Excel技巧: 根据日期汇总月份的计算公式
  8. 免费好用的内网穿透 端口映射工具 实现一键远程 外网访问内网
  9. Java、JSP网上音像管理系统的设计与实现
  10. C#读写西门子PLC数据