在Android中,进程只是一个运行组件的容器,当系统需要运行一个组件时,启动包含该组件的进程,当组件不在使用是,进程会被关闭;
在AMS中,还必须管理和调度进程;AMS对进程的管理,主要体现在两个方面:一个是动态调整进程在mLruProcesses列表的位置,而是调整进程的oom_adj的值,这两项调整和系统进程自动内存回收有关;当系统内存不足时,系统会关闭一些进程来释放内存;
系统主要根据进程的oom_adj值来挑选要杀死的进程,oom_adj值越大表示进程更可能被杀死;

1.创建进程:

执行的是AMS的addAppLocked方法:
final ProcessRecord addAppLocked(ApplicationInfo info, String customProcess, boolean isolated,boolean disableHiddenApiChecks, String abiOverride) {ProcessRecord app;if (!isolated) {app = getProcessRecordLocked(customProcess != null ? customProcess : info.processName,info.uid, true);} else {app = null;}if (app == null) {app = newProcessRecordLocked(info, customProcess, isolated, 0);updateLruProcessLocked(app, false, null);updateOomAdjLocked();}// This package really, really can not be stopped.try {AppGlobals.getPackageManager().setPackageStoppedState(info.packageName, false, UserHandle.getUserId(app.uid));} catch (RemoteException e) {} catch (IllegalArgumentException e) {Slog.w(TAG, "Failed trying to unstop package "+ info.packageName + ": " + e);}if ((info.flags & PERSISTENT_MASK) == PERSISTENT_MASK) {app.persistent = true;app.maxAdj = ProcessList.PERSISTENT_PROC_ADJ;}if (app.thread == null && mPersistentStartingProcesses.indexOf(app) < 0) {mPersistentStartingProcesses.add(app);startProcessLocked(app, "added application",customProcess != null ? customProcess : app.processName, disableHiddenApiChecks,abiOverride);}return app;
}
addAppLocked会根据isolated来决定是否重启一个新进程,如果isolated为TRUE,即使系统中可能已经有一个同名的进程存在,也会在创建一个进程;getProcessRecordLocked方法用于在当前运行的进程列表查询进程;newProcessRecordLocked方法用来创建一个ProcessRecord的数据结构,这两个函数很简单,updateLruProcessLocked方法用来更新运行中的进程状态,updateoomAdjLocked方法用来更新进程的优先级,这两个方法是Process管理的核心,startProcessLocked是启动进程的方法;

2.启动进程:

执行startProcessLocked方法:
private final boolean startProcessLocked(ProcessRecord app, String hostingType,String hostingNameStr, boolean disableHiddenApiChecks, String abiOverride) {if (app.pendingStart) {return true;}long startTime = SystemClock.elapsedRealtime();if (app.pid > 0 && app.pid != MY_PID) {checkTime(startTime, "startProcess: removing from pids map");synchronized (mPidsSelfLocked) {mPidsSelfLocked.remove(app.pid);mHandler.removeMessages(PROC_START_TIMEOUT_MSG, app);}checkTime(startTime, "startProcess: done removing from pids map");app.setPid(0);app.startSeq = 0;}if (DEBUG_PROCESSES && mProcessesOnHold.contains(app)) Slog.v(TAG_PROCESSES,"startProcessLocked removing on hold: " + app);mProcessesOnHold.remove(app);checkTime(startTime, "startProcess: starting to update cpu stats");updateCpuStats();checkTime(startTime, "startProcess: done updating cpu stats");try {try {final int userId = UserHandle.getUserId(app.uid);AppGlobals.getPackageManager().checkPackageStartable(app.info.packageName, userId);} catch (RemoteException e) {throw e.rethrowAsRuntimeException();}int uid = app.uid;int[] gids = null;int mountExternal = Zygote.MOUNT_EXTERNAL_NONE;if (!app.isolated) {int[] permGids = null;try {checkTime(startTime, "startProcess: getting gids from package manager");final IPackageManager pm = AppGlobals.getPackageManager();permGids = pm.getPackageGids(app.info.packageName,MATCH_DEBUG_TRIAGED_MISSING, app.userId);StorageManagerInternal storageManagerInternal = LocalServices.getService(StorageManagerInternal.class);mountExternal = storageManagerInternal.getExternalStorageMountMode(uid,app.info.packageName);} catch (RemoteException e) {throw e.rethrowAsRuntimeException();}/** Add shared application and profile GIDs so applications can share some* resources like shared libraries and access user-wide resources*/if (ArrayUtils.isEmpty(permGids)) {gids = new int[3];} else {gids = new int[permGids.length + 3];System.arraycopy(permGids, 0, gids, 3, permGids.length);}gids[0] = UserHandle.getSharedAppGid(UserHandle.getAppId(uid));gids[1] = UserHandle.getCacheAppGid(UserHandle.getAppId(uid));gids[2] = UserHandle.getUserGid(UserHandle.getUserId(uid));// Replace any invalid GIDsif (gids[0] == UserHandle.ERR_GID) gids[0] = gids[2];if (gids[1] == UserHandle.ERR_GID) gids[1] = gids[2];}checkTime(startTime, "startProcess: building args");if (mFactoryTest != FactoryTest.FACTORY_TEST_OFF) {if (mFactoryTest == FactoryTest.FACTORY_TEST_LOW_LEVEL&& mTopComponent != null&& app.processName.equals(mTopComponent.getPackageName())) {uid = 0;}if (mFactoryTest == FactoryTest.FACTORY_TEST_HIGH_LEVEL&& (app.info.flags&ApplicationInfo.FLAG_FACTORY_TEST) != 0) {uid = 0;}}int runtimeFlags = 0;if ((app.info.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) {runtimeFlags |= Zygote.DEBUG_ENABLE_JDWP;runtimeFlags |= Zygote.DEBUG_JAVA_DEBUGGABLE;// Also turn on CheckJNI for debuggable apps. It's quite// awkward to turn on otherwise.runtimeFlags |= Zygote.DEBUG_ENABLE_CHECKJNI;}// Run the app in safe mode if its manifest requests so or the// system is booted in safe mode.if ((app.info.flags & ApplicationInfo.FLAG_VM_SAFE_MODE) != 0 ||mSafeMode == true) {runtimeFlags |= Zygote.DEBUG_ENABLE_SAFEMODE;}if ("1".equals(SystemProperties.get("debug.checkjni"))) {runtimeFlags |= Zygote.DEBUG_ENABLE_CHECKJNI;}String genDebugInfoProperty = SystemProperties.get("debug.generate-debug-info");if ("1".equals(genDebugInfoProperty) || "true".equals(genDebugInfoProperty)) {runtimeFlags |= Zygote.DEBUG_GENERATE_DEBUG_INFO;}String genMiniDebugInfoProperty = SystemProperties.get("dalvik.vm.minidebuginfo");if ("1".equals(genMiniDebugInfoProperty) || "true".equals(genMiniDebugInfoProperty)) {runtimeFlags |= Zygote.DEBUG_GENERATE_MINI_DEBUG_INFO;}if ("1".equals(SystemProperties.get("debug.jni.logging"))) {runtimeFlags |= Zygote.DEBUG_ENABLE_JNI_LOGGING;}if ("1".equals(SystemProperties.get("debug.assert"))) {runtimeFlags |= Zygote.DEBUG_ENABLE_ASSERT;}if (mNativeDebuggingApp != null && mNativeDebuggingApp.equals(app.processName)) {// Enable all debug flags required by the native debugger.runtimeFlags |= Zygote.DEBUG_ALWAYS_JIT;          // Don't interpret anythingruntimeFlags |= Zygote.DEBUG_GENERATE_DEBUG_INFO; // Generate debug inforuntimeFlags |= Zygote.DEBUG_NATIVE_DEBUGGABLE;   // Disbale optimizationsmNativeDebuggingApp = null;}if (app.info.isPrivilegedApp() &&DexManager.isPackageSelectedToRunOob(app.pkgList.keySet())) {runtimeFlags |= Zygote.ONLY_USE_SYSTEM_OAT_FILES;}if (!disableHiddenApiChecks && !mHiddenApiBlacklist.isDisabled()) {app.info.maybeUpdateHiddenApiEnforcementPolicy(mHiddenApiBlacklist.getPolicyForPrePApps(),mHiddenApiBlacklist.getPolicyForPApps());@HiddenApiEnforcementPolicy int policy =app.info.getHiddenApiEnforcementPolicy();int policyBits = (policy << Zygote.API_ENFORCEMENT_POLICY_SHIFT);if ((policyBits & Zygote.API_ENFORCEMENT_POLICY_MASK) != policyBits) {throw new IllegalStateException("Invalid API policy: " + policy);}runtimeFlags |= policyBits;}String invokeWith = null;if ((app.info.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) {// Debuggable apps may include a wrapper script with their library directory.String wrapperFileName = app.info.nativeLibraryDir + "/wrap.sh";StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();try {if (new File(wrapperFileName).exists()) {invokeWith = "/system/bin/logwrapper " + wrapperFileName;}} finally {StrictMode.setThreadPolicy(oldPolicy);}}String requiredAbi = (abiOverride != null) ? abiOverride : app.info.primaryCpuAbi;if (requiredAbi == null) {requiredAbi = Build.SUPPORTED_ABIS[0];}String instructionSet = null;if (app.info.primaryCpuAbi != null) {instructionSet = VMRuntime.getInstructionSet(app.info.primaryCpuAbi);}app.gids = gids;app.requiredAbi = requiredAbi;app.instructionSet = instructionSet;// the per-user SELinux context must be setif (TextUtils.isEmpty(app.info.seInfoUser)) {Slog.wtf(TAG, "SELinux tag not defined",new IllegalStateException("SELinux tag not defined for "+ app.info.packageName + " (uid " + app.uid + ")"));}final String seInfo = app.info.seInfo+ (TextUtils.isEmpty(app.info.seInfoUser) ? "" : app.info.seInfoUser);// Start the process.  It will either succeed and return a result containing// the PID of the new process, or else throw a RuntimeException.final String entryPoint = "android.app.ActivityThread";return startProcessLocked(hostingType, hostingNameStr, entryPoint, app, uid, gids,runtimeFlags, mountExternal, seInfo, requiredAbi, instructionSet, invokeWith,startTime);} catch (RuntimeException e) {Slog.e(TAG, "Failure starting process " + app.processName, e);// Something went very wrong while trying to start this process; one// common case is when the package is frozen due to an active// upgrade. To recover, clean up any active bookkeeping related to// starting this process. (We already invoked this method once when// the package was initially frozen through KILL_APPLICATION_MSG, so// it doesn't hurt to use it again.)forceStopPackageLocked(app.info.packageName, UserHandle.getAppId(app.uid), false,false, true, false, false, UserHandle.getUserId(app.userId), "start failure");return false;}
}
startProcessLocked方法的流程是:准备启动应用的参数后,调用Process类的start方法启动进程;启动进程后AMS给自己发一个PROC_START_TIMEOUT_MSG消息,这个消息是用来防止进程启动时间超时;如果时间到了,但是进程还没有启动完成;AMS将弹出发生ANR的对话框;

3.调整进程的位置:

AMS的代码经常调用updateLruProcessLocked方法来调整某个进程在mLruProcesses列表中的位置;每当进程中的Activity或Service发生变化时,意味着进程发生了活动,因此调用这个方法将该进程调整到尽可能高的位置,同时还拥要更新关联进程的位置;在mLruProcesses列表中,最近活动过得进程总是位于最高位置,同时拥有Activity的进程位置,总是高于只有Service的进程的位置;
AMS的成员变量mLruProcessActivityStart和mLruProcessServiceStart分别指向列表中位置最高的,带有Activity进程和没有Activity的进程:
final void updateLruProcessLocked(ProcessRecord app, boolean activityChange,ProcessRecord client) {final boolean hasActivity = app.activities.size() > 0 || app.hasClientActivities|| app.treatLikeActivity || app.recentTasks.size() > 0;final boolean hasService = false; // not impl yet. app.services.size() > 0;if (!activityChange && hasActivity) {// The process has activities, so we are only allowing activity-based adjustments// to move it.  It should be kept in the front of the list with other// processes that have activities, and we don't want those to change their// order except due to activity operations.return;}mLruSeq++;final long now = SystemClock.uptimeMillis();app.lastActivityTime = now;// First a quick reject: if the app is already at the position we will// put it, then there is nothing to do.if (hasActivity) {final int N = mLruProcesses.size();if (N > 0 && mLruProcesses.get(N-1) == app) {if (DEBUG_LRU) Slog.d(TAG_LRU, "Not moving, already top activity: " + app);return;}} else {if (mLruProcessServiceStart > 0&& mLruProcesses.get(mLruProcessServiceStart-1) == app) {if (DEBUG_LRU) Slog.d(TAG_LRU, "Not moving, already top other: " + app);return;}}int lrui = mLruProcesses.lastIndexOf(app);if (app.persistent && lrui >= 0) {// We don't care about the position of persistent processes, as long as// they are in the list.if (DEBUG_LRU) Slog.d(TAG_LRU, "Not moving, persistent: " + app);return;}/* In progress: compute new position first, so we can avoid doing workif the process is not actually going to move.  Not yet working.int addIndex;int nextIndex;boolean inActivity = false, inService = false;if (hasActivity) {// Process has activities, put it at the very tipsy-top.addIndex = mLruProcesses.size();nextIndex = mLruProcessServiceStart;inActivity = true;} else if (hasService) {// Process has services, put it at the top of the service list.addIndex = mLruProcessActivityStart;nextIndex = mLruProcessServiceStart;inActivity = true;inService = true;} else  {// Process not otherwise of interest, it goes to the top of the non-service area.addIndex = mLruProcessServiceStart;if (client != null) {int clientIndex = mLruProcesses.lastIndexOf(client);if (clientIndex < 0) Slog.d(TAG, "Unknown client " + client + " when updating "+ app);if (clientIndex >= 0 && addIndex > clientIndex) {addIndex = clientIndex;}}nextIndex = addIndex > 0 ? addIndex-1 : addIndex;}Slog.d(TAG, "Update LRU at " + lrui + " to " + addIndex + " (act="+ mLruProcessActivityStart + "): " + app);*/if (lrui >= 0) {if (lrui < mLruProcessActivityStart) {mLruProcessActivityStart--;}if (lrui < mLruProcessServiceStart) {mLruProcessServiceStart--;}/*if (addIndex > lrui) {addIndex--;}if (nextIndex > lrui) {nextIndex--;}*/mLruProcesses.remove(lrui);}/*mLruProcesses.add(addIndex, app);if (inActivity) {mLruProcessActivityStart++;}if (inService) {mLruProcessActivityStart++;}*/int nextIndex;if (hasActivity) {final int N = mLruProcesses.size();if ((app.activities.size() == 0 || app.recentTasks.size() > 0)&& mLruProcessActivityStart < (N - 1)) {// Process doesn't have activities, but has clients with// activities...  move it up, but one below the top (the top// should always have a real activity).if (DEBUG_LRU) Slog.d(TAG_LRU,"Adding to second-top of LRU activity list: " + app);mLruProcesses.add(N - 1, app);// To keep it from spamming the LRU list (by making a bunch of clients),// we will push down any other entries owned by the app.final int uid = app.info.uid;for (int i = N - 2; i > mLruProcessActivityStart; i--) {ProcessRecord subProc = mLruProcesses.get(i);if (subProc.info.uid == uid) {// We want to push this one down the list.  If the process after// it is for the same uid, however, don't do so, because we don't// want them internally to be re-ordered.if (mLruProcesses.get(i - 1).info.uid != uid) {if (DEBUG_LRU) Slog.d(TAG_LRU,"Pushing uid " + uid + " swapping at " + i + ": "+ mLruProcesses.get(i) + " : " + mLruProcesses.get(i - 1));ProcessRecord tmp = mLruProcesses.get(i);mLruProcesses.set(i, mLruProcesses.get(i - 1));mLruProcesses.set(i - 1, tmp);i--;}} else {// A gap, we can stop here.break;}}} else {// Process has activities, put it at the very tipsy-top.if (DEBUG_LRU) Slog.d(TAG_LRU, "Adding to top of LRU activity list: " + app);mLruProcesses.add(app);}nextIndex = mLruProcessServiceStart;} else if (hasService) {// Process has services, put it at the top of the service list.if (DEBUG_LRU) Slog.d(TAG_LRU, "Adding to top of LRU service list: " + app);mLruProcesses.add(mLruProcessActivityStart, app);nextIndex = mLruProcessServiceStart;mLruProcessActivityStart++;} else  {// Process not otherwise of interest, it goes to the top of the non-service area.int index = mLruProcessServiceStart;if (client != null) {// If there is a client, don't allow the process to be moved up higher// in the list than that client.int clientIndex = mLruProcesses.lastIndexOf(client);if (DEBUG_LRU && clientIndex < 0) Slog.d(TAG_LRU, "Unknown client " + client+ " when updating " + app);if (clientIndex <= lrui) {// Don't allow the client index restriction to push it down farther in the// list than it already is.clientIndex = lrui;}if (clientIndex >= 0 && index > clientIndex) {index = clientIndex;}}if (DEBUG_LRU) Slog.d(TAG_LRU, "Adding at " + index + " of LRU list: " + app);mLruProcesses.add(index, app);nextIndex = index-1;mLruProcessActivityStart++;mLruProcessServiceStart++;}// If the app is currently using a content provider or service,// bump those processes as well.for (int j=app.connections.size()-1; j>=0; j--) {ConnectionRecord cr = app.connections.valueAt(j);if (cr.binding != null && !cr.serviceDead && cr.binding.service != null&& cr.binding.service.app != null&& cr.binding.service.app.lruSeq != mLruSeq&& !cr.binding.service.app.persistent) {nextIndex = updateLruProcessInternalLocked(cr.binding.service.app, now, nextIndex,"service connection", cr, app);}}for (int j=app.conProviders.size()-1; j>=0; j--) {ContentProviderRecord cpr = app.conProviders.get(j).provider;if (cpr.proc != null && cpr.proc.lruSeq != mLruSeq && !cpr.proc.persistent) {nextIndex = updateLruProcessInternalLocked(cpr.proc, now, nextIndex,"provider reference", cpr, app);}}
}
updateLruProcessLocked方法中调整进程很重要的一个依据是进程中有没有Activity,除了进程本身存在Activity对象外,如果和进程中运行的Service相关联的进程中有Activity,该进程也算是用于Activity进程;这个调整目的是为了将来杀死进程释放内存做准备,如果一个进程的关联进程有Activity对象存在,那么它的重要性也和真正拥有Activity对象的进程相当,如果杀死它,将导致另一个进程出现严重错误;Activity用来显示UI,关系着用户得体验,因此Android尽量不关闭运行Activity组件的进程;
如果一个进程拥有Activity,那么通常把它插入到队列的最高位置,否则,只会把它放到所有没有Activity的进程前面,这个位置正是变量mLruProcessServiceStart所指向的;
调整某个进程的位置之后,还是调整合该进程的关联进程的位置,进程的关联进程有两种类型:一种是绑定了本进程服务的进程,另一种是连接了本进程的ContentProvider的进程;如果这些进程本身有Activity是不会调整的,需要调整的是那些没有Activity的进程,在updateLruProcessInternalLocked方法中会执行这种调整,但是能够调整到最高位置也就是mLruProcessServiceStart指向的位置;

4.调整进程的oom_adj值:

AMS中调整进程oom_adj值的方法是updateOomAdjLocked方法:
private final boolean updateOomAdjLocked(ProcessRecord app, int cachedAdj,ProcessRecord TOP_APP, boolean doingAll, long now) {if (app.thread == null) {return false;}computeOomAdjLocked(app, cachedAdj, TOP_APP, doingAll, now);return applyOomAdjLocked(app, doingAll, now, SystemClock.elapsedRealtime());
}
updateOomAdjLocked方法中通过调用computeoomAdjlocked方法来计算进程的oom_adj值,则表明该进程属于“cached”进程或空进程,updateOomAdjLocked方法将会为该进程分配oom_adj的值,如果用来表示进程状态的变量curProcState的值为PROCESS_STATE_CACHED_ACTIVITY或者PROCESS_STATE_CACHED_ACTIVITY_CLIENT,说明进程是cached进程,否则是空进程;

参考文档:
《深入理解Android5.0系统》

Android Process管理(进程管理) 详解相关推荐

  1. Linux进程管理工具 Supervisor详解

    介绍 Supervisor安装与配置(linux/unix进程管理工具) Supervisor(http://supervisord.org)是用Python开发的一个client/server服务, ...

  2. linux看不到进程管理,关于Linux下进程的详解【进程查看与管理】

    关于Linux下进程的详解[进程查看与管理] 一.关于进程 进程: 已经启动的可执行程序的运行实力 进程的组成:一个进程包含内核中的一部分地址空间和一系列数据结构.其中地址空间是内核标记的一部分内存以 ...

  3. 【云原生之k8s】k8s管理工具kubectl详解

    [云原生之k8s]k8s管理工具kubectl详解 前言 一.陈述式管理 (1)陈述式资源管理方法 (2)k8s相关信息查看 ①查看版本信息 ②查看节点信息 ③查看资源对象简写 ④查看集群信息 ⑤配置 ...

  4. Kubernetes二进制集群部署+Web管理界面+kubectl 命令管理+YAML文件详解(集合)

    Kubernetes---- 二进制集群部署(ETCD集群+Flannel网络) Kubernetes----单节点部署 Kubernetes----双master节点二进制部署 Kubernetes ...

  5. python sqlsever 时间_Python sqlalchemy时间戳及密码管理实现代码详解

    一.时间戳 实际开发中,我们一般希望create_time和update_time这两个属性能自动设置,所以在建表的时候需要在model上特殊处理一下: from sqlalchemy.sql imp ...

  6. ElasticSearch最全详细使用教程:入门、索引管理、映射详解、索引别名、分词器、文档管理、路由、搜索详解...

    墨墨导读:之前我们分享了ElasticSearch最全详细使用教程:入门.索引管理.映射详解,本文详细介绍ElasticSearch的索引别名.分词器.文档管理.路由.搜索详解. 一.索引别名 1. ...

  7. ElasticSearch最全详细使用教程:入门、索引管理、映射详解

    墨墨导读:本文介绍了ElasticSearch的必备知识:从入门.索引管理到映射详解. 一.快速入门 1. 查看集群的健康状况http://localhost:9200/_cat http://loc ...

  8. Linux服务器,服务管理--systemctl命令详解,设置开机自启动

    Linux服务器,服务管理--systemctl命令详解,设置开机自启动 syetemclt就是service和chkconfig这两个命令的整合,在CentOS 7就开始被使用了. 摘要: syst ...

  9. 系统批量运维管理器Fabric详解

    系统批量运维管理器Fabric详解 Fabrici 是基于python现实的SSH命令行工具,简化了SSH的应用程序部署及系统管理任务,它提供了系统基础的操作组件,可以实现本地或远程shell命令,包 ...

  10. elasticsearch最全详细使用教程:入门、索引管理、映射详解、索引别名、分词器、文档管理、路由、搜索详解

    一.快速入门 1. 查看集群的健康状况 http://localhost:9200/_cat http://localhost:9200/_cat/health?v 说明:v是用来要求在结果中返回表头 ...

最新文章

  1. 【EXLIBRIS】随笔记 011
  2. 单一docker主机网络
  3. applet打包的MANIFEST.MF配置
  4. 【Android 热修复】热修复原理 ( 类加载机制 | PathClassLoader 加载 Dex 机制 | PathDexList 查找 Class 机制 | 类查找的顺序机制 )
  5. V$SESSION_LONGOPS
  6. CSAPP--整数的运算
  7. Android 智能指针 视频,Android系统智能指针中轻量级指针
  8. kubernets 集群和本地环境联调环境打通工具kt-connect
  9. Android WebView示例教程
  10. 与黑产作战,揭秘数据黑产起点
  11. littlevgl抗锯齿_「VGL」Littlevgl 显示汉字 - seo实验室
  12. 职业规划是什么?应该怎么回答?
  13. 2021 第四届安洵杯 MISC wp
  14. 【HTML5 笔记】基础内容
  15. Java的静态类详解
  16. TVM学习(一)安装TVM Ubuntu16.04安装TVM0.8+clang-llvm12.0
  17. 中国航空发动机行业发展动态分析与十四五战略规划研究报告2022-2028年版
  18. c语言给定n个矩形及其长和宽,c1科目一模拟考试2021最新版练习题
  19. 离线数据分析平台——用户兴趣取向分析(3)导入数据到kafka
  20. python练习题答案,python练习题-答案

热门文章

  1. Typora 0.11.18版不能使用:This beta version of Typora is expired, please download and install a newe
  2. mysql创建外键级联更新_MySQL使用外键实现级联删除与更新的方法
  3. 日更100天(5)每天进步一点点
  4. 自定义报表-FineReport JS实现隐藏Tab页
  5. 球球大作战如何在电脑上玩 球球大作战电脑版教程
  6. Linux上安装git
  7. 计算机类自主招生推荐信,自主招生推荐信范文2017
  8. python项目分析报告_Python---项目需求分析
  9. 乐鑫Esp32学习之旅14 esp32 sdk编程实现门户强制认证,连接esp32热点之后,自动强制弹出指定的登录html界面。(附带Demo)
  10. sei数据格式_【石油化工配管设计规定大全】-48个规范文件-SEI内部资料