一 system_server启动PMS

Android的所有Java服务都是通过system_server进程启动的,并且驻留在system_server进程中。SystemServer进程在启动时,通过创建一个ServerThread线程启动所有服务。

1.1 startBootstrapServices()

system_server的startBootstrapServices()函数会启动一些引导服务,比如:

  • ActivityManagerService
  • PowerManagerService
  • DisplayManagerService
  • SensorService

其中PackageManagerService就在这里启动。

private void startBootstrapServices() {//启动installer服务Installer installer = mSystemServiceManager.startService(Installer.class);// We need the default display before we can initialize the package manager.mSystemServiceManager.startBootPhase(SystemService.PHASE_WAIT_FOR_DEFAULT_DISPLAY);//处于加密状态则仅仅解析核心应用// Only run "core" apps if we're encrypting the device.String cryptState = SystemProperties.get("vold.decrypt");if (ENCRYPTING_STATE.equals(cryptState)) {Slog.w(TAG, "Detected encryption in progress - only parsing core apps");mOnlyCore = true;} else if (ENCRYPTED_STATE.equals(cryptState)) {Slog.w(TAG, "Device encrypted - only parsing core apps");mOnlyCore = true;}// 创建PMS对象 - 启动入口traceBeginAndSlog("StartPackageManagerService");mPackageManagerService = PackageManagerService.main(mSystemContext, installer,mFactoryTestMode != FactoryTest.FACTORY_TEST_OFF, mOnlyCore);// 是否首次启动mFirstBoot = mPackageManagerService.isFirstBoot();// 获取PackageManagermPackageManager = mSystemContext.getPackageManager();Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
}

1.2 startOtherService()

system_server的startOtherService()方法会启动其他服务,在这里也会对PMS做一些处理

private void startOtherServices() {......if (!mOnlyCore) {........try {//将调用performDexOpt:Performs dexopt on the set of packagesmPackageManagerService.updatePackagesIfNeeded();}...............try {//执行Fstrim,执行磁盘维护操作//可能类似于TRIM技术,将标记为删除的文件,彻底从硬盘上移除//而不是等到写入时再移除,目的是提高写入时效率mPackageManagerService.performFstrimIfNeeded();}................try {mPackageManagerService.systemReady();}...............}
}

PMS启动后将参与一些系统优化的工作,然后调用SystemReady方法通知系统服务进入就绪状态

在system_server进程启动过程,涉及PMS服务的主要几个处理:

  • PMS.main()
  • PMS.performDexOpt()
  • PMS.systemReady()

二 PMS.main的入口

public static final PackageManagerService main(Context context, Installer installer,boolean factoryTest, boolean onlyCore) {PackageManagerService m = new PackageManagerService(context, installer,factoryTest, onlyCore);ServiceManager.addService("package", m);return m;
}

在这个main入口中,直接实例化一个PMS服务,并将PMS服务放入ServiceManager中,便于管理,那么重点就是实例化PMS

代码比较多,选择一些重点

new PackageManagerService(context, installer, factoryTest, onlyCore);

public PackageManagerService(Context context, Installer installer,boolean factoryTest, boolean onlyCore) {EventLog.writeEvent(EventLogTags.BOOT_PROGRESS_PMS_START,SystemClock.uptimeMillis());if (mSdkVersion <= 0) {Slog.w(TAG, "**** ro.build.version.sdk not set!");}mContext = context;mFactoryTest = factoryTest;mOnlyCore = onlyCore;mLazyDexOpt = "eng".equals(SystemProperties.get("ro.build.type"));// displayMetrics是一个描述界面显示,尺寸,分辨率,密度的类mMetrics = new DisplayMetrics();// Settings是Android的全局管理者,用于协助PMS保存所有的安装包信息mSettings = new Settings(context);mSettings.addSharedUserLPw("android.uid.system", Process.SYSTEM_UID,ApplicationInfo.FLAG_SYSTEM|ApplicationInfo.FLAG_PRIVILEGED);mSettings.addSharedUserLPw("android.uid.phone", RADIO_UID,ApplicationInfo.FLAG_SYSTEM|ApplicationInfo.FLAG_PRIVILEGED);mSettings.addSharedUserLPw("android.uid.log", LOG_UID,ApplicationInfo.FLAG_SYSTEM|ApplicationInfo.FLAG_PRIVILEGED);mSettings.addSharedUserLPw("android.uid.nfc", NFC_UID,ApplicationInfo.FLAG_SYSTEM|ApplicationInfo.FLAG_PRIVILEGED);mSettings.addSharedUserLPw("android.uid.bluetooth", BLUETOOTH_UID,ApplicationInfo.FLAG_SYSTEM|ApplicationInfo.FLAG_PRIVILEGED);mSettings.addSharedUserLPw("android.uid.shell", SHELL_UID,ApplicationInfo.FLAG_SYSTEM|ApplicationInfo.FLAG_PRIVILEGED);.......mInstaller = installer;// 获取默认的显示信息,保存到mMetricsgetDefaultDisplayMetrics(context, mMetrics);// 获取系统配置信息SystemConfig systemConfig = SystemConfig.getInstance();mGlobalGids = systemConfig.getGlobalGids();mSystemPermissions = systemConfig.getSystemPermissions();mAvailableFeatures = systemConfig.getAvailableFeatures();synchronized (mInstallLock) {// writersynchronized (mPackages) {mHandlerThread = new ServiceThread(TAG,Process.THREAD_PRIORITY_BACKGROUND, true /*allowIo*/);mHandlerThread.start();mHandler = new PackageHandler(mHandlerThread.getLooper());Watchdog.getInstance().addThread(mHandler, WATCHDOG_TIMEOUT);// 创建各种目录,此时,到了我们熟悉的Android目录了File dataDir = Environment.getDataDirectory();mAppDataDir = new File(dataDir, "data");mAppInstallDir = new File(dataDir, "app");mAppLib32InstallDir = new File(dataDir, "app-lib");mAsecInternalPath = new File(dataDir, "app-asec").getPath();mUserAppDataDir = new File(dataDir, "user");mDrmAppPrivateInstallDir = new File(dataDir, "app-private");// 创建用户管理服务sUserManager = new UserManagerService(context, this,mInstallLock, mPackages);.......}.......   }

此方法中,做了大概如下操作

  • 构造DisplayMetrics类:描述界面显示,尺寸,分辨率,密度。构造完后并获取默认的信息保存到变量mMetrics中
  • 构造Settings类:这个是Android的全局管理者,用于协助PMS保存所有的安装包信息
  • 保存Installer对象
  • 获取系统配置信息:SystemConfig构造函数中会通过readPermissions()解析指定目录下的所有xml文件,然后把这些信息保存到systemConfig中,涉及的目录有如下:
    • /system/etc/sysconfig
    • /system/etc/permissions
    • /oem/etc/sysconfig
    • /oem/etc/permissions
  • 创建data下的各种目录,比如data/app, data/app-private等

其中我们最为熟悉的就是最后一点,创建各种Android目录结构

各种第三方应用都是安装在mAppInstallDir目录中,直接搜索mAppInstallDir,得知在scanDirLI()函数中调用

扫描指定文件目录下的apk文件

scanDirLI(File dir, int parseFlags, int scanFlags, long currentTime)

private void scanDirLI(File dir, int parseFlags, int scanFlags, long currentTime) {final File[] files = dir.listFiles();if (ArrayUtils.isEmpty(files)) {Log.d(TAG, "No files in app dir " + dir);return;}if (DEBUG_PACKAGE_SCANNING) {Log.d(TAG, "Scanning app dir " + dir + " scanFlags=" + scanFlags+ " flags=0x" + Integer.toHexString(parseFlags));}for (File file : files) {final boolean isPackage = (isApkFile(file) || file.isDirectory())&& !PackageInstallerService.isStageName(file.getName());if (!isPackage) {// Ignore entries which are not packagescontinue;}try {scanPackageLI(file, parseFlags | PackageParser.PARSE_MUST_BE_APK,scanFlags, currentTime, null);} catch (PackageManagerException e) {Slog.w(TAG, "Failed to parse " + file + ": " + e.getMessage());// Delete invalid userdata appsif ((parseFlags & PackageParser.PARSE_IS_SYSTEM) == 0 &&e.error == PackageManager.INSTALL_FAILED_INVALID_APK) {logCriticalInfo(Log.WARN, "Deleting invalid package at " + file);if (file.isDirectory()) {FileUtils.deleteContents(file);}file.delete();}}}}

扫描这个文件夹下面的所有文件,并且判断是否为apk文件,如果不是继续循环,如果是,则扫描这个路径下的文件,调用scanPackageLI()

解析package信息,其中最重要的一个类为PackageParser,这个类和插件化技术处理联系紧密

private PackageParser.Package scanPackageLI(File scanFile, int parseFlags, int scanFlags,long currentTime, UserHandle user) throws PackageManagerException {if (DEBUG_INSTALL) Slog.d(TAG, "Parsing: " + scanFile);parseFlags |= mDefParseFlags;PackageParser pp = new PackageParser();pp.setSeparateProcesses(mSeparateProcesses);pp.setOnlyCoreApps(mOnlyCore);pp.setDisplayMetrics(mMetrics);.......   final PackageParser.Package pkg;try {// 1pkg = pp.parsePackage(scanFile, parseFlags);} catch (PackageParserException e) {throw PackageManagerException.from(e);}
}

其中1位置使用PackageParser对象解析对应scanFile文件下的资源,最终返回一个pkg对象,pkg为PackageParser类中的内部类Package

Package:

public final static class Package {public String packageName;/** Names of any split APKs, ordered by parsed splitName */public String[] splitNames;// TODO: work towards making these paths invariant/*** Path where this package was found on disk. For monolithic packages* this is path to single base APK file; for cluster packages this is* path to the cluster directory.*/public String codePath;/** Path of base APK */public String baseCodePath;/** Paths of any split APKs, ordered by parsed splitName */public String[] splitCodePaths;/** Flags of any split APKs; ordered by parsed splitName */public int[] splitFlags;public boolean baseHardwareAccelerated;// For now we only support one application per package.public final ApplicationInfo applicationInfo = new ApplicationInfo();public final ArrayList<Permission> permissions = new ArrayList<Permission>(0);public final ArrayList<PermissionGroup> permissionGroups = new ArrayList<PermissionGroup>(0);public final ArrayList<Activity> activities = new ArrayList<Activity>(0);public final ArrayList<Activity> receivers = new ArrayList<Activity>(0);public final ArrayList<Provider> providers = new ArrayList<Provider>(0);public final ArrayList<Service> services = new ArrayList<Service>(0);public final ArrayList<Instrumentation> instrumentation = new ArrayList<Instrumentation>(0);.......   .......   .......
}

其中有我们熟悉的activities,receivers,providers,services四大组件集合,Package对象就是包含了我们Android的所有资源信息

PackageParser中的parsePackage()方法--->parseMonolithicPackage()

    public Package parsePackage(File packageFile, int flags) throws PackageParserException {if (packageFile.isDirectory()) {return parseClusterPackage(packageFile, flags);} else {return parseMonolithicPackage(packageFile, flags);}}@Deprecatedpublic Package parseMonolithicPackage(File apkFile, int flags) throws PackageParserException {if (mOnlyCoreApps) {final PackageLite lite = parseMonolithicPackageLite(apkFile, flags);if (!lite.coreApp) {throw new PackageParserException(INSTALL_PARSE_FAILED_MANIFEST_MALFORMED,"Not a coreApp: " + apkFile);}}// 1final AssetManager assets = new AssetManager();try {final Package pkg = parseBaseApk(apkFile, assets, flags);pkg.codePath = apkFile.getAbsolutePath();return pkg;} finally {IoUtils.closeQuietly(assets);}}

在代码中位置1 ,初始化一个AssetManager对象,并作为参数传入parseBaseApk()函数,最终返回一个package对象

由此可见,parseBaseApk函数也是对这个apk文件的资源进行解析处理

注意:此时new出来的assetManager并不能直接加载apk资源,必须还要去调用addAssetPath方法

parseBaseApk():

private Package parseBaseApk(File apkFile, AssetManager assets, int flags)throws PackageParserException {final String apkPath = apkFile.getAbsolutePath();mParseError = PackageManager.INSTALL_SUCCEEDED;mArchiveSourcePath = apkFile.getAbsolutePath();if (DEBUG_JAR) Slog.d(TAG, "Scanning base APK: " + apkPath);// 1 final int cookie = loadApkIntoAssetManager(assets, apkPath, flags);Resources res = null;XmlResourceParser parser = null;try {res = new Resources(assets, mMetrics, null);assets.setConfiguration(0, 0, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,Build.VERSION.RESOURCES_SDK_INT);// 2 parser = assets.openXmlResourceParser(cookie, ANDROID_MANIFEST_FILENAME);final String[] outError = new String[1];// 3final Package pkg = parseBaseApk(res, parser, flags, outError);if (pkg == null) {throw new PackageParserException(mParseError,apkPath + " (at " + parser.getPositionDescription() + "): " + outError[0]);}pkg.baseCodePath = apkPath;pkg.mSignatures = null;return pkg;} catch (PackageParserException e) {throw e;} catch (Exception e) {throw new PackageParserException(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION,"Failed to read manifest from " + apkPath, e);} finally {IoUtils.closeQuietly(parser);}}

位置1代码为

private static int loadApkIntoAssetManager(AssetManager assets, String apkPath, int flags)throws PackageParserException {if ((flags & PARSE_MUST_BE_APK) != 0 && !isApkPath(apkPath)) {throw new PackageParserException(INSTALL_PARSE_FAILED_NOT_APK,"Invalid package file: " + apkPath);}// The AssetManager guarantees uniqueness for asset paths, so if this asset path// already exists in the AssetManager, addAssetPath will only return the cookie// assigned to it.int cookie = assets.addAssetPath(apkPath);if (cookie == 0) {throw new PackageParserException(INSTALL_PARSE_FAILED_BAD_MANIFEST,"Failed adding asset path: " + apkPath);}return cookie;
}

此时,可以通过assetManager加载apk资源,包括res,androidMainfest.xml文件等

位置2 打开androidMainfest文件,也就是解析关键资源入口就找到了

位置3 将解析后的androidMainfest文件返回的parser对象传入parseBaseApk函数

private Package parseBaseApk(Resources res, XmlResourceParser parser, int flags,String[] outError) throws XmlPullParserException, IOException {....... ....... while ((type = parser.next()) != XmlPullParser.END_DOCUMENT&& (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {continue;}String tagName = parser.getName();if (tagName.equals("application")) {if (foundApp) {if (RIGID_PARSER) {outError[0] = "<manifest> has more than one <application>";mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;return null;} else {Slog.w(TAG, "<manifest> has more than one <application>");XmlUtils.skipCurrentTag(parser);continue;}}foundApp = true;//1if (!parseBaseApplication(pkg, res, parser, attrs, flags, outError)) {return null;}}....... }.......
}

在这个方法中,解析了AndroidMainfest中的application节点,这是我们需要关心的重点。

直接看位置1

private boolean parseBaseApplication(Package owner, Resources res,XmlPullParser parser, AttributeSet attrs, int flags, String[] outError)throws XmlPullParserException, IOException {....... .......while ((type = parser.next()) != XmlPullParser.END_DOCUMENT&& (type != XmlPullParser.END_TAG || parser.getDepth() > innerDepth)) {if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {continue;}String tagName = parser.getName();if (tagName.equals("activity")) {// 1Activity a = parseActivity(owner, res, parser, attrs, flags, outError, false,owner.baseHardwareAccelerated);if (a == null) {mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;return false;}owner.activities.add(a);} else if (tagName.equals("receiver")) {// 2Activity a = parseActivity(owner, res, parser, attrs, flags, outError, true, false);if (a == null) {mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;return false;}owner.receivers.add(a);} else if (tagName.equals("service")) {Service s = parseService(owner, res, parser, attrs, flags, outError);if (s == null) {mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;return false;}owner.services.add(s);} else if (tagName.equals("provider")) {Provider p = parseProvider(owner, res, parser, attrs, flags, outError);if (p == null) {mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;return false;}owner.providers.add(p);} else if (tagName.equals("activity-alias")) {Activity a = parseActivityAlias(owner, res, parser, attrs, flags, outError);if (a == null) {mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;return false;}owner.activities.add(a);} else if (parser.getName().equals("meta-data")) {// note: application meta-data is stored off to the side, so it can// remain null in the primary copy (we like to avoid extra copies because// it can be large)if ((owner.mAppMetaData = parseMetaData(res, parser, attrs, owner.mAppMetaData,outError)) == null) {mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;return false;}} else if (tagName.equals("library")) {sa = res.obtainAttributes(attrs,com.android.internal.R.styleable.AndroidManifestLibrary);// Note: don't allow this value to be a reference to a resource// that may change.String lname = sa.getNonResourceString(com.android.internal.R.styleable.AndroidManifestLibrary_name);sa.recycle();if (lname != null) {lname = lname.intern();if (!ArrayUtils.contains(owner.libraryNames, lname)) {owner.libraryNames = ArrayUtils.add(owner.libraryNames, lname);}}XmlUtils.skipCurrentTag(parser);} else if (tagName.equals("uses-library")) {sa = res.obtainAttributes(attrs,com.android.internal.R.styleable.AndroidManifestUsesLibrary);// Note: don't allow this value to be a reference to a resource// that may change.String lname = sa.getNonResourceString(com.android.internal.R.styleable.AndroidManifestUsesLibrary_name);boolean req = sa.getBoolean(com.android.internal.R.styleable.AndroidManifestUsesLibrary_required,true);sa.recycle();if (lname != null) {lname = lname.intern();if (req) {owner.usesLibraries = ArrayUtils.add(owner.usesLibraries, lname);} else {owner.usesOptionalLibraries = ArrayUtils.add(owner.usesOptionalLibraries, lname);}}XmlUtils.skipCurrentTag(parser);} else if (tagName.equals("uses-package")) {// Dependencies for app installers; we don't currently try to// enforce this.XmlUtils.skipCurrentTag(parser);} else {if (!RIGID_PARSER) {Slog.w(TAG, "Unknown element under <application>: " + tagName+ " at " + mArchiveSourcePath + " "+ parser.getPositionDescription());XmlUtils.skipCurrentTag(parser);continue;} else {outError[0] = "Bad element under <application>: " + tagName;mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;return false;}}}
}

可以看到上面就是我们最为熟悉的配置文件中的各种节点配置,包括四大组件,meta-data

等。

其中有一点可以注意下:activity节点和receiver节点处理类型都为Activity对象,这是因为activity和receiver节点结构一样,类似于JavaBean结构。

activity和receiver节点结构:

       <activity android:name=".MainActivity"><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter></activity><receiver android:name=".MainActivity"><intent-filter><action android:name="android.intent.action.MAIN" /></intent-filter></receiver>

节点中都有intent-filter,action节点。那么在上面parseBaseApplication()函数代码块中位置1和位置2的函数parseActivity()如下:

private Activity parseActivity(Package owner, Resources res,XmlPullParser parser, AttributeSet attrs, int flags, String[] outError,boolean receiver, boolean hardwareAccelerated)throws XmlPullParserException, IOException {......// 1Activity a = new Activity(mParseActivityArgs, new ActivityInfo());......while ((type=parser.next()) != XmlPullParser.END_DOCUMENT&& (type != XmlPullParser.END_TAG|| parser.getDepth() > outerDepth)) {if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {continue;}if (parser.getName().equals("intent-filter")) {//2ActivityIntentInfo intent = new ActivityIntentInfo(a);if (!parseIntent(res, parser, attrs, true, intent, outError)) {return null;}if (intent.countActions() == 0) {Slog.w(TAG, "No actions in intent filter at "+ mArchiveSourcePath + " "+ parser.getPositionDescription());} else {a.intents.add(intent);}} else if (parser.getName().equals("meta-data")) {if ((a.metaData=parseMetaData(res, parser, attrs, a.metaData,outError)) == null) {return null;}}}......
}

13行直接匹配解析intent-filter节点,同时将parseIntent()函数返回的intent-filter放入activity对象中的intents集合中。

在第8行中,这个activity对象中会将信息转为ActivityInfo,其中包括各种信息:name,

packageName等等

到这里基本一次解析就结束了,会将得到的信息存储到对应的PackageParser中的对应的集合中,并将这部分信息传递到PMS中

那么一样的道理,在其他文件路径下的处理也类似,比如/data/app-private目录

以上,为PMS原理简单流程,后续可补充。

Android PMS原理总结相关推荐

  1. 【转】Android Activity原理以及其子类描述,androidactivity

        Android Activity原理以及其子类描述,androidactivity 简介 Activity是Android应用程序组件,实现一个用户交互窗口,我们可以实现布局填充屏幕,也可以实 ...

  2. Android测试原理(三)——使用Eclipse的ADT进行测试

    原文链接:http://developer.android.com/tools/testing/testing_eclipse.html 1.使用Eclipse的ADT进行测试 这次的主题介绍了怎么使 ...

  3. android 开发art,Android应用开发之Android 系统启动原理(art 虚拟机)

    本文将带你了解Android应用开发之Android 系统启动原理(art 虚拟机),希望本文对大家学Android有所帮助. Android   系统启动原理(art 虚拟机) 一.虚拟机的启动 A ...

  4. android 实例源码解释,Android Handler 原理分析及实例代码

    Android Handler 原理分析 Handler一个让无数android开发者头疼的东西,希望我今天这边文章能为您彻底根治这个问题 今天就为大家详细剖析下Handler的原理 Handler使 ...

  5. Android 系统(239)---Android PMS的创建过程

    Android PMS的创建过程 ------转自   刘望舒 刘望舒 前言 PMS的创建过程分为两个部分进行讲解,分别是SyetemServer处理部分和PMS构造方法.其中SyetemServer ...

  6. Android布局原理与优化

    Android布局原理与优化 目录: 绘制原理 CPU与GPU Android 图形系统的整体架构 RenderThread 硬件加速和软件绘制 invalidate软件绘制流程 invalidate ...

  7. android view交替动画,Android View原理(View树遍历,View重绘,View动画)

    一.屏幕绘图基础 Android中的GUI系统是客户端和服务端配合的窗口系统,即后台运行了一个绘制服务,每个应用程序都是该服务端的一个客户端,当客户端需要绘制时,首先请求服务端创建一个窗口,然后在窗口 ...

  8. 【移动安全高级篇】————1、Android沙盘原理与实现

    一.前言 据网秦发布的<2012年上半年全球手机安全报告>,2012年上半年Android病毒感染量增长迅猛,尤以5.6月最为突出,上半年感染手机1283万部,比2011年下半年增长62% ...

  9. Android内存原理

    Android内存原理不用在意剩余内存的大小.其实很多人都是把使用其他系统的习惯带过来了.安卓Android大多应用没有退出的设计其实是有道理的,这和系统对进程的调度机制有关系.如果你知道java,就 ...

最新文章

  1. jmeter中文_JMeter安装配置
  2. 基于OpenCV调整图像的对比度和亮度
  3. 2.设计模式中状态模式(对象的行为模式)(Python实现)
  4. 罗永浩开了一家直播界的新东方
  5. oracle数据库文件默认的安装位置,Oracle 10g数据库默认安装应该注意的问题
  6. 完美解决Win8声卡爆音破音、卡顿
  7. nyoj1052 看美女2
  8. ieee1284controller怎么添加打印机_讲述3D打印机怎么用 初学者必看
  9. 拓扑排序算法(1.0版)
  10. qt.qpa.plugin: Could not find the Qt platform plugin “xcb“ i
  11. Vivado使用技巧(23):综合运行与OOC
  12. 4个简单有效的网页视频下载方法,超级简单好用
  13. Android 实现HTTPS自签名证书(非常详细)
  14. Flutter Dio网络请求:DioError [DioErrorType.RESPONSE]: Http status error [400]或者[500]
  15. 2020 ACM杰出科学家名单出炉:叶杰平、崔鹏等26位华人学者上榜
  16. 安装wxpython for Python3.5
  17. java做百度语言识别_java实现百度云文字识别接口代码
  18. cache-cloud 编译
  19. 来测试下 2019 你一共写了多少行代码?
  20. C/S是什么,B/S是什么

热门文章

  1. Linux系统设置三台静态IP,设置网卡,连接Xshell配置映射及免密
  2. 很怀旧的游戏--吃豆子
  3. php中square是什么意思,square.php
  4. linux逐行扫描,FFmpeg怎么区分识别视频是逐行扫描还是隔行扫描
  5. Oracle常用语句大全
  6. 不注册Nvidia账号下载cuDNN的方法
  7. 不背锅运维:分享OpenStack API使用套路
  8. 支持外链的免费相册地址集合
  9. vue开发可在线编辑简历的webApp
  10. C#窗体鼠标事件区分单击双击