【天天聚看点】Baseline Profile 安装时优化在西瓜视频的实践
发布时间:2023-06-03 00:36:15 来源:字节跳动技术团队在Android 5,Google采用的策略是在应用安装期间对APP的全量DEX进行AOT优化。AOT优化(Ahead of time),就是在APP运行前就把DEX字节码编译成本地机器码。虽然运行效率相比DEX解释执行有了大幅提高,但由于是全量AOT,就会导致用户需要等待较长的时间才能打开应用,对于磁盘空间的占用也急剧增大。
于是,为了避免过早的资源占用,从Android 7开始便不再进行全量AOT,而是JIT+AOT的混合编译模式。JIT(Just in time),就是即时优化,也就是在APP运行过程中,实时地把DEX字节码编译成本地机器码。具体方式是,在APP运行时分析运行过的热代码,然后在设备空闲时触发AOT,在下次运行前预编译热代码,提升后续APP运行效率。
但是热代码代码收集需要比较长周期,在APP升级覆盖安装之后,原有的预编译的热代码失效,需要再走一遍运行时分析、空闲时AOT的流程。在单周迭代的研发模式下问题尤为明显。
(资料图)
因此,从Android 9 开始,Google推出了Cloud Profiles技术。它的原理是,在部分先安装APK的用户手机上,Google Play Store收集到热点代码,然后上传到云端并聚合。这样,对于后面安装的用户,Play Store会下发热点代码配置进行预编译,这些用户就不需要进行运行时分析,大大提前了优化时机。不过,这个收集聚合下发过程需要几天时间,大部分用户还是没法享受到这个优化。
最终,在2022年Google推出了 Baseline Profiles(https://developer.android.com/topic/performance/baselineprofiles/overview?hl=zh-cn)技术。它允许开发者内置自己定义的热点代码配置文件。在APP安装期间,系统提前预编译热点代码,大幅提升APP运行效率。
不过,Google官方的Baseline Profiles存在以下局限性:
Baseline Profile 需要使用 AGP 7 及以上的版本,公司内各大APP的版本都还比较低,短期内并不可用安装时优化依赖Google Play,国内无法使用为此,我们开发了一套定制化的Baseline Profiles优化方案,可以适用于全版本AGP。同时通过与国内主流厂商合作,推进支持了安装时优化生效。
方案探索与实现我们先来看一下官方Baseline Profile安装时优化的流程:
这里面主要包含3个步骤:
热点方法收集,通过本地运行设备或者人工配置,得到可读格式的基准配置文本文件(baseline-prof.txt)编译期处理,将基准配置文本文件转换成二进制文件,打包至apk内(baseline.prof和baseline.profm),另外Google Play服务端还会将云端profile与baseline.prof聚合处理。安装时,系统会解析apk内的baseline.prof二进制文件,根据版本号,做一些转换后,提前预编译指定的热点代码为机器码。热点方法收集官方文档(https://developer.android.com/topic/performance/baselineprofiles/create-baselineprofile)提到使用Jetpack Macrobenchmark库(https://developer.android.com/macrobenchmark) 和 BaselineProfileRule
自动收集热点方法。通过在Android Studio中引入Benchmark module,需要编写相应的Rule触发打包、测试等流程。
从下面源码可以看到,最终是通过profman命令可以收集到app运行过程中的热点方法。
private fun profmanGetProfileRules(apkPath: String, pathOptions: List): String { // When compiling with CompilationMode.SpeedProfile, ART stores the profile in one of // 2 locations. The `ref` profile path, or the `current` path. // The `current` path is eventually merged into the `ref` path after background dexopt. val profiles = pathOptions.mapNotNull { currentPath -> Log.d(TAG, "Using profile location: $currentPath") val profile = Shell.executeScriptCaptureStdout( "profman --dump-classes-and-methods --profile-file=$currentPath --apk=$apkPath" ) profile.ifBlank { null } } ... return builder.toString()}
所以,我们可以绕过Macrobenchmark库,直接使用profman命令,减少自动化接入成本。具体命令如下:
adb shell profman --dump-classes-and-methods \--profile-file=/data/misc/profiles/cur/0/com.ss.android.article.video/primary.prof \--apk=/data/app/com.ss.android.article.video-Ctzj32dufeuXB8KOhAqdGg==/base.apk \> baseline-prof.txt
生成的baseline-prof.txt文件内容如下:
PLcom/bytedance/apm/perf/b/f;->a(Lcom/bytedance/apm/perf/b/f;)Ljava/lang/String;PLcom/bytedance/bdp/bdpbase/ipc/n$a;->a()Lcom/bytedance/bdp/bdpbase/ipc/n;HSPLorg/android/spdy/SoInstallMgrSdk;->initSo(Ljava/lang/String;I)ZHSPLorg/android/spdy/SpdyAgent;->InvlidCharJudge([B[B)VLanet/channel/e/a$b;Lcom/bytedance/alliance/services/impl/c;...
这些规则采用两种形式,分别指明方法和类。方法的规则如下所示:
[FLAGS][CLASS_DESCRIPTOR]->[METHOD_SIGNATURE]
FLAGS表示 H
、S
和 P
中的一个或多个字符,用于指示相应方法在启动类型方面应标记为 Hot
、Startup
还是 Post Startup
:
H
标记表示相应方法是一种“热”方法,这意味着相应方法在应用的整个生命周期内会被调用多次。带有 S
标记表示相应方法在启动时被调用。带有 P
标记表示相应方法是与启动无关的热方法。类的规则,则是直接指明类签名即可:
[CLASS_DESCRIPTOR]
不过这里是可读的文本格式,后续还需要进一步转为二进制才可以被系统识别。
另外,release包导出的是混淆后的符号,需要根据mapping文件再做一次反混淆才能使用。
编译期处理在得到base.apk的基准配置文本文件(baseline-prof.txt)之后还不够,一些库里面
(比如androidx的库里https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:recyclerview/recyclerview/src/main/baseline-prof.txt)
也会自带baseline-prof.txt文件。所以,我们还需要把这些子library内附带的baseline-prof.txt取出来,与base.apk的配置一起合并成完整的基准配置文本文件。
接下来,我们需要把完整的配置文件转换成baseline.prof二进制文件。具体是由AGP 7.x内的 CompileArtProfileTask.kt
实现的 :
/** * Task that transforms a human readable art profile into a binary form version that can be shipped * inside an APK or a Bundle. */abstract class CompileArtProfileTask: NonIncrementalTask() {... abstract class CompileArtProfileWorkAction: ProfileAwareWorkAction() { override fun run() { val diagnostics = Diagnostics { error -> throw RuntimeException("Error parsing baseline-prof.txt : $error") } val humanReadableProfile = HumanReadableProfile( parameters.mergedArtProfile.get().asFile, diagnostics ) ?: throw RuntimeException( "Merged ${SdkConstants.FN_ART_PROFILE} cannot be parsed successfully." ) val supplier = DexFileNameSupplier() val artProfile = ArtProfile( humanReadableProfile, if (parameters.obfuscationMappingFile.isPresent) { ObfuscationMap(parameters.obfuscationMappingFile.get().asFile) } else { ObfuscationMap.Empty }, //need to rename dex files with sequential numbers the same way [DexIncrementalRenameManager] does parameters.dexFolders.asFileTree.files.sortedWith(DexFileComparator()).map { DexFile(it.inputStream(), supplier.get()) } ) // the P compiler is always used, the server side will transcode if necessary. parameters.binaryArtProfileOutputFile.get().asFile.outputStream().use { artProfile.save(it, ArtProfileSerializer.V0_1_0_P) } // create the metadata. parameters.binaryArtProfileMetadataOutputFile.get().asFile.outputStream().use { artProfile.save(it, ArtProfileSerializer.METADATA_0_0_2) } } }
这里的核心逻辑就是做了以下3件事:
读取baseline-prof.txt基准配置文本文件,下文用HumanReadableProfile表示将HumanReadableProfile、proguard mapping文件、dex文件作为输入传给ArtProfile由ArtProfile生成特定版本格式的baseline.prof二进制文件ArtProfile类是在profgen子工程内实现的,其中有两个关键的方法:
构造方法:读取HumanReadableProfile、proguard mapping文件、dex文件作为参数,构造ArtProfile实例save()方法:输出指定版本格式的baseline.prof二进制文件参考链接:
https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:profgen/profgen/src/main/kotlin/com/android/tools/profgen/
至此,我们可以基于profgen开发一个gradle plugin,在编译构建流程中插入一个自定义task,将baseline-prof.txt转换成baseline.prof,并内置到apk的asset目录。
核心代码如下:
val packageAndroidTask = variant.variantScope.taskContainer.packageAndroidTask?.get()packageAndroidTask?.doFirst { var dexFiles = collectDexFiles(variant.packageApplication.dexFolders) dexFiles = dexFiles.sortedWith(DexFileComparator()) //基准配置文件的内存表示 var hrp = HumanReadableProfile("baseline-prof.txt") var obfFile: File? = getObfFile(variant, proguardTask) val apk = Apk(dexFiles, "") val obf = if (obfFile != null) ObfuscationMap(obfFile) else ObfuscationMap.Empty val profile = ArtProfile(hrp!!, obf, apk) val dexoptDir = File(variant.mergedAssets.first(), profDir) if (!dexoptDir.exists()) { dexoptDir.mkdirs() } val outFile = File(dexoptDir, "baseline.prof") val metaFile = File(dexoptDir, "baseline.profm") profile.save(outFile.outputStream(), ArtProfileSerializer.V0_1_0_P) profile.save(metaFile.outputStream(), ArtProfileSerializer.METADATA_0_0_2) }
自定义task主要包含以下几个步骤:
解压apk获取dex列表,按照一定规则排序(跟Android的打包规则有关,dex文件名和crc等信息需要和prof二进制文件内的对应上)通过ObfuscationMap将baseline-prof.txt文件中的符号转换成混淆后的符号通过ArtProfile按照一定格式转换成baseline.prof与baseline.profm二进制文件其中有两个文件:
baseline.prof:包含热点方法id、类id信息的二进制编码文件baseline.profm:用于高版本转码的二进制扩展文件关于baseline.prof的格式,我们从ArtProfileSerializer.kt
的注释可以看到不同Android版本有不同的格式。Android 12 开始需要另外转码才能兼容,详见可以看这个issue:
安装期处理参考链接:https://issuetracker.google.com/issues/234353689
在生成带有baseline.prof二进制文件的APK之后,再来看一下系统在安装apk时如何处理这个baseline.prof文件(基于Android 13源码分析)。本地测试通过adb install-multiple release.apk release.dm
命令执行安装,然后通过Android系统包管理子系统进行安装时优化。
Android系统包管理框架分为3层:
应用层:应用通过getPackageManager获取PMS的实例,用于应用的安装,卸载,更新等操作PMS服务层:拥有系统权限,解析并记录应用的基本信息(应用名称,数据存放路径、关系管理等),最终通过binder系统层的installd系统服务进行通讯Installd系统服务层:拥有root权限,完成最终的apk安装、dex优化其中处理baseline.prof二进制文件并最终指导编译生成odex的主要路径如下:
InstallPackageHelper.java#installPackagesLI InstallPackageHelper.java#executePostCommitSteps ArtManagerService.java#prepareAppProfiles Installer.java#prepareAppProfile InstalldNativeService.cpp#prepareAppProfile dexopt.cpp#prepare_app_profile ProfileAssistant.cpp#ProcessProfilesInternal PackageDexOptimizer.java#performDexOpt PackageDexOptimizer.java#performDexOptLI PackageDexOptimizer.java#dexOptPath InstalldNativeService.cpp#dexopt dexopt.cpp#dexopt dex2oat.cc
在入口installPackagesLI函数中,经过prepare、scan、Reconcile、Commit 四个阶段后最终调用executePostCommitSteps完成apk安装、prof文件写入、dexopt优化:
private void installPackagesLI(List requests) { //阶段1:prepare prepareResult = preparePackageLI(request.mArgs, request.mInstallResult); //阶段2:scan final ScanResult result = scanPackageTracedLI( prepareResult.mPackageToScan, prepareResult.mParseFlags, prepareResult.mScanFlags, System.currentTimeMillis(), request.mArgs.mUser, request.mArgs.mAbiOverride); //阶段3:Reconcile reconciledPackages = ReconcilePackageUtils.reconcilePackages( reconcileRequest, mSharedLibraries, mPm.mSettings.getKeySetManagerService(), mPm.mSettings); //阶段4:Commit并安装 commitRequest = new CommitRequest(reconciledPackages, mPm.mUserManager.getUserIds()); executePostCommitSteps(commitRequest); }
executePostCommitSteps中,主要完成prof文件写入与dex优化:
private void executePostCommitSteps(CommitRequest commitRequest) { for (ReconciledPackage reconciledPkg : commitRequest.mReconciledPackages.values()) { final AndroidPackage pkg = reconciledPkg.mPkgSetting.getPkg(); final String packageName = pkg.getPackageName(); final String codePath = pkg.getPath(); //步骤1:prof文件写入 // Prepare the application profiles for the new code paths. // This needs to be done before invoking dexopt so that any install-time profile // can be used for optimizations. mArtManagerService.prepareAppProfiles(pkg, mPm.resolveUserIds(reconciledPkg.mInstallArgs.mUser.getIdentifier()), /* updateReferenceProfileCnotallow= */ true); //步骤2:dex优化,在开启baseline profile优化之后compilation-reasnotallow=install-dm final int compilationReason = mDexManager.getCompilationReasonForInstallScenario( reconciledPkg.mInstallArgs.mInstallScenario); DexoptOptions dexoptOptions = new DexoptOptions(packageName, compilationReason, dexoptFlags); if (performDexopt) { // Compile the layout resources. if (SystemProperties.getBoolean(PRECOMPILE_LAYOUTS, false)) { mViewCompiler.compileLayouts(pkg); } ScanResult result = reconciledPkg.mScanResult; mPackageDexOptimizer.performDexOpt(pkg, realPkgSetting, null /* instructionSets */, mPm.getOrCreateCompilerPackageStats(pkg), mDexManager.getPackageUseInfoOrDefault(packageName), dexoptOptions); } // Notify BackgroundDexOptService that the package has been changed. // If this is an update of a package which used to fail to compile, // BackgroundDexOptService will remove it from its denylist. BackgroundDexOptService.getService().notifyPackageChanged(packageName); notifyPackageChangeObserversOnUpdate(reconciledPkg); } PackageManagerServiceUtils.waitForNativeBinariesExtractionForIncremental( incrementalStorages); }
prof文件写入先来看下prof文件写入流程,主要流程如下图所示:
其入口在ArtManagerService.java``#``prepareAppProfiles
:
/** * Prepare the application profiles. * - create the current primary profile to save time at app startup time. * - copy the profiles from the associated dex metadata file to the reference profile. */ public void prepareAppProfiles( AndroidPackage pkg, @UserIdInt int user, boolean updateReferenceProfileContent) { try { ArrayMap codePathsProfileNames = getPackageProfileNames(pkg); for (int i = codePathsProfileNames.size() - 1; i >= 0; i--) { String codePath = codePathsProfileNames.keyAt(i); String profileName = codePathsProfileNames.valueAt(i); String dexMetadataPath = null; // Passing the dex metadata file to the prepare method will update the reference // profile content. As such, we look for the dex metadata file only if we need to // perform an update. if (updateReferenceProfileContent) { File dexMetadata = DexMetadataHelper.findDexMetadataForFile(new File(codePath)); dexMetadataPath = dexMetadata == null ? null : dexMetadata.getAbsolutePath(); } synchronized (mInstaller) { boolean result = mInstaller.prepareAppProfile(pkg.getPackageName(), user, appId, profileName, codePath, dexMetadataPath); } } } catch (InstallerException e) { } }
其中dexMetadata是后缀为.dm的压缩文件,内部包含primary.prof、primary.profm文件,apk的baseline.prof、baseline.profm会在安装阶段转为成dm文件。
mInstaller.prepareAppProfile
最终会调用到dexopt.cpp#prepare_app_profile
中,通过fork一个子进程执行profman二进制程序,将dm文件、reference_profile文件(位于设备上固定路径,存储汇总的热点方法)、apk文件作为参数输入:
//frameworks/native/cmds/installd/dexopt.cppbool prepare_app_profile(const std::string& package_name, userid_t user_id, appid_t app_id, const std::string& profile_name, const std::string& code_path, const std::optional& dex_metadata) { // We have a dex metdata. Merge the profile into the reference profile. unique_fd ref_profile_fd = open_reference_profile(multiuser_get_uid(user_id, app_id), package_name, profile_name, /*read_write*/ true, /*is_secondary_dex*/ false); unique_fd dex_metadata_fd(TEMP_FAILURE_RETRY( open(dex_metadata->c_str(), O_RDONLY | O_NOFOLLOW))); unique_fd apk_fd(TEMP_FAILURE_RETRY(open(code_path.c_str(), O_RDONLY | O_NOFOLLOW))); RunProfman args; args.SetupCopyAndUpdate(dex_metadata_fd, ref_profile_fd, apk_fd, code_path); pid_t pid = fork(); if (pid == 0) { args.Exec(); } return true;} void SetupCopyAndUpdate(const unique_fd& profile_fd, const unique_fd& reference_profile_fd, const unique_fd& apk_fd, const std::string& dex_location) { SetupArgs(...); } void SetupArgs(const std::vector& profile_fds, const unique_fd& reference_profile_fd, const std::vector& apk_fds, const std::vector& dex_locations, bool copy_and_update, bool for_snapshot, bool for_boot_image) { const char* profman_bin = select_execution_binary("/profman"); if (reference_profile_fd != -1) { AddArg("--reference-profile-file-fd=" + std::to_string(reference_profile_fd.get())); } for (const T& fd : profile_fds) { AddArg("--profile-file-fd=" + std::to_string(fd.get())); } for (const U& fd : apk_fds) { AddArg("--apk-fd=" + std::to_string(fd.get())); } for (const std::string& dex_location : dex_locations) { AddArg("--dex-locatinotallow=" + dex_location); } ...}
实际上,就是执行了下面的profman命令:
./profman --reference-profile-file-fd=9 \--profile-file-fd=10 --apk-fd=11 \--dex-locatinotallow=/data/app/com.ss.android.article.video-4-JZaMrtO7n_kFe4kbhBBA==/base.apk \--copy-and-update-profile-key
reference-profile-file-fd指向/data/misc/profile/ref/$package/primary.prof
文件,记录当前apk版本的热点方法,最终baseline.prof保存的热点方法信息需要写入到reference-profile文件。
profman二进制程序的代码如下:
class ProfMan final { public: void ParseArgs(int argc, char **argv) { MemMap::Init(); for (int i = 0; i < argc; ++i) { if (StartsWith(option, "--profile-file=")) { profile_files_.push_back(std::string(option.substr(strlen("--profile-file=")))); } else if (StartsWith(option, "--profile-file-fd=")) { ParseFdForCollection(raw_option, "--profile-file-fd=", &profile_files_fd_); } else if (StartsWith(option, "--dex-locatinotallow=")) { dex_locations_.push_back(std::string(option.substr(strlen("--dex-locatinotallow=")))); } else if (StartsWith(option, "--apk-fd=")) { ParseFdForCollection(raw_option, "--apk-fd=", &apks_fd_); } else if (StartsWith(option, "--apk=")) { apk_files_.push_back(std::string(option.substr(strlen("--apk=")))); } ... } static int profman(int argc, char** argv) { ProfMan profman; // Parse arguments. Argument mistakes will lead to exit(EXIT_FAILURE) in UsageError. profman.ParseArgs(argc, argv); // Initialize MemMap for ZipArchive::OpenFromFd. MemMap::Init(); ... // Process profile information and assess if we need to do a profile guided compilation. // This operation involves I/O. return profman.ProcessProfiles(); }
可以看到最后一行调用到profman的ProcessProfiles方法,它里面调用了ProfileAssistant.cpp#ProcessProfilesInternal[https://cs.android.com/android/platform/superproject/+/master:art/profman/profile_assistant.cc;l=30?q=ProcessProfilesInternal],核心代码如下:
ProfmanResult::ProcessingResult ProfileAssistant::ProcessProfilesInternal( const std::vector& profile_files, const ScopedFlock& reference_profile_file, const ProfileCompilationInfo::ProfileLoadFilterFn& filter_fn, const Options& options) { ProfileCompilationInfo info(options.IsBootImageMerge()); //步骤1:Load the reference profile. if (!info.Load(reference_profile_file->Fd(), true, filter_fn)) { return ProfmanResult::kErrorBadProfiles; } // Store the current state of the reference profile before merging with the current profiles. uint32_t number_of_methods = info.GetNumberOfMethods(); uint32_t number_of_classes = info.GetNumberOfResolvedClasses(); //步骤2:Merge all current profiles. for (size_t i = 0; i < profile_files.size(); i++) { ProfileCompilationInfo cur_info(options.IsBootImageMerge()); if (!cur_info.Load(profile_files[i]->Fd(), /*merge_classes=*/ true, filter_fn)) { return ProfmanResult::kErrorBadProfiles; } if (!info.MergeWith(cur_info)) { return ProfmanResult::kErrorBadProfiles; } } // 如果新增方法/类没有达到阈值,则跳过 if (((info.GetNumberOfMethods() - number_of_methods) < min_change_in_methods_for_compilation) && ((info.GetNumberOfResolvedClasses() - number_of_classes) < min_change_in_classes_for_compilation)) { return kSkipCompilation; } ... //步骤3:We were successful in merging all profile information. Update the reference profile. ... if (!info.Save(reference_profile_file->Fd())) { return ProfmanResult::kErrorIO; } return options.IsForceMerge() ? ProfmanResult::kSuccess : ProfmanResult::kCompile;}
这里首先通过ProfileCompilationInfo的load方法,读取reference_profile二进制文件序列化加载到内存。再调用MergeWith方法将cur_profile二进制文件(也就是apk内的baseline.prof)合并到reference_profile文件中,最后调用Save方法保存。
再来看下ProfileCompilationInfo的类结构,可以发现与前面编译期处理提到的ArtProfile序列化格式是一致的。
参考链接:https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:profgen/profgen/src/main/kotlin/com/android/tools/profgen/ArtProfileSerializer.kt
//art/libprofile/profile/profile_compilation_info.h/** * Profile information in a format suitable to be queried by the compiler and * performing profile guided compilation. * It is a serialize-friendly format based on information collected by the * interpreter (ProfileInfo). * Currently it stores only the hot compiled methods. */class ProfileCompilationInfo { public: static const uint8_t kProfileMagic[]; static const uint8_t kProfileVersion[]; static const uint8_t kProfileVersionForBootImage[]; static const char kDexMetadataProfileEntry[]; static constexpr size_t kProfileVersionSize = 4; static constexpr uint8_t kIndividualInlineCacheSize = 5; ... }
dex优化分析完prof二进制文件处理流程之后,接着再来看dex优化部分。主要流程如下图所示:
dex优化的入口函数PackageDexOptimizer.java#performDexOptLI
,跟踪代码可以发现最终是通过调用dex2oat二进制程序:
//dexopt.cppint dexopt(const char* dex_path, uid_t uid, const char* pkgname, const char* instruction_set, int dexopt_needed, const char* oat_dir, int dexopt_flags, const char* compiler_filter, const char* volume_uuid, const char* class_loader_context, const char* se_info, bool downgrade, int target_sdk_version, const char* profile_name, const char* dex_metadata_path, const char* compilation_reason, std::string* error_msg, /* out */ bool* completed) { ... RunDex2Oat runner(dex2oat_bin, execv_helper.get()); runner.Initialize(...); bool cancelled = false; pid_t pid = dexopt_status_->check_cancellation_and_fork(&cancelled); if (cancelled) { *completed = false; return 0; } if (pid == 0) { //设置schedpolicy,设置为后台线程 SetDex2OatScheduling(boot_complete); //执行dex2oat命令 runner.Exec(DexoptReturnCodes::kDex2oatExec); } else { //父进程等待dex2oat子进程执行完,超时时间9.5分钟. int res = wait_child_with_timeout(pid, kLongTimeoutMs); if (res == 0) { LOG(VERBOSE) << "DexInv: --- END "" << dex_path << "" (success) ---"; } else { //dex2oat执行失败 } } // dex2oat ran successfully, so profile is safe to keep. reference_profile.DisableCleanup(); return 0;}
实际上是执行了如下命令:
/apex/com.android.runtime/bin/dex2oat \--input-vdex-fd=-1 --output-vdex-fd=11 \--resolve-startup-const-strings=true \--max-image-block-size=524288 --compiler-filter=speed-profile --profile-file-fd=14 \--classpath-dir=/data/app/com.ss.android.article.video-4-JZaMrtO7n_kFe4kbhBBA== \--class-loader-context=PCL[]{PCL[/system/framework/org.apache.http.legacy.jar]} \--generate-mini-debug-info --compact-dex-level=none --dm-fd=15 \--compilation-reasnotallow=install-dm
常规安装时不会带上dm-fd和install-dm参数,所以不会触发baseline profile相关优化。
dex2oat用于将dex字节码编译成本地机器码,相关的编译流程如下代码:
static dex2oat::ReturnCode Dex2oat(int argc, char** argv) { TimingLogger timings("compiler", false, false); // 解析参数 dex2oat->ParseArgs(argc, argv); art::MemMap::Init(); // 加载profile热点方法文件 if (dex2oat->HasProfileInput()) { if (!dex2oat->LoadProfile()) { return dex2oat::ReturnCode::kOther; } } //打开输入文件 dex2oat->OpenFile(); //准备de2oat环境,包括启动runtime、加载boot class path dex2oat::ReturnCode setup_code = dex2oat->Setup(); //检查profile热点方法是否被加载到内存,并做crc校验 if (dex2oat->DoProfileGuidedOptimizations()) { //校验profile_compilation_info_中dex的crc与apk中dex的crc是否一致 dex2oat->VerifyProfileData(); } ... //正式开始编译 dex2oat::ReturnCode result = DoCompilation(*dex2oat); ... return result;}
这个流程包含:
解析命令行传入的参数调用LoadProfile()加载profile热点方法文件,保存到profile_compilation_info_成员变量中准备dex2oat环境,包括启动unstarted runtime、加载boot class pathprofile相关校验,主要检查profile_compilation_info_中的dex的crc与apk中dex的crc是否一致,方法数是否一致调用DoCompilation正式开始编译LoadProfile方法加载profile热点方法文件如下代码:
bool LoadProfile() { //初始化profile热点方法的内存对象:profile_compilation_info_ profile_compilation_info_.reset(new ProfileCompilationInfo()); //读取reference profile文件列表 // Dex2oat only uses the reference profile and that is not updated concurrently by the app or // other processes. So we don"t need to lock (as we have to do in profman or when writing the // profile info). std::vector> profile_files; if (!profile_file_fds_.empty()) { for (int fd : profile_file_fds_) { profile_files.push_back(std::make_unique(DupCloexec(fd))); } } ... //依次加载到profile_compilation_info_中 for (const std::unique_ptr& profile_file : profile_files) { if (!profile_compilation_info_->Load(profile_file->Fd())) { return false; } } return true; }
LoadProfile方法,将之前生成的profile文件加载到内存,保存到profile_compilation_info_变量中。
接着调用Compile方法完成odex文件的编译生成,如下代码:
// Set up and create the compiler driver and then invoke it to compile all the dex files. jobject Compile() REQUIRES(!Locks::mutator_lock_) { ClassLinker* const class_linker = Runtime::Current()->GetClassLinker(); TimingLogger::ScopedTiming t("dex2oat Compile", timings_); ... compiler_options_->profile_compilation_info_ = profile_compilation_info_.get(); driver_.reset(new CompilerDriver(compiler_options_.get(), verification_results_.get(), compiler_kind_, thread_count_, swap_fd_)); driver_->PrepareDexFilesForOatFile(timings_); return CompileDexFiles(dex_files); }
profile_compilation_info_作为参数传给了CompilerDriver,在之后的编译过程中将用来判断是否编译某个方法和机器码重排。
CompilerDriver::Compile方法开始编译dex字节码,代码如下:
void CompilerDriver::Compile(jobject class_loader, const std::vector& dex_files, TimingLogger* timings) { for (const DexFile* dex_file : dex_files) { CompileDexFile(this,class_loader,*dex_file,dex_files, "Compile Dex File Quick",CompileMethodQuick); }}static void CompileMethodQuick(...) { auto quick_fn = [profile_index](...) { CompiledMethod* compiled_method = nullptr; if ((access_flags & kAccNative) != 0) { //jni方法编译... } else if ((access_flags & kAccAbstract) != 0) { // Abstract methods don"t have code. } else if (annotations::MethodIsNeverCompile(dex_file, dex_file.GetClassDef(class_def_idx), method_idx)) { // Method is annotated with @NeverCompile and should not be compiled. } else { const CompilerOptions& compiler_options = driver->GetCompilerOptions(); const VerificationResults* results = driver->GetVerificationResults(); MethodReference method_ref(&dex_file, method_idx); // Don"t compile class initializers unless kEverything. bool compile = (compiler_options.GetCompilerFilter() == CompilerFilter::kEverything) || ((access_flags & kAccConstructor) == 0) || ((access_flags & kAccStatic) == 0); // Check if it"s an uncompilable method found by the verifier. compile = compile && !results->IsUncompilableMethod(method_ref); // Check if we should compile based on the profile. compile = compile && ShouldCompileBasedOnProfile(compiler_options, profile_index, method_ref); if (compile) { compiled_method = driver->GetCompiler()->Compile(...); } } return compiled_method; }; CompileMethodHarness(self,driver,code_item,access_flags, invoke_type,class_def_idx,class_loader, dex_file,dex_cache,quick_fn);}
在CompileMethodQuick方法中可以看到针对不同的方法(jni方法、虚方法、构造函数等)有不同的处理方式,常规方法会通过ShouldCompileBasedOnProfile来判断某个method是否需要被编译。
具体判断条件如下:
// Checks whether profile guided compilation is enabled and if the method should be compiled// according to the profile file.static bool ShouldCompileBasedOnProfile(const CompilerOptions& compiler_options, ProfileCompilationInfo::ProfileIndexType profile_index, MethodReference method_ref) { if (profile_index == ProfileCompilationInfo::MaxProfileIndex()) { // No profile for this dex file. Check if we"re actually compiling based on a profile. if (!CompilerFilter::DependsOnProfile(compiler_options.GetCompilerFilter())) { return true; } // Profile-based compilation without profile for this dex file. Do not compile the method. return false; } else { const ProfileCompilationInfo* profile_compilation_info = compiler_options.GetProfileCompilationInfo(); // Compile only hot methods, it is the profile saver"s job to decide // what startup methods to mark as hot. bool result = profile_compilation_info->IsHotMethod(profile_index, method_ref.index); if (kDebugProfileGuidedCompilation) { LOG(INFO) << "[ProfileGuidedCompilation] " << (result ? "Compiled" : "Skipped") << " method:" << method_ref.PrettyMethod(true); } return result; }}
可以看到是依据profile_compilation_info_是否命中hotmethod来判断。我们把编译日志打开,可以看到具体哪些方法被编译,哪些方法被跳过,如下图所示,这与我们配置的profile是一致的。
机器码生成的实现在CodeGenerator类中,代码如下,具体细节将不再展开。
//art/compiler/optimizing/code_generator.ccvoid CodeGenerator::Compile(CodeAllocator* allocator) { InitializeCodeGenerationData(); HGraphVisitor* instruction_visitor = GetInstructionVisitor(); GetStackMapStream()->BeginMethod(...); size_t frame_start = GetAssembler()->CodeSize(); GenerateFrameEntry(); if (disasm_info_ != nullptr) { disasm_info_->SetFrameEntryInterval(frame_start, GetAssembler()->CodeSize()); } for (size_t e = block_order_->size(); current_block_index_ < e; ++current_block_index_) { HBasicBlock* block = (*block_order_)[current_block_index_]; Bind(block); MaybeRecordNativeDebugInfo(/* instructinotallow= */ nullptr, block->GetDexPc()); for (HInstructionIterator it(block->GetInstructions()); !it.Done(); it.Advance()) { HInstruction* current = it.Current(); DisassemblyScope disassembly_scope(current, *this); current->Accept(instruction_visitor); } } GenerateSlowPaths(); if (graph_->HasTryCatch()) { RecordCatchBlockInfo(); } // Finalize instructions in assember; Finalize(allocator); GetStackMapStream()->EndMethod(GetAssembler()->CodeSize());}
另外,profile_compilation_info_也会影响机器码重排,我们知道系统在通过IO加载文件的时候,一般都是按页维度来加载的(一般等于4KB),热点代码重排在一起,可以减少IO读取的次数,从而提升性能。
odex文件的机器码布局部分由OatWriter
类实现,声明代码如下:
class OatWriter { public: OatWriter(const CompilerOptions& compiler_options, const VerificationResults* verification_results, TimingLogger* timings, ProfileCompilationInfo* info, CompactDexLevel compact_dex_level); ... // Profile info used to generate new layout of files. ProfileCompilationInfo* profile_compilation_info_; // Compact dex level that is generated. CompactDexLevel compact_dex_level_; using OrderedMethodList = std::vector; ...
从中可以看到profile_compilation_info_会被OatWriter
类用到,用于生成odex机器码的布局。
具体代码如下:
// Visit every compiled method in order to determine its order within the OAT file.// Methods from the same class do not need to be adjacent in the OAT code.class OatWriter::LayoutCodeMethodVisitor final : public OatDexMethodVisitor { public: LayoutCodeMethodVisitor(OatWriter* writer, size_t offset) : OatDexMethodVisitor(writer, offset), profile_index_(ProfileCompilationInfo::MaxProfileIndex()), profile_index_dex_file_(nullptr) { } bool StartClass(const DexFile* dex_file, size_t class_def_index) final { // Update the cached `profile_index_` if needed. This happens only once per dex file // because we visit all classes in a dex file together, so mark that as `UNLIKELY`. if (UNLIKELY(dex_file != profile_index_dex_file_)) { if (writer_->profile_compilation_info_ != nullptr) { profile_index_ = writer_->profile_compilation_info_->FindDexFile(*dex_file); } profile_index_dex_file_ = dex_file; } return OatDexMethodVisitor::StartClass(dex_file, class_def_index); } bool VisitMethod(size_t class_def_method_index, const ClassAccessor::Method& method){ OatClass* oat_class = &writer_->oat_classes_[oat_class_index_]; CompiledMethod* compiled_method = oat_class->GetCompiledMethod(class_def_method_index); if (HasCompiledCode(compiled_method)) { // Determine the `hotness_bits`, used to determine relative order // for OAT code layout when determining binning. uint32_t method_index = method.GetIndex(); MethodReference method_ref(dex_file_, method_index); uint32_t hotness_bits = 0u; if (profile_index_ != ProfileCompilationInfo::MaxProfileIndex()) { ProfileCompilationInfo* pci = writer_->profile_compilation_info_; // Note: Bin-to-bin order does not matter. If the kernel does or does not read-ahead // any memory, it only goes into the buffer cache and does not grow the PSS until the // first time that memory is referenced in the process. hotness_bits = (pci->IsHotMethod(profile_index_, method_index) ? kHotBit : 0u) | (pci->IsStartupMethod(profile_index_, method_index) ? kStartupBit : 0u) } } OrderedMethodData method_data = {hotness_bits,oat_class,compiled_method,method_ref,...}; ordered_methods_.push_back(method_data); } return true; }
在LayoutCodeMethodVisitor类中,根据profile_compilation_info_指定的热点方法的FLAG,判断是否打开hotness_bits标志位。热点方法会一起被重排在odex文件靠前的位置。
小结一下,在系统安装app阶段,会读取apk中baselineprofile文件,经过porfman根据当前系统版本做一定转换并序列化到本地的reference_profile路径下,再通过dexoat编译热点方法为本地机器码并通过代码重排提升性能。
厂商合作Baseline Profile安装时优化需要Google Play支持,但国内手机由于没有Google Play,无法在安装期做实现优化效果。为此,我们协同抖音与小米、华为等主流厂商建立了合作,共同推进Baseline Profile安装时优化在国内环境的落地。具体的合作方式是:
我们通过编译期改造,提供带Baseline Profile的APK给到厂商验证联调。厂商具体的优化策略会综合考量安装时长、dex2oat消耗资源情况而定,比如先用默认策略安装apk,再后台异步执行Baseline Profile编译。最后通过Google提供的初步显示所用时间 (TTID) 来验证优化效果(TTID指标用于测量应用生成第一帧所用的时间,包括进程初始化、activity 创建以及显示第一帧。)参考链接
https://developer.android.com/topic/performance/vitals/launch-time?hl=zh-cn
在与厂商联调的过程中,我们解决了各种问题,其中包括有一个资源压缩方式错误。具体错误信息如下:
java.io.FileNotFoundException: This file can not be opened as a file descriptor; it is probably compressed
原来安卓系统要求apk内的baseline.prof二进制是不压缩格式的。我们可以用unzip -v来检验文件是否未被压缩,Defl标志表示压缩,Stored标志表示未压缩。
我们可以在打包流程中指定其为STORED格式,即不压缩。
private void writeNoCompress(@NonNull JarEntry entry, @NonNull InputStream from) throws IOException { byte[] bytes = new byte[from.available()]; from.read(bytes); entry.setMethod(JarEntry.STORED); entry.setSize(bytes.length); CRC32 crc32 = new CRC32(); crc32.update(bytes,0,bytes.length); entry.setCrc(crc32.getValue()); setEntryAttributes(entry); jarOutputStream.putNextEntry(entry); jarOutputStream.write(bytes, 0, bytes.length); jarOutputStream.closeEntry();}
改完之后我们再检查一下文件是否被压缩。
baseline.prof二进制是不压缩对包体积影响比较小,因为这个文件大部分都是int类型的methodid。经测试,7万+热点方法文件,生成baseline.prof二进制文件62KB,压缩率只有0.1%;如果通过通配符配置,压缩率在5%左右。
一般应用商店下载安装包时在网络传输过程中做了(压缩)https://zh.wikipedia.org/wiki/HTTP%E5%8E%8B%E7%BC%A9处理,这种情况不压缩处理基本不影响包大小,同时不压缩处理也能避免解压缩带来的耗时。
优化效果在自测中,我们可以通过下面的方式通过install-multiple
命令安装APK。
# Unzip the Release APK firstunzip release.apk# Create a ZIP archivecp assets/dexopt/baseline.prof primary.profcp assets/dexopt/baseline.profm primary.profm# Create an archivezip -r release.dm primary.prof primary.profm# Install APK + Profile togetheradb install-multiple release.apk release.dm
在厂商测试中通过下面的命令测试冷启动耗时
PACKAGE_NAME=com.ss.android.article.videoadb shell am start-activity -W -n $PACKAGE_NAME/.SplashActivity | grep "TotalTime"
冷启动Activity耗时比较 | 未优化 | 已优化 | 优化率 |
荣耀Android11 | 950ms | 884ms | 6.9% |
小米Android13 | 821ms | 720ms | 12.3% |
可以看到,在开启Baseline Profile优化之后,首装冷启动(TTID)耗时减少约10%左右,为新用户的启动速度体验带来了极大的提升。
参考文章Android 端内数据状态同步方案VM-Mapping开源 | Scene:Android 开源页面导航和组合框架团队介绍我们是字节跳动西瓜视频客户端团队,专注于西瓜视频 App 的开发和基础技术建设,在客户端架构、性能、稳定性、编译构建、研发工具等方向都有投入。如果你也想一起攻克技术难题,迎接更大的技术挑战,欢迎点击阅读原文,或者投递简历到xiaolin.gan@bytedance.com。
最 Nice 的工作氛围和成长机会,福利与机遇多多,在北上杭三地均有职位,欢迎加入西瓜视频客户端团队 !
标签:
精彩推送
【天天聚看点】Baseline Profile 安装时优化在西瓜视频的实践
背景在Android5,Google采用的策略是在应用安装期间对APP的全量DEX进行AOT优化。AOT优化(Aheadoftime),
商洛市委宣传部机关党委开展第五期“周讲堂”活动
商洛新闻网讯:今天(6月2日),商洛市委宣传部机关党委开展第五期“周讲堂”活动。“周讲堂”活动是市委宣
今日热搜:RCEP对15个签署国全面生效:为区域经济一体化注入强劲动力
中国日报网6月2日电6月2日,《区域全面经济伙伴关系协定》(RCEP)对菲律宾正式生效,标志着RCEP对15个签署
新疆新鑫矿业(03833)将于6月30日派末期股息每股0.16629港元 当前热文
新疆新鑫矿业(03833)发布公告,将于2023年6月30日派发截至2022年12
世界讯息:拓扑结构图是什么意思_拓扑结构是什么意思
今天小编肥嘟来为大家解答以上的问题。拓扑结构图是什么意思,拓扑结构是什么意思相信很多小伙伴还不知道,
银城国际前5个月合约销售额约37.16亿元 均价2.19万元/平米|世界即时看
以此计算,银城国际控股今年前5个月合约销售额同比下降约27 29%,总合约建筑面积同比下降26 68%。
最新:平谷一宗工业用地出让公告
平谷一宗工业用地出让公告,平谷,起始价,工业用地,储备中心,出让公告,住宅用地
世界百事通!港股异动 | 汇量科技(01860)现涨近5% Mintegral进入利润稳定释放期 SaaS产品有望带来新增量
汇量科技(01860)现涨近5%,截至发稿,涨4 71%,报3 78港元,成交额280 83万港元。
龙虎榜 | 融捷股份今日涨停 深股通专用买入1.27亿元并卖出3818.7万元|今亮点
6月2日,融捷股份今日涨停,龙虎榜数据显示,上榜营业部席位全天成交3 8亿元,占当日总成交金额比例为28 09
2023年无锡跨境电商会活动有哪些|每日看点
2023年无锡跨境电商会活动有哪些展期活动安排(具体活动内容点击查看)展会亮点:1、探秘展会盛况:精彩企
终于松了一口气!德媒:“美国的债务闹剧已经结束”|滚动
终于松了一口气!德媒:“美国的债务闹剧已经结束”,拜登,德媒,民主党,共和党,华盛顿,美国政府,债务闹剧,美
全球新消息丨万科前5个月合同销售1677.9亿元,同比下滑0.19%
万科前5个月合同销售1677 9亿元,同比下滑0 19%,万科,房地产,恒大集团,合同销售面积,中国交通运输公司
禹州市:党员冲上前 全力保夏收_环球时讯
“我报名,报名参加!”5月31日,禹州市颍川街道十里社区党委刚刚在“党建微信群”发布了招募“三夏”志愿
全球看点:地产&汽车,出台了大政策!
地产&汽车,出台了大政策!,楼市,房地产,商品住房,新能源汽车
世界热推荐:2023昆明壹城青年音乐节参演阵容名单(附乐队介绍)
2023昆明壹城青年音乐节参演阵容新学校废物合唱团这是一支让你看到名字就想一探究竟的乐团!究竟有几只废物?
曝黄晓明带新女友逛街,女方下车时被摸屁股,长相甜美像女星杨颖
而对于这个年轻女孩的身份,狗仔说自己没认出来,但通过了某款软件的“识图”功能,找到了和这个女孩非常
2023青岛市各级机关公务员补录原则和条件
补录的原则和条件(一)报考人员须符合2023年度青岛市各级机关招录公务员报考资格条件,并已参加2023年度山
环球速读:联想小新 24 FHD 高刷屏显示器发布,首销价 599 元
IT之家6月2日消息,联想推出新款小新24FHD高刷屏显示器,显示器厚度6 9mm,边框2mm。新款显示器正面是一块
用音乐点亮城市夏夜 2023南京长江民乐汇圆满收官
此页面是否是列表页或首页?未找到合适正文内容。
天天新资讯:国内迷你主机厂商极摩客公布了新款K4迷你主机的鲁大师跑分
6月1日消息,国内迷你主机厂商极摩客,公布了新款K4迷你主机的鲁大师跑分:综合性能超150分。其中,该主机
速看:2023全球数字经济大会7月初举行,构建数字经济“朋友圈”
2023全球数字经济大会7月初举行,构建数字经济“朋友圈”,朋友圈,中关村论坛,数字经济大会
多地5月用电负荷突破历史峰值,全力备战迎峰度夏保供“大考”
多地5月用电负荷突破历史峰值,全力备战迎峰度夏保供“大考”,迎峰,电力,保供,夏保,电负荷
扬州:“新8条”落地三个月 仅 “商转公”为购房者节省1.2亿 全球快播报
扬州:“新8条”落地三个月仅“商转公”为购房者节省1 2亿,商转,购房者,扬州市,大运河,中国文物,公积金贷款
南昌34.88亿元挂牌5宗涉宅用地 6月21日开拍|当前最新
南昌34 88亿元挂牌5宗涉宅用地6月21日开拍,南昌,挂牌,楼面价,红谷滩,涉宅用地,住宅用地
聚乙烯市场竞争加剧 新增产能不断释放
随着聚乙烯(PE)新增产能不断释放,市场进入阵痛期。金联创数据显示,截至2023年5月,国内聚乙烯新增产能2
SE《最终幻想16》首发只推出PS主机版 PC版要多等半年_速看
即便PS5版一发售就着手开发PC版,也无法在半年内就优化完毕,所以《最终幻想16》PC版还要多等半年以上的时
焦点观察:北京农产品供应基地落地济宁市兖州区
北京农产品供应基地落地济宁市兖州区---
惠普CQ42153TX(惠普CQ42471TU)
当前大家对于惠普CQ42-471TU都是颇为感兴趣的,大家都想要了解一下惠普CQ42-471TU,那么小美也是在网络上收
“六一”儿童节,他们收到了一份难忘的礼物 天天快看点
“六一”儿童节,他们收到了一份难忘的礼物---6月1日儿童节这天,在汉川市汈东小学举办的“六一”国际儿童
观天下!水滴公司公布2023年Q1业绩:营收6.06亿元,连续5个季度保持盈利
2023年6月2日,水滴公司(NYSE:WDH)公布截至2023年3月31日的第一季度未经审计的财务业绩报告。数据显示,水
环球速读:南财观察丨东莞旧改变局:调整周期悄然而至,“政府指导+国企推进”或成新趋势
南财观察丨东莞旧改变局:调整周期悄然而至,“政府指导+国企推进”或成新趋势,政府,三旧,东莞市,旧村改造,
兰州公积金提取时限最新调整!
兰州公积金提取时限最新调整!,贷款,档案,购房,住房公积金,兰州中川国际机场
焦点报道:中航重机:拟定增募资不超22.12亿元
中航重机公告,公司拟向特定对象发行A股股票,募资总额不超221,200万元,扣除发行费用后的募资净额拟全部投
新动态:降首付、调限售,青岛打出楼市组合拳
降首付、调限售,青岛打出楼市组合拳,限购,降首付,调限售,青岛市,楼市新政,商品住房,首付比例,房地产政策,
视点!发布推迟 玛莎拉蒂Grecale将2022年首发
[本站资讯]日前,我们从玛莎拉蒂官方获悉,旗下全新的“入门版”中型SUV――玛莎拉蒂Grecale的首发日期将推
网商银行公益小店微电影郑州首映|环球快报
6月1日,由河南省农业农村厅、河南日报社指导,顶端新闻和网商银行主办的《我和我的小店》郑州公益观影会在
最新消息:四问美债危机
新华社记者美国国会参议院1日投票通过一项关于联邦政府债务上限和预算的法案,递交总统拜登签字后即可生效
万科前5月合同销售额1678亿 5月单月权益拿地金额85.5亿
万科前5月合同销售额1678亿5月单月权益拿地金额85 5亿,万科,地价,恒大集团,合同销售额,合同销售面积,中国交
每日热议!淘气天尊:市场突破这个点位,行情或将井喷!
周五上午市场呈现高开高走的格局,投资者可以看到,早盘沪指高开7点于3212点,创业板高开7点于2213点,早盘
银城国际前5个月合约销售额约37.16亿元 均价2.19万元/平米 当前快看
以此计算,银城国际控股今年前5个月合约销售额同比下降约27 29%,总合约建筑面积同比下降26 68%。
中国球员张之臻法网创造历史
北京时间6月1日晚,在2023法国网球公开赛男单第二轮比赛中,中国球员张之臻直落三盘以7:6(3)、6:3、6:4击败
微微一笑很倾城大结局结婚吻戏_微微一笑很倾城大结局 世界资讯
想必现在有很多小伙伴对于微微一笑很倾城大结局方面的知识都比较想要了解,那么今天小好小编就为大家收集了
17.76%!又又又溢价!福清一地块以2.52亿元成功出让 每日播报
17 76%!又又又溢价!福清一地块以2 52亿元成功出让,限地价,福清市,住宅用地
特斯拉市值一夜大涨 马斯克重返世界首富宝座 百事通
【特斯拉市值一夜大涨】5月30日,马斯克访华。国务委员兼外长秦刚会见马斯克时表示,中国将致力于为包括特
鼎捷软件股东户数下降3.85%,户均持股22.38万元
鼎捷软件最新股东户数2 5万户,低于行业平均水平。公司户均持有流通股份1 06万股;户均流通市值22 38万元。
朔城区人民政府通知!土地与房屋征收补偿···涉及华宝、三中、四中等片区
朔城区人民政府通知!土地与房屋征收补偿···涉及华宝、三中、四中等片区,朔城区,区政府,朔州市,人民政府
5月份债基涨幅冠军:博时裕坤3个月定开债上涨3.75%|资讯
5月份债基涨幅冠军:博时裕坤3个月定开债上涨3 75%
武乡399名青年志愿者助力八路军文化旅游节_全球观察
武乡399名青年志愿者助力八路军文化旅游节,主流媒体,山西门户。山西新闻网是经国务院新闻办审核批准,由山
全球头条:六年级下册英语单词朗读视频_六年级下册英语单词
想必现在有很多小伙伴对于六年级下册英语单词方面的知识都比较想要了解,那么今天小好小编就为大家收集了一
通讯!2023年5月泉州市区房企销售排行榜
2023年5月泉州市区房企销售排行榜,房源,城建,泉州市,商品住宅,保利地产,房地产行业
- 3467元/㎡起!赣州2宗重磅住宅地块挂牌! 当前看点
- 立体手工怎么做蝴蝶结_立体手工怎么做贺卡 天天速看料
- 以科技改善人类健康 智能品牌康小虎亮相2023硬科技嘉年华 天天实时
- 世界新动态:六水氯化镁商品报价动态(2023-06-02)
- 天天热消息:“六一新政”前后体验香港线下数字资产交易:新增实名认证
- 【全球播资讯】破防了!没想到海珠这个“高富帅”,还会心疼刚需
- 增长率计算公式excel_增长率-每日短讯
- 让服务者更有尊严 让居住更加美好——贝壳合肥站首届新经纪服务者荣誉盛典成功举办_环球看热讯
- 6宗地块释出规划,半年内成交土地陆续亮相
- 世界热点评!江苏银保监局召开全省银行业保险业深化“四保障六提升”行动推进(电视电话)会议
- 集中亮相!成都大运会会徽、吉祥物、火炬、奖牌长这样
- 天天热推荐:2022年阿里云再获金融云整体市场第一
- 国网资阳供电公司:大力推广电加热 老产业焕发新活力 天天快看
- 全球最资讯丨A股日报 | 6月2日沪指收涨0.79%,两市成交额达9406亿元
- 上海应用技术学院保卫处具体地点电子职业电话 环球观点
- 赵成才少将简历_赵成模|简讯
- 当前速看:法治宣传进校园 守护净土筑安全——凉山州、德昌县市场监管局联合开展法制宣讲进校园活动
- 每日看点!护航中高考 四川加强市场监管工作
- 5月政策月报|房地产政策延续宽松基调,频次有所下滑,湖南省直公积金实现商转公直转_世界新动态
- 环球百事通!践行美好心交付:绿城中原公司坚持全品质、高质量、可持续发展
- 尔康制药(300267.SZ):苏易康拟对其不动产权进行处置 涉及收储价格2000万元
- “甘美达”重磅亮相中男科学术盛会——中华医学会第二十二次全国男科学术会议
- 视讯!佳兆业集团5月交付近4000套房源
- 环球热推荐:cos大佬“一人千面”,高度还原迪士尼反派,网友:我佛了
- 组图:口岸繁忙 海关助力外贸稳增长_天天最新
- 海盗行径!美军又出动数十辆盗油车盗运叙利亚石油
- 被问到性生活,西蒙尼:每月少于4次就别在我的球队踢球 频率还行|天天微资讯
- 前海人寿广州总医院荣获国际医疗旅游试点示范基地称号
- 今日观点!简阳市市场监管局召开营商环境工作推进会
- 快看点丨佛山:多孩家庭租住商品住房年最高可提取额度超6万元
- 青岛楼市新政:放松限售、降首付、试点房票制 今日精选
- 广元市朝天区“三强化”全力做好“三考”期间安全保障工作
- 邻水县市场监管局 “异地打照”让企业更“近一步”
- 雁江区市场监管局开展“迎大运•保安全” 气瓶充装单位专项交叉检查-全球观热点
- 当前动态:养老新职业引来年轻人,90后花式服务“90后”
- 便民!中山市“以图查房”线上自助查询功能上线!
- 中南置地:1-5月累计交付22276套房屋 5月交付7558套|即时焦点
- 全球通讯!洛丽塔cla是什么意思_cla什么意思
- 速看:名扬中外的扬是什么意思_名扬中外的扬意思是什么
- 巴黎表示加尔蒂表述有误,应为梅西本赛季在王子公园最后一场比赛 微头条
- 林丹为李宗伟找到新工作!国羽天王挺讲究,网友:这工作意义重大
- 摩托罗拉甚至还没有宣布Razr40Ultra它的广告已经泄露|焦点精选
- 【原】不为失败找借口-每日热闻
- 【世界热闻】亿纬锂能:公司在储能领域布局较早,锂电池产品已广泛应用于通讯储能、电力储能等
- 河北沧州市新华区好育佳培训学校已退还网友电工证报名费|世界快报
- 《再见爱人》郭柯宇章贺最后在一起了吗_最好的爱也许是放手 速读
- ChatGPT考高三物理得0分_网友:偏科太严重
- 关注:青岛楼市新政!首付比例最低20%,特殊群体新房拿证两年可售
- wk生活平台最新2023清退消息:P2P新停顿!对于清退消息的全新告示_环球微头条
- 恒生指数收盘大涨逾4% 恒生科技指数飙升超5% 当前播报
- 被围猎的北京土拍,到底发生了什么
- 坐标兴智!G19地块规划设计方案来了 环球时快讯
- 当前速递!“互联网+”助力鲜花飘香海外 点亮“花卉经济”致富密码
- 香港5月楼宇买卖合约共5284份 同比下跌33.5%|重点聚焦
- 欧冠决赛主裁遭到投诉 欧足联密切关注 不排除更换裁判
- 宁静发布会现场回应“内娱完了” 大胆发言引发热议
- 链接资本与项目:“AI+元宇宙”企业家峰会暨川渝元宇宙三十人论坛共建启动大会成功举办,川渝元宇宙行业前景广阔
- 杭州育才中学全称_杭州育才中学地址
- 功勋模范丨他们,为祖国的花朵保驾护航 每日资讯
- 膏药适合治疗哪些病症和部位?仙佑医药膏药贴品牌怎么代理?
- 环球时讯:2023年深圳礼品展哈尔滨巡回展圆满结束
- 宝积资本高峰论坛第二届在佛山隆重召开
- 宝积资本(08168.HK)与广明大健康订立合作备忘录
- 环球短讯!首创证券:新房二手房环比走弱,首套二套利差持续扩大
- 每日报道:上海周贤地产一期在建工程等项目二次拍卖 起拍价15.41亿元
- 青岛楼市新政:非限购区域首套房首付比例最低调整为20%_微资讯
- 四川省食品安全委员会关于公布第四批四川省食品安全示范县(市、区)创建名单的通知
- 焦点观察:即将拆迁!大兴连发多则征地公告!补偿金额达...
- “夜经济”催生中国消费新“夜”态
- 全球热议:6月华南西南等地降水偏多 四川西南部等地气温偏高
- 运机集团6月2日快速上涨-今头条
- 哈伊高铁哈铁段开始箱梁建设-环球消息
- “护苗”进校园 护航成长路
- 健康产业引领者:武汉百年臻承推拿馆为您提供专业技术培训
- 萌虎“出山” 相约“六一”
- 融媒·画像丨星娃开画展——14岁自闭症少年的逐梦路 天天要闻
- 天天讯息:首届“萧红文化周”启幕
- 古墓丽影1攻略怎么拿弓箭 古墓丽影1攻略
- 绿色心情儿童节两城数店上演“回忆莎”,国民雪糕实现真正闭环营销
- 兰州新区房产领域矛盾化解实现“标准三统一” 观察
- 全球投资者大会③中金公司彭文生:数字经济时代 中国的规模经济优势将更加突出 精选
- 随着经济增速回落,中产家庭也要降低未来的投资预期_今日热讯
- 山东将全省推行不动产“带押过户”
- 11人斩获6项大奖,广元苍溪在全市职业技能竞赛中再创佳绩 世界快讯
- 恒生科技指数涨幅扩大至5%:腾讯涨超5%,携程快手涨超6%|全球要闻
- 今年快递业务量已达500亿件(新数据 新看点)
- 信用卡逾期会影响孩子吗?信用卡逾期一个月还能用吗?
- AI换脸新型诈骗来袭 双“法”齐下破解AI诈骗难题
- excel表格幂函数公式 幂函数公式-重点聚焦
- 轮到米哈游守江山了-环球观焦点
- 苇渡科技获数亿元A轮融资,2024年将量产新能源重卡_热资讯
- 【天天新视野】东易日盛6月2日盘中涨幅达5%
- 南部:这个“六一”,市场监管局为孩子们送上专属“礼物”
- 苏州市区372个存量住宅用地公布 未动工项目95个-环球快报
- 世界视点!翰博高新6月2日盘中跌幅达5%
- 天天速讯:信号!刚刚4城出台楼市新政,新一轮救市潮来势汹汹!徐州…
- 教育人才“组团式”帮扶在剑阁撒下“希望之种”
- 别样过“六一”,北川儿童节活动欢乐多
- 避免数字技术与教育评价融合走向误区 聚焦
- 超10万!2022年安徽平均工资发布
- 每日讯息!泸州市纳溪区市场监管:“五强化”护航中高考
- 每日热点:泸州市“五举措”开展茶叶过度包装专项治理行动
- 南京楼市,量价双杀了! 环球快讯
- 必应聊天放宽限制:每轮会话最多 30 次,每天上限提至 300 次
- 东鹏饮料去年合作经销商增两成,机构看好东鹏饮料 全球快看点
- 对话中关村科金张杰:通用大模型落地企服赛道,领域适配是门槛
- 新能源车挑战更多前沿赛道 全球微速讯
- 【金融机构债发行结果】23人保集团资本补充债01票面利率为3.2900% 环球速看
- 港股午评︱指数大反攻 恒科指大涨4.84% 科技股强劲 快手大涨近7%,百度、阿里涨超6%
- 世界微头条丨又要摇号了?南昌市本级5宗共481亩地上线,起拍总价约34.88亿元
- 下调中介费刚实施就撤销!福州市房地产中介行业协会深夜致歉
- 盐城师范学院分数线2021山东|天天微头条
- 【公司债新发公告】23穗港01今日发布发行公告 热文
- 通讯!戴尔发布第一季度财报:营收暴跌 20%,但优于分析师预期
- 雁江区市场监管局开展食品安全“微”课堂暨六一儿童节活动-今日最新
- 世界最资讯丨新疆部分宾馆酒店开展重大风险隐患专项排查整治
- 淘宝banner图尺寸 淘宝banner尺寸大小
- 今日要闻!位置优越!南沙这条村征地47亩,将新建住宅、学校!
- 【独家焦点】巴州区第二人民医院:开展儿童节特别团体心理辅导活动
- 独家专访 | 对话经济学家姚洋:面对现代社会的不确定性,经济学还有用吗?|封面天天见
- “贴身”“俯身”“转身” 建设对儿童友好的城市 今热点
- 主汛期!正式进入!
- 男性熬夜的危害远大于女性,三高风险明显上升!|快讯
- 热头条丨今年全国快递业务量已达500亿件
- 加格达奇林业局:坚持常态化巡查 守护生态资源安全_世界最新
- 全球微速讯:塔河县总工会开展“六一”特别关爱活动
- 今日要闻!6月3日起报名!无锡幼儿园报名攻略来了
- 天天快讯:2023年国家林草局科技活动周大兴安岭分会场活动在图强林业局启动
- 《一访定心》“一线倾听”:行道树上的“紧箍咒” 全球热点评
- 极萌Jmoon与周冬雨联手:引领极速科技美容行业发展趋势
- 环球通讯!LV在多哈开设首个品牌机场贵宾室
- 【世界时快讯】刚刚,今年全市首家创业板上市!
- 黑色系期货全线走高 焦点观察
- 教育部和各省(区、市)开通2023年高考举报电话
- 如何根据月线选股?板块选股技巧有哪些?
- 做长线怎么选股票?股票长线和短线有哪些区别?
- 筹码集中度怎么选股?成交量选股技巧有哪些?
- 极速科技美容行业新活力:极萌Jmoon选择周冬雨作为品牌全球代言人
- 组合条件选股技巧有哪些?基本面选股技巧有哪些?
- 【全球聚看点】难搓掉、难放心、难监管 警惕儿童纹身贴这“三难”
- 如何在弱市中选股?股票池怎么选股?
- 怎么运用均线形态选股?如何利用振幅进行选股?
- 怎么利用筹码峰选股票?股票筹码峰相关知识有哪些?
- 如何选择短线强势股?中短线选股如何选?
- 股票好坏是从哪些方面确认?如何根据股东的变化去选股?
- 尾盘30分钟选股法是什么?收盘前30分钟的选股要点有哪些?
- 怎么购买股票?股票选股五步法是什么?
- 主升浪启动前有哪些特征?主升浪中选啥股?
- 龙头股票怎么找?庄家怎么选股?
- 克而瑞:头部房企差距拉大,5月份拿地金额TOP20中仅3家民企