Intent
关于Intent我们都知道他是各个组件,进程之间的通信纽带,Android系统也是通过Intent来查找软件中的对应的组件,并且进行通信。
我们知道在系统启动之后,系统会自动注册各种服务,WindowManagerService和ActivityManagerService就在其中,另外,一个和Intent息息相关的服务也被注册了,这个服务就是PackageManagerService,这个服务主要功能就是解析AndroidMainnifest.xml文件中的APP相关的信息,获取信息完成之后构建项目的信息树。
我们看下PackageManagerService的构造函数:
public PackageManagerService(Context context, Installer installer,
boolean factoryTest, boolean onlyCore) {
//代码省略
synchronized (mInstallLock) {
// writer
synchronized (mPackages) {
//获取/data目录
File dataDir = Environment.getDataDirectory();
mAppDataDir = new File(dataDir, "data");
//获取第三方应用安装目录 /data/app
mAppInstallDir = new File(dataDir, "app");
//代码省略
File frameworkDir = new File(Environment.getRootDirectory(), "framework");
//加载FrameWork资源
alreadyDexOpted.add(frameworkDir.getPath() + "/framework-res.apk");
//加载核心库
alreadyDexOpted.add(frameworkDir.getPath() + "/core-libart.jar");
//省略代码
// 获取系统APP安装路径
File systemAppDir = new File(Environment.getRootDirectory(), "app");
mSystemInstallObserver = new AppDirObserver(
systemAppDir.getPath(), OBSERVER_EVENTS, true, false);
mSystemInstallObserver.startWatching();
//扫描系统APP安装路径
scanDirLI(systemAppDir, PackageParser.PARSE_IS_SYSTEM
| PackageParser.PARSE_IS_SYSTEM_DIR, scanMode, 0);
//省略代码
//非核心应用
if (!mOnlyCore) {
EventLog.writeEvent(EventLogTags.BOOT_PROGRESS_PMS_DATA_SCAN_START,
SystemClock.uptimeMillis());
mAppInstallObserver = new AppDirObserver(
mAppInstallDir.getPath(), OBSERVER_EVENTS, false, false);
mAppInstallObserver.startWatching();
//扫描第三方APP安装路径
scanDirLI(mAppInstallDir, 0, scanMode, 0);
//代码省略
} else {
mAppInstallObserver = null;
mDrmAppInstallObserver = null;
}
//代码省略
} // synchronized (mPackages)
} // synchronized (mInstallLock)
}
代码又臭又长,省略了部分,我们只要明白在构造函数里面PMS做了几件事:
- 加载了系统已经安装的APK
- 加载了FrameWork与核心库
- 扫描了指定目录下的apk文件并进行解析
上面代码中有个很重要的函数就是扫描第三方的APP安装路径即:scanDirLI函数,我们去看下具体实现:
private void scanDirLI(File dir, int flags, int scanMode, long currentTime) {
//获取目录下的所有文件
String[] files = dir.list();
if (files == null) {
Log.d(TAG, "No files in app dir " + dir);
return;
}
if (DEBUG_PACKAGE_SCANNING) {
Log.d(TAG, "Scanning app dir " + dir + " scanMode=" + scanMode
+ " flags=0x" + Integer.toHexString(flags));
}
int i;
//解析目录下的所有apk文件
for (i=0; i<files.length; i++) {
File file = new File(dir, files[i]);
if (!isPackageFilename(files[i])) {
// Ignore entries which are not apk's
//不是apk文件,忽略
continue;
}
//解析apk文件
PackageParser.Package pkg = scanPackageLI(file,
flags|PackageParser.PARSE_MUST_BE_APK, scanMode, currentTime, null);
// Don't mess around with apps in system partition.
if (pkg == null && (flags & PackageParser.PARSE_IS_SYSTEM) == 0 &&
mLastScanError == PackageManager.INSTALL_FAILED_INVALID_APK) {
// Delete the apk
Slog.w(TAG, "Cleaning up failed install of " + file);
file.delete();
}
}
}
这个方法主要的功能就是扫描指定目录下的apk文件,然后通过scanPackageLIb函数进行解析。所以这个方法的主要功能在scanPackageLIb函数中,我们去查看这个代码:
private PackageParser.Package scanPackageLI(File scanFile,
int parseFlags, int scanMode, long currentTime, UserHandle user) {
mLastScanError = PackageManager.INSTALL_SUCCEEDED;
String scanPath = scanFile.getPath();
if (DEBUG_INSTALL) Slog.d(TAG, "Parsing: " + scanPath);
parseFlags |= mDefParseFlags;
PackageParser pp = new PackageParser(scanPath);
pp.setSeparateProcesses(mSeparateProcesses);
pp.setOnlyCoreApps(mOnlyCore);
final PackageParser.Package pkg = pp.parsePackage(scanFile,
scanPath, mMetrics, parseFlags);
if (pkg == null) {
mLastScanError = pp.getParseError();
return null;
}
PackageSetting ps = null;
PackageSetting updatedPkg;
// reader
synchronized (mPackages) {
// Look to see if we already know about this package.
String oldName = mSettings.mRenamedPackages.get(pkg.packageName);
if (pkg.mOriginalPackages != null && pkg.mOriginalPackages.contains(oldName)) {
// This package has been renamed to its original name. Let's
// use that.
ps = mSettings.peekPackageLPr(oldName);
}
// If there was no original package, see one for the real package name.
if (ps == null) {
ps = mSettings.peekPackageLPr(pkg.packageName);
}
// Check to see if this package could be hiding/updating a system
// package. Must look for it either under the original or real
// package name depending on our state.
updatedPkg = mSettings.getDisabledSystemPkgLPr(ps != null ? ps.name : pkg.packageName);
if (DEBUG_INSTALL && updatedPkg != null) Slog.d(TAG, "updatedPkg = " + updatedPkg);
}
// First check if this is a system package that may involve an update
if (updatedPkg != null && (parseFlags&PackageParser.PARSE_IS_SYSTEM) != 0) {
if (ps != null && !ps.codePath.equals(scanFile)) {
// The path has changed from what was last scanned... check the
// version of the new path against what we have stored to determine
// what to do.
if (DEBUG_INSTALL) Slog.d(TAG, "Path changing from " + ps.codePath);
if (pkg.mVersionCode < ps.versionCode) {
// The system package has been updated and the code path does not match
// Ignore entry. Skip it.
Log.i(TAG, "Package " + ps.name + " at " + scanFile
+ " ignored: updated version " + ps.versionCode
+ " better than this " + pkg.mVersionCode);
if (!updatedPkg.codePath.equals(scanFile)) {
Slog.w(PackageManagerService.TAG, "Code path for hidden system pkg : "
+ ps.name + " changing from " + updatedPkg.codePathString
+ " to " + scanFile);
updatedPkg.codePath = scanFile;
updatedPkg.codePathString = scanFile.toString();
// This is the point at which we know that the system-disk APK
// for this package has moved during a reboot (e.g. due to an OTA),
// so we need to reevaluate it for privilege policy.
if (locationIsPrivileged(scanFile)) {
updatedPkg.pkgFlags |= ApplicationInfo.FLAG_PRIVILEGED;
}
}
updatedPkg.pkg = pkg;
mLastScanError = PackageManager.INSTALL_FAILED_DUPLICATE_PACKAGE;
return null;
} else {
// The current app on the system partion is better than
// what we have updated to on the data partition; switch
// back to the system partition version.
// At this point, its safely assumed that package installation for
// apps in system partition will go through. If not there won't be a working
// version of the app
// writer
synchronized (mPackages) {
// Just remove the loaded entries from package lists.
mPackages.remove(ps.name);
}
Slog.w(TAG, "Package " + ps.name + " at " + scanFile
+ "reverting from " + ps.codePathString
+ ": new version " + pkg.mVersionCode
+ " better than installed " + ps.versionCode);
InstallArgs args = createInstallArgs(packageFlagsToInstallFlags(ps),
ps.codePathString, ps.resourcePathString, ps.nativeLibraryPathString);
synchronized (mInstallLock) {
args.cleanUpResourcesLI();
}
synchronized (mPackages) {
mSettings.enableSystemPackageLPw(ps.name);
}
}
}
}
if (updatedPkg != null) {
// An updated system app will not have the PARSE_IS_SYSTEM flag set
// initially
parseFlags |= PackageParser.PARSE_IS_SYSTEM;
// An updated privileged app will not have the PARSE_IS_PRIVILEGED
// flag set initially
if ((updatedPkg.pkgFlags & ApplicationInfo.FLAG_PRIVILEGED) != 0) {
parseFlags |= PackageParser.PARSE_IS_PRIVILEGED;
}
}
// Verify certificates against what was last scanned
if (!collectCertificatesLI(pp, ps, pkg, scanFile, parseFlags)) {
Slog.w(TAG, "Failed verifying certificates for package:" + pkg.packageName);
return null;
}
/*
* A new system app appeared, but we already had a non-system one of the
* same name installed earlier.
*/
boolean shouldHideSystemApp = false;
if (updatedPkg == null && ps != null
&& (parseFlags & PackageParser.PARSE_IS_SYSTEM_DIR) != 0 && !isSystemApp(ps)) {
/*
* Check to make sure the signatures match first. If they don't,
* wipe the installed application and its data.
*/
if (compareSignatures(ps.signatures.mSignatures, pkg.mSignatures)
!= PackageManager.SIGNATURE_MATCH) {
if (DEBUG_INSTALL) Slog.d(TAG, "Signature mismatch!");
deletePackageLI(pkg.packageName, null, true, null, null, 0, null, false);
ps = null;
} else {
/*
* If the newly-added system app is an older version than the
* already installed version, hide it. It will be scanned later
* and re-added like an update.
*/
if (pkg.mVersionCode < ps.versionCode) {
shouldHideSystemApp = true;
} else {
/*
* The newly found system app is a newer version that the
* one previously installed. Simply remove the
* already-installed application and replace it with our own
* while keeping the application data.
*/
Slog.w(TAG, "Package " + ps.name + " at " + scanFile + "reverting from "
+ ps.codePathString + ": new version " + pkg.mVersionCode
+ " better than installed " + ps.versionCode);
InstallArgs args = createInstallArgs(packageFlagsToInstallFlags(ps),
ps.codePathString, ps.resourcePathString, ps.nativeLibraryPathString);
synchronized (mInstallLock) {
args.cleanUpResourcesLI();
}
}
}
}
// The apk is forward locked (not public) if its code and resources
// are kept in different files. (except for app in either system or
// vendor path).
// TODO grab this value from PackageSettings
if ((parseFlags & PackageParser.PARSE_IS_SYSTEM_DIR) == 0) {
if (ps != null && !ps.codePath.equals(ps.resourcePath)) {
parseFlags |= PackageParser.PARSE_FORWARD_LOCK;
}
}
String codePath = null;
String resPath = null;
if ((parseFlags & PackageParser.PARSE_FORWARD_LOCK) != 0) {
if (ps != null && ps.resourcePathString != null) {
resPath = ps.resourcePathString;
} else {
// Should not happen at all. Just log an error.
Slog.e(TAG, "Resource path not set for pkg : " + pkg.packageName);
}
} else {
resPath = pkg.mScanPath;
}
codePath = pkg.mScanPath;
// Set application objects path explicitly.
setApplicationInfoPaths(pkg, codePath, resPath);
// Note that we invoke the following method only if we are about to unpack an application
PackageParser.Package scannedPkg = scanPackageLI(pkg, parseFlags, scanMode
| SCAN_UPDATE_SIGNATURE, currentTime, user);
/*
* If the system app should be overridden by a previously installed
* data, hide the system app now and let the /data/app scan pick it up
* again.
*/
if (shouldHideSystemApp) {
synchronized (mPackages) {
/*
* We have to grant systems permissions before we hide, because
* grantPermissions will assume the package update is trying to
* expand its permissions.
*/
grantPermissionsLPw(pkg, true);
mSettings.disableSystemPackageLPw(pkg.packageName);
}
}
return scannedPkg;
}
private PackageParser.Package scanPackageLI(File scanFile,
int parseFlags, int scanMode, long currentTime, UserHandle user) {
mLastScanError = PackageManager.INSTALL_SUCCEEDED;
String scanPath = scanFile.getPath();
if (DEBUG_INSTALL) Slog.d(TAG, "Parsing: " + scanPath);
parseFlags |= mDefParseFlags;
//创建一个包解析器
PackageParser pp = new PackageParser(scanPath);
pp.setSeparateProcesses(mSeparateProcesses);
pp.setOnlyCoreApps(mOnlyCore);
//解析apk包
final PackageParser.Package pkg = pp.parsePackage(scanFile,
scanPath, mMetrics, parseFlags);
if (pkg == null) {
mLastScanError = pp.getParseError();
return null;
}
//代码省略
//解析apk包中的Activity,Service等组件
PackageParser.Package scannedPkg = scanPackageLI(pkg, parseFlags, scanMode
| SCAN_UPDATE_SIGNATURE, currentTime, user);
//代码省略
return scannedPkg;
}
这个方法做了下面几件事情:
- 创建一个包解析器PackageParser
- 通过包解析器解析apk包
- 解析了apk包中的Activity,Service等组件
从上面的代码可以看到,解析apk包的重要的功能是PackageParser下的parsePackage函数,我们追踪这个函数,看看具体实现:
private Package parsePackage(
Resources res, XmlResourceParser parser, int flags, String[] outError)
throws XmlPullParserException, IOException {
AttributeSet attrs = parser;
mParseInstrumentationArgs = null;
mParseActivityArgs = null;
mParseServiceArgs = null;
mParseProviderArgs = null;
//解析到AndroidMainnifest.xml的包名
//代码省略
//构建Package对象
final Package pkg = new Package(pkgName);
boolean foundApp = false;
//获取AndroidMainnifest.xml文件中的VersionCode,VersionName等参数
TypedArray sa = res.obtainAttributes(attrs,
com.android.internal.R.styleable.AndroidManifest);
pkg.mVersionCode = sa.getInteger(
com.android.internal.R.styleable.AndroidManifest_versionCode, 0);
pkg.mVersionName = sa.getNonConfigurationString(
com.android.internal.R.styleable.AndroidManifest_versionName, 0);
if (pkg.mVersionName != null) {
pkg.mVersionName = pkg.mVersionName.intern();
}
String str = sa.getNonConfigurationString(
com.android.internal.R.styleable.AndroidManifest_sharedUserId, 0);
if (str != null && str.length() > 0) {
String nameError = validateName(str, true);
if (nameError != null && !"android".equals(pkgName)) {
outError[0] = "<manifest> specifies bad sharedUserId name \""
+ str + "\": " + nameError;
mParseError = PackageManager.INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID;
return null;
}
pkg.mSharedUserId = str.intern();
pkg.mSharedUserLabel = sa.getResourceId(
com.android.internal.R.styleable.AndroidManifest_sharedUserLabel, 0);
}
sa.recycle();
pkg.installLocation = sa.getInteger(
com.android.internal.R.styleable.AndroidManifest_installLocation,
PARSE_DEFAULT_INSTALL_LOCATION);
pkg.applicationInfo.installLocation = pkg.installLocation;
/* Set the global "forward lock" flag */
if ((flags & PARSE_FORWARD_LOCK) != 0) {
pkg.applicationInfo.flags |= ApplicationInfo.FLAG_FORWARD_LOCK;
}
/* Set the global "on SD card" flag */
if ((flags & PARSE_ON_SDCARD) != 0) {
pkg.applicationInfo.flags |= ApplicationInfo.FLAG_EXTERNAL_STORAGE;
}
// Resource boolean are -1, so 1 means we don't know the value.
int supportsSmallScreens = 1;
int supportsNormalScreens = 1;
int supportsLargeScreens = 1;
int supportsXLargeScreens = 1;
int resizeable = 1;
int anyDensity = 1;
//解析AndroidMainnifest.xml中的元素
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("application")) {
//代码省略
//解析Application标签,Activity和Service的所在
if (!parseApplication(pkg, res, parser, attrs, flags, outError)) {
return null;
}
} else if (tagName.equals("uses-permission")) {
//解析用户权限标签
if (!parseUsesPermission(pkg, res, parser, attrs, outError)) {
return null;
}
}
//其他标签省略~很多,常见的两个在上面了
}
//省略代码
return pkg;
}
这个上面可以看到做的几件事情:
- 解析包名
- 构建Package对象
- 解析AndroidMainnifest.xml中的各个标签
我们在上面的代码表现出来两个解析标签的函数,我们找到其中一个就是application的好了,我们发现具体解析application的函数是parseApplication函数,我们跟踪这个函数,看看具体实现:
private boolean parseApplication(Package owner, Resources res,
XmlPullParser parser, AttributeSet attrs, int flags, String[] outError)
throws XmlPullParserException, IOException {
//应用信息
final ApplicationInfo ai = owner.applicationInfo;
//包名
final String pkgName = owner.applicationInfo.packageName;
//获取Application的标签
TypedArray sa = res.obtainAttributes(attrs,
com.android.internal.R.styleable.AndroidManifestApplication);
//获取应用名
String name = sa.getNonConfigurationString(
com.android.internal.R.styleable.AndroidManifestApplication_name, 0);
if (name != null) {
ai.className = buildClassName(pkgName, name, outError);
if (ai.className == null) {
sa.recycle();
mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
return false;
}
}
//代码省略
//获取程序的icon,logo等参数
ai.icon = sa.getResourceId(
com.android.internal.R.styleable.AndroidManifestApplication_icon, 0);
ai.logo = sa.getResourceId(
com.android.internal.R.styleable.AndroidManifestApplication_logo, 0);
ai.banner = sa.getResourceId(
com.android.internal.R.styleable.AndroidManifestApplication_banner, 0);
ai.theme = sa.getResourceId(
com.android.internal.R.styleable.AndroidManifestApplication_theme, 0);
ai.descriptionRes = sa.getResourceId(
com.android.internal.R.styleable.AndroidManifestApplication_description, 0);
//代码省略
final int innerDepth = parser.getDepth();
int type;
//获取Application下的所有子元素
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();
//解析Activity
if (tagName.equals("activity")) {
Activity a = parseActivity(owner, res, parser, attrs, flags, outError, false,
hardwareAccelerated);
if (a == null) {
mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
return false;
}
owner.activities.add(a);
//解析receiver
} else if (tagName.equals("receiver")) {
Activity 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);
} //解析其他的例如Service等~代码省略
return true;
}
这个代码做的一个非常普通的xml解析,并且会根据不同的标签使用不同的解析方法例如解析Activity的parseActivity和解析service的parseService他们都回返回一个向右的实例,并把这个实例添加到Package对象列表中去 。
好了,解析apk的方法我们算是走了一遍了,回到调用这个方法的最初点:就是PackageManagerService类中的scanPackageLI方法,在这个方法里我们看到最后调用了一个和scanPackageLI同名的但是不同参数的函数scanPackageLI(PackageParser.Package pkg,int parseFlags, int scanMode, long currentTime, UserHandle user) 这个函数的我们看下主要实现过程:
private PackageParser.Package scanPackageLI(PackageParser.Package pkg,
int parseFlags, int scanMode, long currentTime, UserHandle user) {
//代码省略
int N = pkg.providers.size();
StringBuilder r = null;
int i;
for (i=0; i<N; i++) {
PackageParser.Provider p = pkg.providers.get(i);
p.info.processName = fixProcessName(pkg.applicationInfo.processName,
p.info.processName, pkg.applicationInfo.uid);
mProviders.addProvider(p);
p.syncable = p.info.isSyncable;
if (p.info.authority != null) {
String names[] = p.info.authority.split(";");
p.info.authority = null;
for (int j = 0; j < names.length; j++) {
if (j == 1 && p.syncable) {
// We only want the first authority for a provider to possibly be
// syncable, so if we already added this provider using a different
// authority clear the syncable flag. We copy the provider before
// changing it because the mProviders object contains a reference
// to a provider that we don't want to change.
// Only do this for the second authority since the resulting provider
// object can be the same for all future authorities for this provider.
p = new PackageParser.Provider(p);
p.syncable = false;
}
if (!mProvidersByAuthority.containsKey(names[j])) {
mProvidersByAuthority.put(names[j], p);
if (p.info.authority == null) {
p.info.authority = names[j];
} else {
p.info.authority = p.info.authority + ";" + names[j];
}
if (DEBUG_PACKAGE_SCANNING) {
if ((parseFlags & PackageParser.PARSE_CHATTY) != 0)
Log.d(TAG, "Registered content provider: " + names[j]
+ ", className = " + p.info.name + ", isSyncable = "
+ p.info.isSyncable);
}
} else {
PackageParser.Provider other = mProvidersByAuthority.get(names[j]);
Slog.w(TAG, "Skipping provider name " + names[j] +
" (in package " + pkg.applicationInfo.packageName +
"): name already used by "
+ ((other != null && other.getComponentName() != null)
? other.getComponentName().getPackageName() : "?"));
}
}
}
if ((parseFlags&PackageParser.PARSE_CHATTY) != 0) {
if (r == null) {
r = new StringBuilder(256);
} else {
r.append(' ');
}
r.append(p.info.name);
}
}
if (r != null) {
if (DEBUG_PACKAGE_SCANNING) Log.d(TAG, " Providers: " + r);
}
N = pkg.services.size();
r = null;
for (i=0; i<N; i++) {
PackageParser.Service s = pkg.services.get(i);
s.info.processName = fixProcessName(pkg.applicationInfo.processName,
s.info.processName, pkg.applicationInfo.uid);
mServices.addService(s);
if ((parseFlags&PackageParser.PARSE_CHATTY) != 0) {
if (r == null) {
r = new StringBuilder(256);
} else {
r.append(' ');
}
r.append(s.info.name);
}
}
if (r != null) {
if (DEBUG_PACKAGE_SCANNING) Log.d(TAG, " Services: " + r);
}
N = pkg.receivers.size();
r = null;
for (i=0; i<N; i++) {
PackageParser.Activity a = pkg.receivers.get(i);
a.info.processName = fixProcessName(pkg.applicationInfo.processName,
a.info.processName, pkg.applicationInfo.uid);
mReceivers.addActivity(a, "receiver");
if ((parseFlags&PackageParser.PARSE_CHATTY) != 0) {
if (r == null) {
r = new StringBuilder(256);
} else {
r.append(' ');
}
r.append(a.info.name);
}
}
if (r != null) {
if (DEBUG_PACKAGE_SCANNING) Log.d(TAG, " Receivers: " + r);
}
N = pkg.activities.size();
r = null;
for (i=0; i<N; i++) {
PackageParser.Activity a = pkg.activities.get(i);
a.info.processName = fixProcessName(pkg.applicationInfo.processName,
a.info.processName, pkg.applicationInfo.uid);
mActivities.addActivity(a, "activity");
if ((parseFlags&PackageParser.PARSE_CHATTY) != 0) {
if (r == null) {
r = new StringBuilder(256);
} else {
r.append(' ');
}
r.append(a.info.name);
}
}
if (r != null) {
if (DEBUG_PACKAGE_SCANNING) Log.d(TAG, " Activities: " + r);
}
//代码省略
if (pkg.protectedBroadcasts != null) {
N = pkg.protectedBroadcasts.size();
for (i=0; i<N; i++) {
mProtectedBroadcasts.add(pkg.protectedBroadcasts.get(i));
}
}
pkgSetting.setTimeStamp(scanFileTime);
}
return pkg;
}
这个函数主要做了一件事情,就是把在parsePackage中获取的apk配置列表读取出来,并加入到对应的mActivities,mServices等列表中去。到了这一步,整个已经安装apk的信息树已经建立了,每个apk的应用包名,应用名称,图标等等都已经在系统中有存储,当Intent跳转到某个Activity中的时候Intent就会在这个信息书中的列表中查找,符合条件的组件就会启动。
这样的话就可以通过Intent将各个组件连接到一起,使Android系统成为一个组件可复用的系统。