Android 11 PackageManagerService源码分析(二):Packages.xml详解

2024-08-25 13:08

本文主要是介绍Android 11 PackageManagerService源码分析(二):Packages.xml详解,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

1、开篇

在上一篇文章中提到Settings类会在PackageManagerService启动过程中对packages.xml等一些列xml文件进行解析。那么有以下问题:

  1. 这些文件记录了什么内容?
  2. 为什么需要这些文件?

让我们一起通过阅读源码解决这些问题吧。

2、packages.xml文件详解

要在真机上拿到packages.xml殊为不易,所以我这里是在模拟器上通过adb命令拉取了一份:

adb pull /data/system/packages.xml

文件内容精简后如下:

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<packages><version sdkVersion="24" databaseVersion="3" fingerprint="Android/sdk_phone_x86/generic_x86:7.0/NYC/4174735:userdebug/test-keys" /><version volumeUuid="primary_physical" sdkVersion="24" databaseVersion="3" fingerprint="Android/sdk_phone_x86/generic_x86:7.0/NYC/4174735:userdebug/test-keys" /><permission-trees /><permissions><item name="android.permission.REAL_GET_TASKS" package="android" protection="18" /><item name="android.permission.ACCESS_CACHE_FILESYSTEM" package="android" protection="18" />...</permissions><package name="com.android.providers.media" codePath="/system/priv-app/MediaProvider" nativeLibraryPath="/system/priv-app/MediaProvider/lib" primaryCpuAbi="x86" publicFlags="944291397" privateFlags="8" ft="15d38697a58" it="15d38697a58" ut="15d38697a58" version="800" sharedUserId="10010"><sigs count="1"><cert index="2" key="308..." /></sigs><perms><item name="android.permission.ACCESS_CACHE_FILESYSTEM" granted="true" flags="0" />...</perms><proper-signing-keyset identifier="4" /></package>...<shared-user name="com.android.emergency.uid" userId="10011"><sigs count="1"><cert index="1" /></sigs><perms><item name="android.permission.MANAGE_USERS" granted="true" flags="0" /></perms></shared-user><keyset-settings version="1"><keys><public-key identifier="1" value="MIIBIDAN..." /><public-key identifier="2" value="MIIBI..." /><public-key identifier="3" value="MIIBI..." /><public-key identifier="4" value="MIIBID..." /></keys><keysets><keyset identifier="1"><key-id identifier="1" /></keyset><keyset identifier="2"><key-id identifier="2" /></keyset><keyset identifier="3"><key-id identifier="3" /></keyset><keyset identifier="4"><key-id identifier="4" /></keyset></keysets><lastIssuedKeyId value="4" /><lastIssuedKeySetId value="4" /></keyset-settings>
</packages>

可以看到,packages.xml主要记录了以下几方面的信息:

  1. 权限信息,permission-trees标签和permissions标签。这里记录的是系统里所有的权限条目
  2. 安装的App信息,包括系统App和用户自行安装的App,package和updated-package标签。其中package标签用户记录一般App的信息;而updated-package通常用于被用户手动升级了的系统App,比如说手机自带了计算器App,这个自带的App的版本是1.0,而厂商又在它的应用商店发布了计算器2.0,当我们在应用商店更新成2.0版本的时候,在系统分区和用户安装分区会各存在一个计算器App。这也是为什么我们在系统应用管理界面删除更新后的计算器2.0后,手机上还是会有计算器1.0版本,而不是彻底消失的原因。
    package标签的属性记录了包名、代码路径、版本等各项信息。另外,package还会有一些子标签,sigs记录App的签名信息,perms记录App的权限信息,注意这里的权限是manifest中声明的权限而不是已经授予的权限。
  3. 共享用户信息,shared-user标签。一般来说一个Android系统会为每一个App分配一个user id,但是我们也可以在manifest元素中指定android:sharedUserId属性,使多个App具有同样的user id从而可以共享数据等等。但是除了系统App,Android强烈建议我们不要使用它,并且在未来的版本可能会被移除,所以了解即可。
  4. 签名信息,keyset-settings标签,记录的是应用签名的公钥。

packages.xml也不止以上这些标签,具体可以参考Settings类对packages.xml的解析:

boolean readLPw(@NonNull List<UserInfo> users) {FileInputStream str = null;if (mBackupSettingsFilename.exists()) {try {str = new FileInputStream(mBackupSettingsFilename);mReadMessages.append("Reading from backup settings file\n");PackageManagerService.reportSettingsProblem(Log.INFO,"Need to read from backup settings file");if (mSettingsFilename.exists()) {// 两者都存在的时候,说明上次更新packages.xml时发生了异常Slog.w(PackageManagerService.TAG, "Cleaning up settings file "+ mSettingsFilename);mSettingsFilename.delete();}} catch (java.io.IOException e) {// We'll try for the normal settings file.}}mPendingPackages.clear();mPastSignatures.clear();mKeySetRefs.clear();mInstallerPackages.clear();try {if (str == null) {if (!mSettingsFilename.exists()) {mReadMessages.append("No settings file found\n");PackageManagerService.reportSettingsProblem(Log.INFO,"No settings file; creating initial state");// It's enough to just touch version details to create them// with default valuesfindOrCreateVersion(StorageManager.UUID_PRIVATE_INTERNAL).forceCurrent();findOrCreateVersion(StorageManager.UUID_PRIMARY_PHYSICAL).forceCurrent();return false;}str = new FileInputStream(mSettingsFilename);}XmlPullParser parser = Xml.newPullParser();parser.setInput(str, StandardCharsets.UTF_8.name());int type;while ((type = parser.next()) != XmlPullParser.START_TAG&& type != XmlPullParser.END_DOCUMENT) {;}if (type != XmlPullParser.START_TAG) {mReadMessages.append("No start tag found in settings file\n");PackageManagerService.reportSettingsProblem(Log.WARN,"No start tag found in package manager settings");Slog.wtf(PackageManagerService.TAG,"No start tag found in package manager settings");return false;}int outerDepth = parser.getDepth();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("package")) {readPackageLPw(parser);} else if (tagName.equals("permissions")) {mPermissions.readPermissions(parser);} else if (tagName.equals("permission-trees")) {mPermissions.readPermissionTrees(parser);} else if (tagName.equals("shared-user")) {readSharedUserLPw(parser);} else if (tagName.equals("preferred-packages")) {// 不再使用了,所以不做任何操作} else if (tagName.equals("preferred-activities")) {// Upgrading from old single-user implementation;// these are the preferred activities for user 0.readPreferredActivitiesLPw(parser, 0);} else if (tagName.equals(TAG_PERSISTENT_PREFERRED_ACTIVITIES)) {// TODO: check whether this is okay! as it is very// similar to how preferred-activities are treatedreadPersistentPreferredActivitiesLPw(parser, 0);} else if (tagName.equals(TAG_CROSS_PROFILE_INTENT_FILTERS)) {// TODO: check whether this is okay! as it is very// similar to how preferred-activities are treatedreadCrossProfileIntentFiltersLPw(parser, 0);} else if (tagName.equals(TAG_DEFAULT_BROWSER)) {readDefaultAppsLPw(parser, 0);} else if (tagName.equals("updated-package")) {// 注意这里,updated-package记录的package视为disabledreadDisabledSysPackageLPw(parser);} else if (tagName.equals("renamed-package")) {String nname = parser.getAttributeValue(null, "new");String oname = parser.getAttributeValue(null, "old");if (nname != null && oname != null) {mRenamedPackages.put(nname, oname);}} else if (tagName.equals("restored-ivi")) {readRestoredIntentFilterVerifications(parser);} else if (tagName.equals("last-platform-version")) {// Upgrade from older XML schemafinal VersionInfo internal = findOrCreateVersion(StorageManager.UUID_PRIVATE_INTERNAL);final VersionInfo external = findOrCreateVersion(StorageManager.UUID_PRIMARY_PHYSICAL);internal.sdkVersion = XmlUtils.readIntAttribute(parser, "internal", 0);external.sdkVersion = XmlUtils.readIntAttribute(parser, "external", 0);internal.fingerprint = external.fingerprint =XmlUtils.readStringAttribute(parser, "fingerprint");} else if (tagName.equals("database-version")) {// Upgrade from older XML schemafinal VersionInfo internal = findOrCreateVersion(StorageManager.UUID_PRIVATE_INTERNAL);final VersionInfo external = findOrCreateVersion(StorageManager.UUID_PRIMARY_PHYSICAL);internal.databaseVersion = XmlUtils.readIntAttribute(parser, "internal", 0);external.databaseVersion = XmlUtils.readIntAttribute(parser, "external", 0);} else if (tagName.equals("verifier")) {final String deviceIdentity = parser.getAttributeValue(null, "device");try {mVerifierDeviceIdentity = VerifierDeviceIdentity.parse(deviceIdentity);} catch (IllegalArgumentException e) {Slog.w(PackageManagerService.TAG, "Discard invalid verifier device id: "+ e.getMessage());}} else if (TAG_READ_EXTERNAL_STORAGE.equals(tagName)) {final String enforcement = parser.getAttributeValue(null, ATTR_ENFORCEMENT);mReadExternalStorageEnforced ="1".equals(enforcement) ? Boolean.TRUE : Boolean.FALSE;} else if (tagName.equals("keyset-settings")) {mKeySetManagerService.readKeySetsLPw(parser, mKeySetRefs);} else if (TAG_VERSION.equals(tagName)) {final String volumeUuid = XmlUtils.readStringAttribute(parser,ATTR_VOLUME_UUID);final VersionInfo ver = findOrCreateVersion(volumeUuid);ver.sdkVersion = XmlUtils.readIntAttribute(parser, ATTR_SDK_VERSION);ver.databaseVersion = XmlUtils.readIntAttribute(parser, ATTR_DATABASE_VERSION);ver.fingerprint = XmlUtils.readStringAttribute(parser, ATTR_FINGERPRINT);} else {Slog.w(PackageManagerService.TAG, "Unknown element under <packages>: "+ parser.getName());XmlUtils.skipCurrentTag(parser);}}str.close();} catch (XmlPullParserException e) {...} catch (java.io.IOException e) {...}...return true;
}

3、Packages.xml的作用

在上篇文章中我们可以看到,packages.xml文件最终被解析和保存到了Settings的mPackages属性里了。来看一下PMS的构造方法里,它都发挥了什么作用吧

public PackageManagerService(Injector injector, boolean onlyCore, boolean factoryTest) {...mSettings = injector.getSettings();...t.traceBegin("addSharedUsers");// 创建一些列系统shared user idmSettings.addSharedUserLPw("android.uid.system", Process.SYSTEM_UID,ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);mSettings.addSharedUserLPw("android.uid.phone", RADIO_UID,ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);...// CHECKSTYLE:OFF IndentationChecksynchronized (mInstallLock) {// writersynchronized (mLock) {...// 读取packages.xml或packages-backup.xml,两者的格式一样,上次更新packages.xml出现异常的时候才会出现packages-backup.xmlt.traceBegin("read user settings");mFirstBoot = !mSettings.readLPw(mInjector.getUserManagerInternal().getUsers(false));t.traceEnd();// 清除代码路径不存在的packagefinal int packageSettingCount = mSettings.mPackages.size();for (int i = packageSettingCount - 1; i >= 0; i--) {PackageSetting ps = mSettings.mPackages.valueAt(i);if (!isExternal(ps) && (ps.codePath == null || !ps.codePath.exists())&& mSettings.getDisabledSystemPkgLPr(ps.name) != null) {mSettings.mPackages.removeAt(i);mSettings.enableSystemPackageLPw(ps.name);}}if (!mOnlyCore && mFirstBoot) {requestCopyPreoptedFiles();}...// Save the names of pre-existing packages prior to scanning, so we can determine// which system packages are completely new due to an upgrade.// 在扫描之前保存预先存在的App的名称,以便确定哪些系统App由于升级完全新加的if (isDeviceUpgrading()) {mExistingPackages = new ArraySet<>(mSettings.mPackages.size());for (PackageSetting ps : mSettings.mPackages.values()) {mExistingPackages.add(ps.name);}}// 扫描系统App,这里代码省略...// Prune any system packages that no longer exist.final List<String> possiblyDeletedUpdatedSystemApps = new ArrayList<>();// Stub packages must either be replaced with full versions in the /data// partition or be disabled.final List<String> stubSystemApps = new ArrayList<>();if (!mOnlyCore) {...final Iterator<PackageSetting> psit = mSettings.mPackages.values().iterator();while (psit.hasNext()) {PackageSetting ps = psit.next();// 非系统App跳过if ((ps.pkgFlags & ApplicationInfo.FLAG_SYSTEM) == 0) {continue;}final AndroidPackage scannedPkg = mPackages.get(ps.name);if (scannedPkg != null) { // packages.xml和即时扫描的结果都存在这个package// 如果系统App扫描到了,而且是disabled状态,也就是被记录在updated-package标签里,把这个记录删除,以期使用用户安装的版本// 后面如果没有找到用户安装的版本,会恢复系统自带的版本if (mSettings.isDisabledSystemPackageLPr(ps.name)) {logCriticalInfo(Log.WARN,"Expecting better updated system app for " + ps.name+ "; removing system app.  Last known"+ " codePath=" + ps.codePathString+ ", versionCode=" + ps.versionCode+ "; scanned versionCode=" + scannedPkg.getLongVersionCode());removePackageLI(scannedPkg, true);mExpectingBetter.put(ps.name, ps.codePath);}continue;}// packages.xml中存在的package如果没有被扫描到执行接下来的代码if (!mSettings.isDisabledSystemPackageLPr(ps.name)) { // 不是disabled状态说明用户没有手动更新过,直接删除psit.remove();logCriticalInfo(Log.WARN, "System package " + ps.name+ " no longer exists; it's data will be wiped");// Assume package is truly gone and wipe residual permissions.mPermissionManager.updatePermissions(ps.name, null);// 真正删除代码和数据的操作会在后面执行} else {// 在disabled list里,判断代码路径是不是还存在,存在的话可能是升级的时候改了包名,不存在则可能删除了final PackageSetting disabledPs =mSettings.getDisabledSystemPkgLPr(ps.name);if (disabledPs.codePath == null || !disabledPs.codePath.exists()|| disabledPs.pkg == null) {possiblyDeletedUpdatedSystemApps.add(ps.name);} else {// 加到mExpectingBetter,以便后续扫描到对应的升级版本的时候继续保持系统版本disabled,而使用用户版本,没有扫描到则再处理是删除还是保留mExpectingBetter.put(disabledPs.name, disabledPs.codePath);}}}}final int cachedSystemApps = PackageCacher.sCachedPackageReadCount.get();// 移除那些没有package关联的shared user idmSettings.pruneSharedUsersLPw();...// 扫描用户安装的Appif (!mOnlyCore) {EventLog.writeEvent(EventLogTags.BOOT_PROGRESS_PMS_DATA_SCAN_START,SystemClock.uptimeMillis());scanDirTracedLI(sAppInstallDir, 0, scanFlags | SCAN_REQUIRE_KNOWN, 0,packageParser, executorService);}packageParser.close();List<Runnable> unfinishedTasks = executorService.shutdownNow();if (!unfinishedTasks.isEmpty()) {throw new IllegalStateException("Not all tasks finished before calling close: "+ unfinishedTasks);}if (!mOnlyCore) {// Remove disable package settings for updated system apps that were// removed via an OTA. If the update is no longer present, remove the// app completely. Otherwise, revoke their system privileges.// 系统升级中移除了App,如果App还存在于用户区(用户手动安装过新版本),剥夺App的系统级权限,否则完全删除for (int i = possiblyDeletedUpdatedSystemApps.size() - 1; i >= 0; --i) {final String packageName = possiblyDeletedUpdatedSystemApps.get(i);final AndroidPackage pkg = mPackages.get(packageName);final String msg;// remove from the disabled system list; do this first so any future// scans of this package are performed without this statemSettings.removeDisabledSystemPackageLPw(packageName);if (pkg == null) {// 这里仍然没找到扫描结果,直接删除msg = "Updated system package " + packageName+ " no longer exists; removing its data";// 真正删除代码和数据的操作会在后面执行} else {// 扫描到了,剥夺系统级权限msg = "Updated system package " + packageName+ " no longer exists; rescanning package on data";// NOTE: We don't do anything special if a stub is removed from the// system image. But, if we were [like removing the uncompressed// version from the /data partition], this is where it'd be done.// remove the package from the system and re-scan it without any// special privileges// 先删除,后重新扫描removePackageLI(pkg, true);try {final File codePath = new File(pkg.getCodePath());// 重新扫描scanPackageTracedLI(codePath, 0, scanFlags, 0, null);} catch (PackageManagerException e) {Slog.e(TAG, "Failed to parse updated, ex-system package: "+ e.getMessage());}}// 最终确认结果final PackageSetting ps = mSettings.mPackages.get(packageName);if (ps != null && mPackages.get(packageName) == null) {removePackageDataLIF(ps, null, null, 0, false);}logCriticalInfo(Log.WARN, msg);}/** Make sure all system apps that we expected to appear on* the userdata partition actually showed up. If they never* appeared, crawl back and revive the system version.*/// 确保应该在用户区出现的系统App存在,不存在则使用系统区的版本for (int i = 0; i < mExpectingBetter.size(); i++) {final String packageName = mExpectingBetter.keyAt(i);if (!mPackages.containsKey(packageName)) {final File scanFile = mExpectingBetter.valueAt(i);logCriticalInfo(Log.WARN, "Expected better " + packageName+ " but never showed up; reverting to system");@ParseFlags int reparseFlags = 0;@ScanFlags int rescanFlags = 0;for (int i1 = mDirsToScanAsSystem.size() - 1; i1 >= 0; i1--) {final ScanPartition partition = mDirsToScanAsSystem.get(i1);if (partition.containsPrivApp(scanFile)) {reparseFlags = systemParseFlags;rescanFlags = systemScanFlags | SCAN_AS_PRIVILEGED| partition.scanFlag;break;}if (partition.containsApp(scanFile)) {reparseFlags = systemParseFlags;rescanFlags = systemScanFlags | partition.scanFlag;break;}}if (rescanFlags == 0) {Slog.e(TAG, "Ignoring unexpected fallback path " + scanFile);continue;}mSettings.enableSystemPackageLPw(packageName);try {scanPackageTracedLI(scanFile, reparseFlags, rescanFlags, 0, null);} catch (PackageManagerException e) {Slog.e(TAG, "Failed to parse original system package: "+ e.getMessage());}}}...}mExpectingBetter.clear();...for (SharedUserSetting setting : mSettings.getAllSharedUsersLPw()) {// NOTE: We ignore potential failures here during a system scan (like// the rest of the commands above) because there's precious little we// can do about it. A settings error is reported, though.final List<String> changedAbiCodePath =applyAdjustedAbiToSharedUser(setting, null /*scannedPackage*/,mInjector.getAbiHelper().getAdjustedAbiForSharedUser(setting.packages, null /*scannedPackage*/));if (changedAbiCodePath != null && changedAbiCodePath.size() > 0) {for (int i = changedAbiCodePath.size() - 1; i >= 0; --i) {final String codePathString = changedAbiCodePath.get(i);try {mInstaller.rmdex(codePathString,getDexCodeInstructionSet(getPreferredInstructionSet()));} catch (InstallerException ignored) {}}}// Adjust seInfo to ensure apps which share a sharedUserId are placed in the same// SELinux domain.setting.fixSeInfoLocked();setting.updateProcesses();}// Now that we know all the packages we are keeping,// read and update their last usage times.mPackageUsage.read(mSettings.mPackages);...t.traceBegin("write settings");mSettings.writeLPr();...mSettings.setPermissionControllerVersion(getPackageInfo(mRequiredPermissionControllerPackage, 0,UserHandle.USER_SYSTEM).getLongVersionCode());...} // synchronized (mLock)} // synchronized (mInstallLock)...
}

这里删除了非常多的代码,只列出了关键性的。可以看到packages.xml的主要作用存储上一次启动时扫描和更新的结果,和本次启动扫描的结果进行比较,判断哪些该更新,哪些该删除。这就是它的主要作用。

这篇关于Android 11 PackageManagerService源码分析(二):Packages.xml详解的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1105635

相关文章

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

性能分析之MySQL索引实战案例

文章目录 一、前言二、准备三、MySQL索引优化四、MySQL 索引知识回顾五、总结 一、前言 在上一讲性能工具之 JProfiler 简单登录案例分析实战中已经发现SQL没有建立索引问题,本文将一起从代码层去分析为什么没有建立索引? 开源ERP项目地址:https://gitee.com/jishenghua/JSH_ERP 二、准备 打开IDEA找到登录请求资源路径位置

OpenHarmony鸿蒙开发( Beta5.0)无感配网详解

1、简介 无感配网是指在设备联网过程中无需输入热点相关账号信息,即可快速实现设备配网,是一种兼顾高效性、可靠性和安全性的配网方式。 2、配网原理 2.1 通信原理 手机和智能设备之间的信息传递,利用特有的NAN协议实现。利用手机和智能设备之间的WiFi 感知订阅、发布能力,实现了数字管家应用和设备之间的发现。在完成设备间的认证和响应后,即可发送相关配网数据。同时还支持与常规Sof

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

Android平台播放RTSP流的几种方案探究(VLC VS ExoPlayer VS SmartPlayer)

技术背景 好多开发者需要遴选Android平台RTSP直播播放器的时候,不知道如何选的好,本文针对常用的方案,做个大概的说明: 1. 使用VLC for Android VLC Media Player(VLC多媒体播放器),最初命名为VideoLAN客户端,是VideoLAN品牌产品,是VideoLAN计划的多媒体播放器。它支持众多音频与视频解码器及文件格式,并支持DVD影音光盘,VCD影

6.1.数据结构-c/c++堆详解下篇(堆排序,TopK问题)

上篇:6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)-CSDN博客 本章重点 1.使用堆来完成堆排序 2.使用堆解决TopK问题 目录 一.堆排序 1.1 思路 1.2 代码 1.3 简单测试 二.TopK问题 2.1 思路(求最小): 2.2 C语言代码(手写堆) 2.3 C++代码(使用优先级队列 priority_queue)

Java ArrayList扩容机制 (源码解读)

结论:初始长度为10,若所需长度小于1.5倍原长度,则按照1.5倍扩容。若不够用则按照所需长度扩容。 一. 明确类内部重要变量含义         1:数组默认长度         2:这是一个共享的空数组实例,用于明确创建长度为0时的ArrayList ,比如通过 new ArrayList<>(0),ArrayList 内部的数组 elementData 会指向这个 EMPTY_EL

如何在Visual Studio中调试.NET源码

今天偶然在看别人代码时,发现在他的代码里使用了Any判断List<T>是否为空。 我一般的做法是先判断是否为null,再判断Count。 看了一下Count的源码如下: 1 [__DynamicallyInvokable]2 public int Count3 {4 [__DynamicallyInvokable]5 get

SWAP作物生长模型安装教程、数据制备、敏感性分析、气候变化影响、R模型敏感性分析与贝叶斯优化、Fortran源代码分析、气候数据降尺度与变化影响分析

查看原文>>>全流程SWAP农业模型数据制备、敏感性分析及气候变化影响实践技术应用 SWAP模型是由荷兰瓦赫宁根大学开发的先进农作物模型,它综合考虑了土壤-水分-大气以及植被间的相互作用;是一种描述作物生长过程的一种机理性作物生长模型。它不但运用Richard方程,使其能够精确的模拟土壤中水分的运动,而且耦合了WOFOST作物模型使作物的生长描述更为科学。 本文让更多的科研人员和农业工作者