android M Launcher之LauncherModel (二)
上一篇我们通过LauncherModel的创建 ,实例化,以及与LauncherModel之间的沟通方式。初步了解了LauncherModel一些功能及用法,如果对LauncherModel一系列初始化动作还不了解的可以看
android M Launcher之LauncherModel (一)
好了 接下来我们继续分析,大家都知道 LauncherModel是Launcher的数据中心,但是数据中心的数据是怎么加载出来的呢,这里就要说到LoaderTask了,它是LauncherModel的核心任务。
1、LoaderTask的定义、属性及构造
要了解一个类的功能和作用,我们通常从该类的定义及其定义的成员变量开始,就好比了解一个人最先看到的肯定是他的外貌了。
private class LoaderTask implements Runnable {
private Context mContext;
boolean mIsLoadingAndBindingWorkspace;
private boolean mStopped;
boolean mLoadAndBindStepFinished;
}
由定义可以看出LoaderTask是一个实现Runnable 接口的类,我们都知道Runnable 是一个任务,它可以被抛到线程中执行,也可以被抛到进程的处理器中执行,通过实现Runnable接口来实现多线程以及线程间的通信是比较常用的做法。
LoaderTask的执行需要Launcher应用程序提供必要的支持,而外界也需要知道Loaderask的执行状态,因此在LoaderTask中,以成员变量的方式保存了LoaderTask执行所必需的支持以及自身相关的状态。
mIsLoadingAndBindingWorkspace :它是LoaderTask执行动作的指示性变量,当正在加载和绑定桌面数据时它为true,当动作执行完成后它为false。
mStopped :标示整个加载任务是否已经被强制停止,LoaderTask对外提供了一个停止加载的接口,如果外部通过这个接口来停止一个加载任务,那么它将为true,默认是false
mLoadAndBindStepFinished : 通过它可以知道整个加载任务所涉及的动作已经执行完毕
2、LoaderTask的run接口实现
LoaderTask是一个Run那边了接口的实现,每一个Runnable接口的实现都需要实现run方法,LoaderTask也不例外,当LoaderTask作为一个任务在某一个线程中运行时,实际就是这个线程调用了它的run方法
public void run() {
synchronized (mLock) {
if (mStopped) {
return;
}
mIsLoaderTaskRunning = true;
}
// Optimize for end-user experience: if the Launcher is up and // running with the
// All Apps interface in the foreground, load All Apps first. Otherwise, load the
// workspace first (default).
keep_running:
{
if (DEBUG_LOADERS) Log.d(TAG, "step 1: loading workspace");
loadAndBindWorkspace();
if (mStopped) {
break keep_running;
}
waitForIdle();
// second step
if (DEBUG_LOADERS) Log.d(TAG, "step 2: loading all apps");
loadAndBindAllApps();
}
// Clear out this reference, otherwise we end up holding it until all of the
// callback runnables are done.
mContext = null;
synchronized (mLock) {
// If we are still the last one to be scheduled, remove ourselves.
if (mLoaderTask == this) {
mLoaderTask = null;
}
mIsLoaderTaskRunning = false;
mHasLoaderCompletedOnce = true;
}
}
这里主要做了两件事
1、loading workspace
2、loading all apps
其他的就是一些成员变量的赋值。
首先loading workspace 我们画流程图来看:
private void loadAndBindWorkspace() {
mIsLoadingAndBindingWorkspace = true;
// Load the workspace
if (DEBUG_LOADERS) {
Log.d(TAG, "loadAndBindWorkspace mWorkspaceLoaded=" + mWorkspaceLoaded);
}
if (!mWorkspaceLoaded) {
loadWorkspace();
synchronized (LoaderTask.this) {
if (mStopped) {
return;
}
mWorkspaceLoaded = true;
}
}
// Bind the workspace
bindWorkspace(-1);
}
结合上面代码 可以发现 它又分为两步
1、生成桌面数据loadWorkspace;
2、绑定桌面配置数据bindWorkspace。
loadWorkspace
Launcher桌面的数据主要包括来自Launcher数据库的各种表,loadWorkspace方法将负责从这些数据库表中读取数据并转译为Launcher桌面项的数据结构。
这个代码比较多同样先上流程图。
- 获取系统服务一节设备属性并调整桌面页顺序
在加载数据桌面数据前,loadWorkspace 方法必需获取到必要的支持,因为loadWorkspace方法主要处理应用程序包的相关信息,所以首先要获取包管理服务,其次
需要确定快捷方式或者桌面小部件的位置,因此它还需要获取屏幕的宽和高代码如下:
final Context context = mContext;
final ContentResolver contentResolver = context.getContentResolver();
//获取包管理服务,并查询当前是否处于安全模式
final PackageManager manager = context.getPackageManager();
final boolean isSafeMode = manager.isSafeMode();
//获取Launcher定制的应用程序管理接口
final LauncherAppsCompat launcherApps = LauncherAppsCompat.getInstance(context);
final boolean isSdCardReady = context.registerReceiver(null,
new IntentFilter(StartupReceiver.SYSTEM_READY)) != null;
//获取桌面有多少行,多少列
LauncherAppState app = LauncherAppState.getInstance();
InvariantDeviceProfile profile = app.getInvariantDeviceProfile();
int countX = profile.numColumns;
int countY = profile.numRows;
if (MigrateFromRestoreTask.ENABLED && MigrateFromRestoreTask.shouldRunTask(mContext)) {
long migrationStartTime = System.currentTimeMillis();
Log.v(TAG, "Starting workspace migration after restore");
try {
//根据实际情况调整桌面页的顺序
MigrateFromRestoreTask task = new MigrateFromRestoreTask(mContext);
// Clear the flags before starting the task, so that we do not run the task
// again, in case there was an uncaught error.
MigrateFromRestoreTask.clearFlags(mContext);
task.execute();
} catch (Exception e) {
Log.e(TAG, "Error during grid migration", e);
// Clear workspace.
mFlags = mFlags | LOADER_FLAG_CLEAR_WORKSPACE;
}
Log.v(TAG, "Workspace migration completed in "
+ (System.currentTimeMillis() - migrationStartTime));
}
- 根据输入的标志对数据进行必要的预处理
在不同的场景下,LoadWorkspace可能要对需要处理的数据来源进行必要的处理,这取决于mFlags 标志,代码如下:
//如果mFlags中包含LOADER_FLAG_CLEAR_WORKSPACE,则清理原有的数据
if ((mFlags & LOADER_FLAG_CLEAR_WORKSPACE) != 0) {
Launcher.addDumpLog(TAG, "loadWorkspace: resetting launcher database", true);
LauncherAppState.getLauncherProvider().deleteDatabase();
}
// Make sure the default workspace is loaded
//确保默认数据得以加载
Launcher.addDumpLog(TAG, "loadWorkspace: loading default favorites", false);
LauncherAppState.getLauncherProvider().loadDefaultFavoritesIfNecessary();
- 清理历史数据并准备必要数据
在每次加载桌面数据时,需要对这些数据区域进行清理 这个平时我们往listview中添加数据前要清理一样的道理, 它主要在clearSBgDataStructures
中完成
/**
* Clears all the sBg data structures
*/
private void clearSBgDataStructures() {
synchronized (sBgLock) {
sBgWorkspaceItems.clear();//清理桌面项列表
sBgAppWidgets.clear();//清理桌面快捷方式列表
sBgFolders.clear();//清理文件夹数据列表
sBgItemsIdMap.clear();//清理桌面项字段
sBgWorkspaceScreens.clear();//清理桌面页记录列表
}
}
完成清理后loadWorkspace方法需要知道当前设备中安装了多少应用程序,除了这些外,在加载桌面桌面数据的过程中会产出一些过程数据,他们保持在其他区域代码如下:
final HashMap<String, Integer> installingPkgs = PackageInstallerCompat
.getInstance(mContext).updateAndGetActiveSessionCache();
sBgWorkspaceScreens.addAll(loadWorkspaceScreensDb(mContext));
final ArrayList<Long> itemsToRemove = new ArrayList<Long>();
final ArrayList<Long> restoredRows = new ArrayList<Long>();
有了上面这些基础后 ,接下来就开始加载数据了。
- 查询Favorites表数据库并准备桌面占用情况标志映射
final Uri contentUri = LauncherSettings.Favorites.CONTENT_URI;
if (DEBUG_LOADERS) Log.d(TAG, "loading model from " + contentUri);
final Cursor c = contentResolver.query(contentUri, null, null, null, null);
// +1 for the hotseat (it can be larger than the workspace)
// Load workspace in reverse order to ensure that latest items are loaded first (and
// before any earlier duplicates)
final LongArrayMap<ItemInfo[][]> occupied = new LongArrayMap<>();
- 从Cursor中获取桌面项类型itemType等数据项
int itemType = c.getInt(itemTypeIndex);//桌面项类型
boolean restored = 0 != c.getInt(restoredIndex);//是否已经恢复
boolean allowMissingTarget = false;
- 应用程序入口及其他入口快捷方式的处理
在这里Launcher需要处理桌面上快捷方式的记录,将其转换成Launcher可以处理的对象。如下图
处理文件夹记录处理
桌面文件只需要处理好文件夹的标题,位置以及所处的容器即可。
桌面小部件记录处理
这里或处理桌面快捷方式类似,可以参考处理桌面快捷方式的流程来看处理桌面小部件。
从数据库中清理需要删除的数据项
通过以上处理步骤,可能产生依稀需要删除的记录如下:
if (itemsToRemove.size() > 0) {
// Remove dead items
contentResolver.delete(LauncherSettings.Favorites.CONTENT_URI,
Utilities.createDbSelectionQuery(
LauncherSettings.Favorites._ID, itemsToRemove), null);
if (DEBUG_LOADERS) {
Log.d(TAG, "Removed = " + Utilities.createDbSelectionQuery(
LauncherSettings.Favorites._ID, itemsToRemove));
}
// Remove any empty folder
for (long folderId : LauncherAppState.getLauncherProvider()
.deleteEmptyFolders()) {
sBgWorkspaceItems.remove(sBgFolders.get(folderId));
sBgFolders.remove(folderId);
sBgItemsIdMap.remove(folderId);
}
}
// Sort all the folder items and make sure the first 3 items are high resolution.
for (FolderInfo folder : sBgFolders) {
Collections.sort(folder.contents, Folder.ITEM_POS_COMPARATOR);
int pos = 0;
for (ShortcutInfo info : folder.contents) {
if (info.usingLowResIcon) {
info.updateIcon(mIconCache, false);
}
pos++;
if (pos >= FolderIcon.NUM_ITEMS_IN_PREVIEW) {
break;
}
}
}
- 置恢复状态为以恢复
有些记录可能来自备份与恢复的过程,如果当前处理的记录属于这种,并且已经得到完整的恢复那么这些记录ID都会被缓存在restoredRows中,在处理往需要删除的记录后,就要将restoredRows中指定记录恢复状态置为0.
if (restoredRows.size() > 0) {
// Update restored items that no longer require special handling
ContentValues values = new ContentValues();
values.put(LauncherSettings.Favorites.RESTORED, 0);
contentResolver.update(LauncherSettings.Favorites.CONTENT_URI, values,
Utilities.createDbSelectionQuery(
LauncherSettings.Favorites._ID, restoredRows), null);
}
- 处理安装在SDCARD上的应用程序
if (!isSdCardReady && !sPendingPackages.isEmpty()) {
context.registerReceiver(new AppsAvailabilityCheck(),
new IntentFilter(StartupReceiver.SYSTEM_READY),
null, sWorker);
}
当sPendingPackages中有了记录并且SDCARD没有挂载好的时候,Launcher注册相关的广播接收器等待SDCARD的挂载完成。
好了 LoadWorkspace分析就完成了,真心不容易呀。
再接再厉 我们看bindWorkspace
上面我们已经准备好了需要加载的数据,那么接下来需要做的就是将这些数据发送到Launcher,让它将这些数据转化为一个个可以显示的view,这个过程由bindWorkspace来完成。
private void bindWorkspace(int synchronizeBindPage)
- synchronizeBindPage 如果它的值小于0说明需要对所有桌面页进行刷新,如果大于等于0,则对指定页进行刷新。
在画图看下bindWorkspace的流程。发现Xmind用的越来越6了 O(∩_∩)O哈哈~
在加载桌面数据的过程中,已经把需要加载的数据分门别类的放在不同的数据缓冲区。由于LauncherModel的LoaderTask可以因为LauncherModel执行不同的任务而被多次实例化,这将会引起缓冲区数据共享问题,为了解决这个问题每次绑定数据的时候都要临时将数据缓冲区的数据备份,代码如下:
// Save a copy of all the bg-thread collections
//桌面数据项列表
ArrayList<ItemInfo> workspaceItems = new ArrayList<ItemInfo>();
//桌面小部件数据项列表
ArrayList<LauncherAppWidgetInfo> appWidgets =
new ArrayList<LauncherAppWidgetInfo>();
//经过排序的桌面页索引列表
ArrayList<Long> orderedScreenIds = new ArrayList<Long>();
final LongArrayMap<FolderInfo> folders;
final LongArrayMap<ItemInfo> itemsIdMap;
//缓冲区数据复制
synchronized (sBgLock) {
workspaceItems.addAll(sBgWorkspaceItems);
appWidgets.addAll(sBgAppWidgets);
orderedScreenIds.addAll(sBgWorkspaceScreens);
folders = sBgFolders.clone();
itemsIdMap = sBgItemsIdMap.clone();
}
//获取Launcher当前所处的页面索引
final boolean isLoadingSynchronously =
synchronizeBindPage != PagedView.INVALID_RESTORE_PAGE;
int currScreen = isLoadingSynchronously ? synchronizeBindPage :
oldCallbacks.getCurrentWorkspaceScreen();
if (currScreen >= orderedScreenIds.size()) {
// There may be no workspace screens (just hotseat items and an empty page).
currScreen = PagedView.INVALID_RESTORE_PAGE;
}
final int currentScreen = currScreen;
final long currentScreenId = currentScreen < 0
? INVALID_SCREEN_ID : orderedScreenIds.get(currentScreen);
分离当前页面与其他页面的数据并通知Launcher加载开始
// Separate the items that are on the current screen, and all the other remaining items
//分类数据区定义
ArrayList<ItemInfo> currentWorkspaceItems = new ArrayList<ItemInfo>();
ArrayList<ItemInfo> otherWorkspaceItems = new ArrayList<ItemInfo>();
ArrayList<LauncherAppWidgetInfo> currentAppWidgets =
new ArrayList<LauncherAppWidgetInfo>();
ArrayList<LauncherAppWidgetInfo> otherAppWidgets =
new ArrayList<LauncherAppWidgetInfo>();
LongArrayMap<FolderInfo> currentFolders = new LongArrayMap<>();
LongArrayMap<FolderInfo> otherFolders = new LongArrayMap<>();
//分类桌面项数据
filterCurrentWorkspaceItems(currentScreenId, workspaceItems, currentWorkspaceItems,
otherWorkspaceItems);
//分类小部件数据
filterCurrentAppWidgets(currentScreenId, appWidgets, currentAppWidgets,
otherAppWidgets);
filterCurrentFolders(currentScreenId, itemsIdMap, folders, currentFolders,
otherFolders);
//对数据进行排序
sortWorkspaceItemsSpatially(currentWorkspaceItems);
sortWorkspaceItemsSpatially(otherWorkspaceItems);
// Tell the workspace that we're about to start binding items
//在主线程上执行通知Launcher绑定开始任务
r = new Runnable() {
public void run() {
Callbacks callbacks = tryGetCallbacks(oldCallbacks);
if (callbacks != null) {
callbacks.startBinding();
}
}
};
runOnMainThread(r);
LoaderTask在绑定数据的过程中会产生不同的过程状态信息,这些信息会通过回调接口通知LauncherModel对数据处理的状态信息,比如绑定数据之前,会通过 startBinding接口通知Launcher准备开始绑定数据,在绑定数据结束时通过Launcher实现的finishBindingItems通知Launcher数据绑定完成等。这些回调方法的实现绝大多数需要处理界面上的view因此他们都需要在UI线程中。
通过结束绑定为例来说明下这个过程:
// Tell the workspace that we're done binding items
//实现包含了结束绑定通知的任务
r = new Runnable() {
public void run() {
Callbacks callbacks = tryGetCallbacks(oldCallbacks);
if (callbacks != null) {
callbacks.finishBindingItems();
}
//设置加载以及绑定任务结束标志
mIsLoadingAndBindingWorkspace = false;
// Run all the bind complete runnables after workspace is bound.
if (!mBindCompleteRunnables.isEmpty()) {
synchronized (mBindCompleteRunnables) {
for (final Runnable r : mBindCompleteRunnables) {
runOnWorkerThread(r);
}
mBindCompleteRunnables.clear();
}
}
// If we're profiling, ensure this is the last thing in the queue.
if (DEBUG_LOADERS) {
Log.d(TAG, "bound workspace in "
+ (SystemClock.uptimeMillis() - t) + "ms");
}
}
};
if (isLoadingSynchronously) {
synchronized (mDeferredBindRunnables) {
mDeferredBindRunnables.add(r);
}
} else {
//在主线程中执行该任务
runOnMainThread(r);
}
在完成了上面一系列操作后,最新的数据就完成了从数据库记录到桌面上可见的图标的转换。
终于到loading all apps了
它的实现方法为loadAndBindAllApps()
这个方法没有参数也没有返回值 它只是一个主干通过调用其他两个方法来完成应用程序猜的的数据加载及绑定。
加载所有应用程序菜单的过程和加载桌面的过程一样,都是由加载数据和绑定数据。
当第一次对应用程序菜单数据进行处理的时候,需要将这两个过程整合执行,如果之前已经加载了应用程序加载任务,只需要执行绑定动作。
和加载桌面一样 我们首先看下loadAllApps()
- 获取账户并清理缓存
android系统提供了多账户的概念,不同的账户下可以使用的应用程序是不同的,因此Launcher需要注意这个细节,在不同账户下处理不同的应用程序列表信息,所有在加载应用程序列表的时候需要获取当前设备上的所有账户。
final List<UserHandleCompat> profiles = mUserManager.getUserProfiles();
// Clear the list of apps
mBgAllAppsList.clear();
- 获取应用程序入口信息
Android5.0后提供了LauncherApps的服务,所以只需要通过LauncherApps就可以获取到应用程序的入口信息了
final List<LauncherActivityInfoCompat> apps = mLauncherApps.getActivityList(null, user);
- 将应用程序信息加入缓存区
到这里,LauncherModel就查询到了需要处理的数据,为了提高效率LauncherModel队这些数据提供了临时保存的缓存区 如下代码:
// Create the ApplicationInfos
for (int i = 0; i < apps.size(); i++) {
LauncherActivityInfoCompat app = apps.get(i);
// This builds the icon bitmaps.
mBgAllAppsList.add(new AppInfo(mContext, app, user, mIconCache));
}
- 按账户保存查询到的应用列表
通过ManagedProfileHeuristic工具将查询到的数据分类保存到共享文件中如下代码:
//创建与该用户相关联的筛选器实例
final ManagedProfileHeuristic heuristic = ManagedProfileHeuristic.get(mContext, user);
if (heuristic != null) {
//创建按账户分类应用程序的任务
final Runnable r = new Runnable() {
@Override
public void run() {
heuristic.processUserApps(apps);
}
};
//在UI线程中执行这个任务
runOnMainThread(new Runnable() {
@Override
public void run() {
// Check isLoadingWorkspace on the UI thread, as it is updated on
// the UI thread.
if (mIsLoadingAndBindingWorkspace) {
synchronized (mBindCompleteRunnables) {
mBindCompleteRunnables.add(r);
}
} else {
runOnWorkerThread(r);
}
}
});
- 绑定应用程序菜单数据
将需要加载到应用程序菜单中的数据完成分类后,紧接着就需要将数据发送到Launcher中处理 如下:
mHandler.post(new Runnable() {
public void run() {
final long bindTime = SystemClock.uptimeMillis();
final Callbacks callbacks = tryGetCallbacks(oldCallbacks);
if (callbacks != null) {
callbacks.bindAllApplications(added);
if (DEBUG_LOADERS) {
Log.d(TAG, "bound " + added.size() + " apps in "
+ (SystemClock.uptimeMillis() - bindTime) + "ms");
}
} else {
Log.i(TAG, "not binding apps: no Launcher activity");
}
}
});
剩下的onlyBindAllApps()方法做的事情 和上面的绑定应用程序菜单数据是一样的。
OK 至此我们已经把加载绑定桌面和应用程序的流程都走完了。好累
android M Launcher之LauncherModel (二)的更多相关文章
- android M Launcher之LauncherModel (三)
通过前两篇的分析,我们已经知道了LauncherModel的初始化及工作流程,如果您还不熟悉的话请看前两篇博文 android M Launcher之LauncherModel (一) android ...
- android M Launcher之LauncherModel (一)
众所周知 LauncherModel在Launcher中所占的位置,它相当于Launcher的数据中心,Launcher的桌面以及应用程序菜单中所需的数据像 桌面小部件的信息.快捷方式信息.文件信息. ...
- Bmob移动后端云服务平台--Android从零開始--(二)android高速入门
Bmob移动后端云服务平台--Android从零開始--(二)android高速入门 上一篇博文我们简介何为Bmob移动后端服务平台,以及其相关功能和优势. 本文将利用Bmob高速实现简单样例,进一步 ...
- Android系列之网络(二)----HTTP请求头与响应头
[声明] 欢迎转载,但请保留文章原始出处→_→ 生命壹号:http://www.cnblogs.com/smyhvae/ 文章来源:http://www.cnblogs.com/smyhvae/p/ ...
- Android系列之Fragment(二)----Fragment的生命周期和返回栈
[声明] 欢迎转载,但请保留文章原始出处→_→ 生命壹号:http://www.cnblogs.com/smyhvae/ 文章来源:http://www.cnblogs.com/smyhvae/p/ ...
- Android之自定义生成彩色二维码
先导个zxing.jar包 下面是xml布局 activity_main.xml <RelativeLayout xmlns:android="http://schemas.andro ...
- Android 仿PhotoShop调色板应用(二) 透明度绘制之AlphaPatternDrawable
版权声明:本文为博主原创文章,未经博主允许不得转载. Android 仿PhotoShop调色板应用(二) 透明度绘制之AlphaPatternDrawable 这里讲一下如何实现PS调色板中的透明度 ...
- Android图表库MPAndroidChart(十二)——来点不一样的,正负堆叠条形图
Android图表库MPAndroidChart(十二)--来点不一样的,正负堆叠条形图 接上篇,今天要说的,和上篇的类似,只是方向是有相反的两面,我们先看下效果 实际上这样就导致了我们的代码是比较类 ...
- Android源码浅析(二)——Ubuntu Root,Git,VMware Tools,安装输入法,主题美化,Dock,安装JDK和配置环境
Android源码浅析(二)--Ubuntu Root,Git,VMware Tools,安装输入法,主题美化,Dock,安装JDK和配置环境 接着上篇,上片主要是介绍了一些安装工具的小知识点Andr ...
随机推荐
- Windows下使用console线连接思科交换机
在XP下可以直接使用内置工具"超级终端",在win7或者更高版本需要下载安装SecureCRT. 本文假设已经下载安装好了SecureCRT. 首先,将电脑连接console线.因 ...
- requests之一:HTTP OAUTH认证(1)图形解释流程
- 设置python爬虫IP代理(urllib/requests模块)
urllib模块设置代理 如果我们频繁用一个IP去爬取同一个网站的内容,很可能会被网站封杀IP.其中一种比较常见的方式就是设置代理IP from urllib import request proxy ...
- angular的时间指令 以及防止闪烁问题
1.点击事件 <!doctype html><html lang="en"><head> <meta charset="UTF- ...
- [LeetCode] Monotone Increasing Digits 单调递增数字
Given a non-negative integer N, find the largest number that is less than or equal to N with monoton ...
- Pycharm节能模式
如题,开启节能模式代码不会自动补全.
- [NOI 2001]炮兵阵地
Description 司令部的将军们打算在N*M的网格地图上部署他们的炮兵部队.一个N*M的地图由N行M列组成,地图的每一格可能是山地(用“H” 表示),也可能是平原(用“P”表示),如下图.在每一 ...
- bzoj 2339: [HNOI2011]卡农
Description Solution 比较难想.... 我们先考虑去掉无序的这个条件,改为有序,最后除 \(m!\) 即可 设 \(f[i]\) 表示前\(i\)个合法集合的方案数 明确一点: 如 ...
- ●BZOJ 4541 [Hnoi2016]矿区
题链: http://www.lydsy.com/JudgeOnline/problem.php?id=4541 题解: 平面图的对偶图,dfs树 平面图的对偶图的求法: 把所有双向边拆为两条互为反向 ...
- 【luogu3384】【模板】树链剖分
省选被暴虐,成功爆0...顺便ditoly差点全省总分Rank1 orz..... 于是开始赶进度学新算法.... 然后决定开始学习树剖orz... 发现树剖很好用啊!!!! 然后做了模板题. 题目就 ...