[原创]Andorid DexClassLoader的创建过程解析(基于5.0)

时间:2021-08-06 07:12:44

做Android插件框架时,经常会用到dex的动态加载,就要直接或间接的使用DexClassLoader,在new DexClassLoader的时候Android系统做了很多工作,下面我们详细分析一下:

public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
}
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList; public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
  ......
@Override
protected URL findResource(String name) {
return pathList.findResource(name);
} @Override
protected Enumeration<URL> findResources(String name) {
return pathList.findResources(name);
} @Override
public String findLibrary(String name) {
return pathList.findLibrary(name);
}

  ......
}

看到关键步骤了,设置完parent的ClassLoader之后,创建了DexPathList对象pathList,可以看到,很多操作都是直接委托给pathList的,我们看下这个对象里面做了什么。

这里涉及到两个目录,optimizedDirectory和libraryPath,我们先从简单的入手,先看看DexPathList是如何处理libraryPath的

final class DexPathList {
private static final String DEX_SUFFIX = ".dex";/** List of native library directories. */
private final File[] nativeLibraryDirectories;public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) { ...... this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
} private static File[] splitLibraryPath(String path) {
// Native libraries may exist in both the system and
// application library paths, and we use this search order:
//
// 1. this class loader's library path for application libraries
// 2. the VM's library path from the system property for system libraries
//
// This order was reversed prior to Gingerbread; see http://b/2933456.
ArrayList<File> result = splitPaths(path, System.getProperty("java.library.path"), true);
return result.toArray(new File[result.size()]);
} private static ArrayList<File> splitPaths(String path1, String path2,
boolean wantDirectories) {
ArrayList<File> result = new ArrayList<File>(); splitAndAdd(path1, wantDirectories, result);
splitAndAdd(path2, wantDirectories, result);
return result;
} private static void splitAndAdd(String searchPath, boolean directoriesOnly,
ArrayList<File> resultList) {
if (searchPath == null) {
return;
}
for (String path : searchPath.split(":")) {
try {
StructStat sb = Libcore.os.stat(path);
if (!directoriesOnly || S_ISDIR(sb.st_mode)) {
resultList.add(new File(path));
}
} catch (ErrnoException ignored) {
}
}
} }

从上面代码可以看到,系统会将用户传进的目录和默认的系统lib目录(System.getProperty("java.library.path"))通过“:”分割后,存进一个File数组中。

这就是DexClassLoader对libraryPath的所有操作,所以可以看到,和optimizePath不一样,并没有对so文件解压。

接下来,我们看看DexClassLoader初始化时候对optimizePath做了什么

还是从DexPathList入手

final class DexPathList {

    /** class definition context */
private final ClassLoader definingContext; private final Element[] dexElements; private final IOException[] dexElementsSuppressedExceptions; public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
......
//check optimized directory
...... this.definingContext = definingContext;
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions);
//save Exceptions
......
this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
} private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
ArrayList<IOException> suppressedExceptions) {
...... if (name.endsWith(DEX_SUFFIX)) {
// Raw dex file (not inside a zip/jar).
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ex) {
System.logE("Unable to load dex file: " + file, ex);
}
} else {
zip = file;
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException suppressed) {
suppressedExceptions.add(suppressed);
}
}
...... private static DexFile loadDexFile(File file, File optimizedDirectory)
throws IOException {
if (optimizedDirectory == null) {
return new DexFile(file);
} else {
String optimizedPath = optimizedPathFor(file, optimizedDirectory);
return DexFile.loadDex(file.getPath(), optimizedPath, 0);
}
} }

除了一些目录检查,异常处理,从构造函数一步一步调用到了loadDexFile,从而进入到DexFile中,我们继续。

public final class DexFile {

    public DexFile(File file) throws IOException {
this(file.getPath());
} public DexFile(String fileName) throws IOException {
mCookie = openDexFile(fileName, null, 0);
mFileName = fileName;
guard.open("close");
//System.out.println("DEX FILE cookie is " + mCookie + " fileName=" + fileName);
} private DexFile(String sourceName, String outputName, int flags) throws IOException {
if (outputName != null) {
try {
String parent = new File(outputName).getParent();
if (Libcore.os.getuid() != Libcore.os.stat(parent).st_uid) {
throw new IllegalArgumentException("Optimized data directory " + parent
+ " is not owned by the current user. Shared storage cannot protect"
+ " your application from code injection attacks.");
}
} catch (ErrnoException ignored) {
// assume we'll fail with a more contextual error later
}
} mCookie = openDexFile(sourceName, outputName, flags);
mFileName = sourceName;
guard.open("close");
//System.out.println("DEX FILE cookie is " + mCookie + " sourceName=" + sourceName + " outputName=" + outputName);
} static public DexFile loadDex(String sourcePathName, String outputPathName,
int flags) throws IOException {
return new DexFile(sourcePathName, outputPathName, flags);
} private static long openDexFile(String sourceName, String outputName, int flags) throws IOException {
// Use absolute paths to enable the use of relative paths when testing on host.
return openDexFileNative(new File(sourceName).getAbsolutePath(),
(outputName == null) ? null : new File(outputName).getAbsolutePath(),
flags);
}
}

可以看到,最终走到openDexFileNative这个native方法中,这个方法的native实现在art/runtime/native/dalvik_system_DexFile.cc中,看代码:

static jlong DexFile_openDexFileNative(JNIEnv* env, jclass, jstring javaSourceName, jstring javaOutputName, jint) {
......
bool success = linker->OpenDexFilesFromOat(sourceName.c_str(), outputName.c_str(), &error_msgs,
dex_files.get());
......
}

方法OpenDexFilesFromOat的实现在art/runtime/class_linker.cc中,这个方法太长,我先把全部内容贴上来,

 bool ClassLinker::OpenDexFilesFromOat(const char* dex_location, const char* oat_location,
std::vector<std::string>* error_msgs,
std::vector<std::unique_ptr<const DexFile>>* dex_files) {
// 1) Check whether we have an open oat file.
// This requires a dex checksum, use the "primary" one.
uint32_t dex_location_checksum;
uint32_t* dex_location_checksum_pointer = &dex_location_checksum;
bool have_checksum = true;
std::string checksum_error_msg;
if (!DexFile::GetChecksum(dex_location, dex_location_checksum_pointer, &checksum_error_msg)) {
// This happens for pre-opted files since the corresponding dex files are no longer on disk.
dex_location_checksum_pointer = nullptr;
have_checksum = false;
} bool needs_registering = false; const OatFile::OatDexFile* oat_dex_file = FindOpenedOatDexFile(oat_location, dex_location,
dex_location_checksum_pointer);
std::unique_ptr<const OatFile> open_oat_file(
oat_dex_file != nullptr ? oat_dex_file->GetOatFile() : nullptr); // 2) If we do not have an open one, maybe there's one on disk already. // In case the oat file is not open, we play a locking game here so
// that if two different processes race to load and register or generate
// (or worse, one tries to open a partial generated file) we will be okay.
// This is actually common with apps that use DexClassLoader to work
// around the dex method reference limit and that have a background
// service running in a separate process.
ScopedFlock scoped_flock; if (open_oat_file.get() == nullptr) {
if (oat_location != nullptr) {
// Can only do this if we have a checksum, else error.
if (!have_checksum) {
error_msgs->push_back(checksum_error_msg);
return false;
} std::string error_msg; // We are loading or creating one in the future. Time to set up the file lock.
if (!scoped_flock.Init(oat_location, &error_msg)) {
error_msgs->push_back(error_msg);
return false;
} // TODO Caller specifically asks for this oat_location. We should honor it. Probably?
open_oat_file.reset(FindOatFileInOatLocationForDexFile(dex_location, dex_location_checksum,
oat_location, &error_msg)); if (open_oat_file.get() == nullptr) {
std::string compound_msg = StringPrintf("Failed to find dex file '%s' in oat location '%s': %s",
dex_location, oat_location, error_msg.c_str());
VLOG(class_linker) << compound_msg;
error_msgs->push_back(compound_msg);
}
} else {
// TODO: What to lock here?
bool obsolete_file_cleanup_failed;
open_oat_file.reset(FindOatFileContainingDexFileFromDexLocation(dex_location,
dex_location_checksum_pointer,
kRuntimeISA, error_msgs,
&obsolete_file_cleanup_failed));
// There's no point in going forward and eventually try to regenerate the
// file if we couldn't remove the obsolete one. Mostly likely we will fail
// with the same error when trying to write the new file.
// TODO: should we maybe do this only when we get permission issues? (i.e. EACCESS).
if (obsolete_file_cleanup_failed) {
return false;
}
}
needs_registering = true;
} // 3) If we have an oat file, check all contained multidex files for our dex_location.
// Note: LoadMultiDexFilesFromOatFile will check for nullptr in the first argument.
bool success = LoadMultiDexFilesFromOatFile(open_oat_file.get(), dex_location,
dex_location_checksum_pointer,
false, error_msgs, dex_files);
if (success) {
const OatFile* oat_file = open_oat_file.release(); // Avoid deleting it.
if (needs_registering) {
// We opened the oat file, so we must register it.
RegisterOatFile(oat_file);
}
// If the file isn't executable we failed patchoat but did manage to get the dex files.
return oat_file->IsExecutable();
} else {
if (needs_registering) {
// We opened it, delete it.
open_oat_file.reset();
} else {
open_oat_file.release(); // Do not delete open oat files.
}
} // 4) If it's not the case (either no oat file or mismatches), regenerate and load. // Need a checksum, fail else.
if (!have_checksum) {
error_msgs->push_back(checksum_error_msg);
return false;
} // Look in cache location if no oat_location is given.
std::string cache_location;
if (oat_location == nullptr) {
// Use the dalvik cache.
const std::string dalvik_cache(GetDalvikCacheOrDie(GetInstructionSetString(kRuntimeISA)));
cache_location = GetDalvikCacheFilenameOrDie(dex_location, dalvik_cache.c_str());
oat_location = cache_location.c_str();
} bool has_flock = true;
// Definitely need to lock now.
if (!scoped_flock.HasFile()) {
std::string error_msg;
if (!scoped_flock.Init(oat_location, &error_msg)) {
error_msgs->push_back(error_msg);
has_flock = false;
}
} if (Runtime::Current()->IsDex2OatEnabled() && has_flock && scoped_flock.HasFile()) {
// Create the oat file.
open_oat_file.reset(CreateOatFileForDexLocation(dex_location, scoped_flock.GetFile()->Fd(),
oat_location, error_msgs));
} // Failed, bail.
if (open_oat_file.get() == nullptr) {
std::string error_msg;
// dex2oat was disabled or crashed. Add the dex file in the list of dex_files to make progress.
DexFile::Open(dex_location, dex_location, &error_msg, dex_files);
error_msgs->push_back(error_msg);
return false;
} // Try to load again, but stronger checks.
success = LoadMultiDexFilesFromOatFile(open_oat_file.get(), dex_location,
dex_location_checksum_pointer,
true, error_msgs, dex_files);
if (success) {
RegisterOatFile(open_oat_file.release());
return true;
} else {
return false;
}
}

class_linker.cc的方法ClassLinker::OpenDexFilesFromOat

我们按步骤分析,首先会先获取这个dex文件的Checksum值:

  if (!DexFile::GetChecksum(dex_location, dex_location_checksum_pointer, &checksum_error_msg)) {
// This happens for pre-opted files since the corresponding dex files are no longer on disk.
dex_location_checksum_pointer = nullptr;
have_checksum = false;
}

这个checksum的值存放在dex_location_checksum_pointer指向的地址,然后通过这个dex_location和checksum查找oat文件是否已经生成,并且加载到内存,

  const OatFile::OatDexFile* oat_dex_file = FindOpenedOatDexFile(oat_location, dex_location, dex_location_checksum_pointer);

进入FindOpenedOatDexFile()函数,

const OatFile::OatDexFile* ClassLinker::FindOpenedOatDexFile(const char* oat_location,
const char* dex_location,
const uint32_t* dex_location_checksum) {
ReaderMutexLock mu(Thread::Current(), dex_lock_);
for (const OatFile* oat_file : oat_files_) {
DCHECK(oat_file != nullptr); if (oat_location != nullptr) {
if (oat_file->GetLocation() != oat_location) {
continue;
}
} const OatFile::OatDexFile* oat_dex_file = oat_file->GetOatDexFile(dex_location,
dex_location_checksum,
false);
if (oat_dex_file != nullptr) {
return oat_dex_file;
}
}
return nullptr;
}

从代码看出,先根据oat的文件路径,查找内存里是否加载了同路径的oat文件,有的话执行oat_file->GetOatDexFile(),

 const OatFile::OatDexFile* OatFile::GetOatDexFile(const char* dex_location,
const uint32_t* dex_location_checksum,
bool warn_if_not_found) const {
// NOTE: We assume here that the canonical location for a given dex_location never
// changes. If it does (i.e. some symlink used by the filename changes) we may return
// an incorrect OatDexFile. As long as we have a checksum to check, we shall return
// an identical file or fail; otherwise we may see some unpredictable failures. // TODO: Additional analysis of usage patterns to see if this can be simplified
// without any performance loss, for example by not doing the first lock-free lookup. const OatFile::OatDexFile* oat_dex_file = nullptr;
StringPiece key(dex_location);
// Try to find the key cheaply in the oat_dex_files_ map which holds dex locations
// directly mentioned in the oat file and doesn't require locking.
auto primary_it = oat_dex_files_.find(key);
if (primary_it != oat_dex_files_.end()) {
oat_dex_file = primary_it->second;
DCHECK(oat_dex_file != nullptr);
} else {
// This dex_location is not one of the dex locations directly mentioned in the
// oat file. The correct lookup is via the canonical location but first see in
// the secondary_oat_dex_files_ whether we've looked up this location before.
MutexLock mu(Thread::Current(), secondary_lookup_lock_);
auto secondary_lb = secondary_oat_dex_files_.lower_bound(key);
if (secondary_lb != secondary_oat_dex_files_.end() && key == secondary_lb->first) {
oat_dex_file = secondary_lb->second; // May be nullptr.
} else {
// We haven't seen this dex_location before, we must check the canonical location.
std::string dex_canonical_location = DexFile::GetDexCanonicalLocation(dex_location);
if (dex_canonical_location != dex_location) {
StringPiece canonical_key(dex_canonical_location);
auto canonical_it = oat_dex_files_.find(canonical_key);
if (canonical_it != oat_dex_files_.end()) {
oat_dex_file = canonical_it->second;
} // else keep nullptr.
} // else keep nullptr. // Copy the key to the string_cache_ and store the result in secondary map.
string_cache_.emplace_back(key.data(), key.length());
StringPiece key_copy(string_cache_.back());
secondary_oat_dex_files_.PutBefore(secondary_lb, key_copy, oat_dex_file);
}
}
if (oat_dex_file != nullptr &&
(dex_location_checksum == nullptr ||
oat_dex_file->GetDexFileLocationChecksum() == *dex_location_checksum)) {
return oat_dex_file;
} if (warn_if_not_found) {
std::string dex_canonical_location = DexFile::GetDexCanonicalLocation(dex_location);
std::string checksum("<unspecified>");
if (dex_location_checksum != NULL) {
checksum = StringPrintf("0x%08x", *dex_location_checksum);
}
LOG(WARNING) << "Failed to find OatDexFile for DexFile " << dex_location
<< " ( canonical path " << dex_canonical_location << ")"
<< " with checksum " << checksum << " in OatFile " << GetLocation();
if (kIsDebugBuild) {
for (const OatDexFile* odf : oat_dex_files_storage_) {
LOG(WARNING) << "OatFile " << GetLocation()
<< " contains OatDexFile " << odf->GetDexFileLocation()
<< " (canonical path " << odf->GetCanonicalDexFileLocation() << ")"
<< " with checksum 0x" << std::hex << odf->GetDexFileLocationChecksum();
}
}
} return NULL;
}

oat_file.cc的OatFile::GetOatDexFile函数

简单说一下GetOatDexFile函数做了什么,系统每次成功生成oat后,都会将dex文件的路径(注意这里是dex文件,不是oat文件)和对应的OatDexFile对象以key/value的形式保存到oat_dex_files_中,在GetOatDexFile函数中,会根据key从oat_dex_files_中查找是否有已加载到内存的oat对象。(这里有两点注意,第一,缓存的key值是原dex或apk的路径,不是oat的;第二,之所以一个oat对象会用一个map做缓存,是因为会有multidex的情况存在。)

首次运行,内存中没有已打开的oat对象,从磁盘查找,

      open_oat_file.reset(FindOatFileInOatLocationForDexFile(dex_location, dex_location_checksum,
oat_location, &error_msg)); if (open_oat_file.get() == nullptr) {
std::string compound_msg = StringPrintf("Failed to find dex file '%s' in oat location '%s': %s",
dex_location, oat_location, error_msg.c_str());
VLOG(class_linker) << compound_msg;
error_msgs->push_back(compound_msg);
}

FindOatFileInOatLocationForDexFile()函数会尝试打开目标oat文件,并对其checksum,oat_offset,patch_delta做校验,如果oat文件存在且内容正确,则

  bool success = LoadMultiDexFilesFromOatFile(open_oat_file.get(), dex_location,
dex_location_checksum_pointer,
false, error_msgs, dex_files);
if (success) {
const OatFile* oat_file = open_oat_file.release(); // Avoid deleting it.
if (needs_registering) {
// We opened the oat file, so we must register it.
RegisterOatFile(oat_file);
}
// If the file isn't executable we failed patchoat but did manage to get the dex files.
return oat_file->IsExecutable();
}

LoadMultiDexFilesFromOatFile()是将多个dex文件(例如classes1.dex,classes2.dex)映射成DexFile对象,并添加到一个vector数据结构dex_files中,之后调用RegisterOatFile()函数将内存中的oat对象注册到oat_files_中,然后整个流程就跑完了。

如果oat文件不存在,或者文件不匹配,则重新创建dex的oat文件,并加载,

 // Look in cache location if no oat_location is given.
std::string cache_location;
if (oat_location == nullptr) {
// Use the dalvik cache.
  // GetDalvikCacheOrDie()函数在art/runtime/utils.cc中
// 这句意味着如果没有指定oat的存放目录,则使用类似/data/dalvik-cache目录
const std::string dalvik_cache(GetDalvikCacheOrDie(GetInstructionSetString(kRuntimeISA)));
cache_location = GetDalvikCacheFilenameOrDie(dex_location, dalvik_cache.c_str());
oat_location = cache_location.c_str();
} // 对文件上锁
bool has_flock = true;
// Definitely need to lock now.
if (!scoped_flock.HasFile()) {
std::string error_msg;
if (!scoped_flock.Init(oat_location, &error_msg)) {
error_msgs->push_back(error_msg);
has_flock = false;
}
} if (Runtime::Current()->IsDex2OatEnabled() && has_flock && scoped_flock.HasFile()) {
// Create the oat file.
// 如果dex2oat工具可用,则生成oat文件
open_oat_file.reset(CreateOatFileForDexLocation(dex_location, scoped_flock.GetFile()->Fd(),
oat_location, error_msgs));
}

由于代码比较简单,直接写在注释里了,接下来看下oat是怎么生成的,也就是CreateOatFileForDexLocation()函数做了什么,

const OatFile* ClassLinker::CreateOatFileForDexLocation(const char* dex_location,
int fd, const char* oat_location,
std::vector<std::string>* error_msgs) {
std::string error_msg;
if (!GenerateOatFile(dex_location, fd, oat_location, &error_msg)) {
CHECK(!error_msg.empty());
error_msgs->push_back(error_msg);
return nullptr;
}
...... return oat_file.release();
}

真正生成oat是在GenerateOatFile()函数中,

 bool ClassLinker::GenerateOatFile(const char* dex_filename,
int oat_fd,
const char* oat_cache_filename,
std::string* error_msg) {
Locks::mutator_lock_->AssertNotHeld(Thread::Current()); // Avoid starving GC.
std::string dex2oat(Runtime::Current()->GetCompilerExecutable()); gc::Heap* heap = Runtime::Current()->GetHeap();
std::string boot_image_option("--boot-image=");
if (heap->GetImageSpace() == nullptr) {
// TODO If we get a dex2dex compiler working we could maybe use that, OTOH since we are likely
// out of space anyway it might not matter.
*error_msg = StringPrintf("Cannot create oat file for '%s' because we are running "
"without an image.", dex_filename);
return false;
}
boot_image_option += heap->GetImageSpace()->GetImageLocation(); std::string dex_file_option("--dex-file=");
dex_file_option += dex_filename; std::string oat_fd_option("--oat-fd=");
StringAppendF(&oat_fd_option, "%d", oat_fd); std::string oat_location_option("--oat-location=");
oat_location_option += oat_cache_filename; std::vector<std::string> argv;
argv.push_back(dex2oat);
argv.push_back("--runtime-arg");
argv.push_back("-classpath");
argv.push_back("--runtime-arg");
argv.push_back(Runtime::Current()->GetClassPathString()); Runtime::Current()->AddCurrentRuntimeFeaturesAsDex2OatArguments(&argv); if (!Runtime::Current()->IsVerificationEnabled()) {
argv.push_back("--compiler-filter=verify-none");
} if (Runtime::Current()->MustRelocateIfPossible()) {
argv.push_back("--runtime-arg");
argv.push_back("-Xrelocate");
} else {
argv.push_back("--runtime-arg");
argv.push_back("-Xnorelocate");
} if (!kIsTargetBuild) {
argv.push_back("--host");
} argv.push_back(boot_image_option);
argv.push_back(dex_file_option);
argv.push_back(oat_fd_option);
argv.push_back(oat_location_option);
const std::vector<std::string>& compiler_options = Runtime::Current()->GetCompilerOptions();
for (size_t i = ; i < compiler_options.size(); ++i) {
argv.push_back(compiler_options[i].c_str());
} if (!Exec(argv, error_msg)) {
// Manually delete the file. Ensures there is no garbage left over if the process unexpectedly
// died. Ignore unlink failure, propagate the original error.
TEMP_FAILURE_RETRY(unlink(oat_cache_filename));
return false;
} return true;
}

上面代码一目了然,直接执行dex2oat工具来生成oat文件,这个dex2oat工具是如何执行的就不分析了。

我们再回到OpenDexFilesFromOat函数中,继续往下走,

  // Failed, bail.
if (open_oat_file.get() == nullptr) {
std::string error_msg;
// dex2oat was disabled or crashed. Add the dex file in the list of dex_files to make progress.
DexFile::Open(dex_location, dex_location, &error_msg, dex_files);
error_msgs->push_back(error_msg);
return false;
} // Try to load again, but stronger checks.
success = LoadMultiDexFilesFromOatFile(open_oat_file.get(), dex_location,
dex_location_checksum_pointer,
true, error_msgs, dex_files);
if (success) {
RegisterOatFile(open_oat_file.release());
return true;
} else {
return false;
}

这里和之前一样,先LoadMultiDexFilesFromOatFile()来加载其他dex,如果成功则RegisterOatFile()注册到oat_files_中。

至此,整个DexClassLoader的创建流程就分析完了。

总结

1.在创建DexClassLoader的时候,Android只是把libraryPath提供的目录保存到一个File数组中,以便之后知道去哪找so库,并没有真正解压出so文件;

2.其实最主要工作还是生成dex文件的oat文件。首先系统会先从内存中查找是否有已经优化并加载好的oat,如果没有,则再从磁盘查找是否有dex对应的oat文件,并判断oat文件是否和dex匹配,如果没有对应的oat文件,则使用dex2oat工具生成oat文件,文件保存在DexClassLoader构造函数中指定的参数目录,如果没指定,就保存在类似/data/dalvik-cache的系统目录中,如果oat文件创建成功,就加载到内存,最后DexClassLoader构造完毕。