问题
资源不同步的问题对于插件开发的哥们应该都不是很陌生,记得刚到两年前刚接触插件开发的时候,由于产品中代码中很多都是用java.io.File
操作文件资源,导致经常有这种问题发生,例如删除不掉、内容更新失败等。下班之前,以资源删除失败为例子,写篇小随笔,把这个资源不同步的问题多少说道说道。
首先看个可能不陌生的错误(错误本质上都是不同步引起,但是可能包装形式很多):
下面是引起错误的代码:
public void run(IAction action) {
try {
//获取一个存在的文件
IFile eclipseFile = ResourcesPlugin.getWorkspace().getRoot().getProject("project").getFile(new Path("folder/file.txt"));
//用java IO更新底层资源
eclipseFile.getLocation().toFile().setLastModified(eclipseFile.getLocalTimeStamp() + 100);
//删除文件
eclipseFile.delete(false, null);
} catch (CoreException e) {
Activator.getDefault().getLog().log(new Status(IStatus.ERROR, "aaa", 99, "删除资源失败", e));
}
}
猜测
是不是我们用Java IO修改了文件,而Eclipse工作区不知道,这可能就是不同步?
分析
一个文件资源的状态描述,我们可以从两个层面来看:一个Eclipse工作区层面的资源状态;二是文件系统层面的资源状态。对于这两者(ResourceInfo
IFileInfo
),Eclipse资源管理模块中都有对应的类型支持。
ResourceInfo
ResourceInfo
:封装了工作区对一个文件资源的描述,也就是我们常说的工作区资源树上的一个数据节点,Eclipse资源管理中的IResource
系列接口本身也是ResourceInfo
的代理,ResourceInfo
主要操作如下:
ResourceInfo
的主要常规获取方式Workspace.getResourceInfo
:
public ResourceInfo getResourceInfo(IPath path, boolean phantom, boolean mutable) {
try {
if (path.segmentCount() == 0) {
ResourceInfo info = (ResourceInfo) tree.getTreeData();
Assert.isNotNull(info, "Tree root info must never be null"); //$NON-NLS-1$
return info;
}
ResourceInfo result = null;
if (!tree.includes(path))
return null;
if (mutable)
result = (ResourceInfo) tree.openElementData(path);
else
result = (ResourceInfo) tree.getElementData(path);
if (result != null && (!phantom && result.isSet(M_PHANTOM)))
return null;
return result;
} catch (IllegalArgumentException e) {
return null;
}
}
资源树的影子出来了,我们获取ResourceInfo
的过程其实就是在资源树上面定位对应数据节点的过程。那可以自然的猜测(有兴趣的Tx可以接着撒两眼资源树是如何实现的),ResourceInfo
的获取过程并不是每次都会产生一个新的ResourceInfo
实例,因为直觉告诉我们这可能是性能敏感的。
IFileInfo
IFileInfo
:一个资源在特定时间点上的状态快照(snapshot
),可以理解为一个底层文件系统资源对应的静态只读信息的集合。我们看一下它的获取方式,IFileStore.fetchInfo()
实现(有关IFleStore
这里就省略了)
public IFileInfo fetchInfo(int options, IProgressMonitor monitor) {
if (LocalFileNatives.usingNatives()) {
FileInfo info = LocalFileNatives.fetchFileInfo(filePath);
//natives don't set the file name on all platforms
if (info.getName().length() == 0)
info.setName(file.getName());
return info;
}
//in-lined non-native implementation
FileInfo info = new FileInfo(file.getName());
final long lastModified = file.lastModified();
if (lastModified <= 0) {
//if the file doesn't exist, all other attributes should be default values
info.setExists(false);
return info;
}
info.setLastModified(lastModified);
info.setExists(true);
info.setLength(file.length());
info.setDirectory(file.isDirectory());
info.setAttribute(EFS.ATTRIBUTE_READ_ONLY, file.exists() && !file.canWrite());
info.setAttribute(EFS.ATTRIBUTE_HIDDEN, file.isHidden());
return info;
}
可以看出来,每次获取IFileInfo的过程是每次都产生新的实例,这个新的实例来描述该时间点上的文件状态。
到这里我们知道了,ResourceInfo
其实是内存中Eclipse维护的一个东东,IFileInfo
是实时获取的,那么在特定的时间点上面,前者有可能和后者不统一。 这是不是就是不同步问题的根源呢?
资源不同步检查
为了验证上面的猜测,我们追踪调试一下文章开头提到的测试代码:
找到了检查文件资源是否同步的代码,FileSystemResourceManager.isSynchronized(IResource target, int depth)
,资源树在检查一个资源是否同步时候(IResourceTree.isisSynchronized(IResource target, int depth)
),也是委托给了FileSystemResourceManager
:
public boolean isSynchronized(IResource target, int depth) {
switch (target.getType()) {
case IResource.ROOT :
if (depth == IResource.DEPTH_ZERO)
return true;
//check sync on child projects.
depth = depth == IResource.DEPTH_ONE ? IResource.DEPTH_ZERO : depth;
IProject[] projects = ((IWorkspaceRoot) target).getProjects();
for (int i = 0; i < projects.length; i++) {
if (!isSynchronized(projects[i], depth))
return false;
}
return true;
case IResource.PROJECT :
if (!target.isAccessible())
return true;
break;
case IResource.FILE :
if (fastIsSynchronized((File) target))
return true;
break;
}
IsSynchronizedVisitor visitor = new IsSynchronizedVisitor(Policy.monitorFor(null));
UnifiedTree tree = new UnifiedTree(target);
try {
tree.accept(visitor, depth);
} catch (CoreException e) {
Policy.log(e);
return false;
} catch (IsSynchronizedVisitor.ResourceChangedException e) {
//visitor throws an exception if out of sync
return false;
}
return true;
}
我们接着看一下上面负责File级别同步检查的FileSystemResourceManager.fastIsSynchronized
方法:
public boolean fastIsSynchronized(File target) {
ResourceInfo info = target.getResourceInfo(false, false);
if (target.exists(target.getFlags(info), true)) {
IFileInfo fileInfo = getStore(target).fetchInfo();
if (!fileInfo.isDirectory() && info.getLocalSyncInfo() == fileInfo.getLastModified())
return true;
}
return false;
}
注意:info.getLocalSyncInfo() == fileInfo.getLastModified())
告诉我们,对于一个文件级别的资源,判断是否同步就是检查Eclipse维护的时间戳和底层文件系统的时间戳是否一致!!!
小结
如果用Eclipse IResource
API来修改文件资源,Eclipse自己会知道;如果用java IO
或者java NIO
来修改文件资源,Eclipse就一无所知,状态维护就会出问题。不知道就出事情了,不同步只是后果之一,变化跟踪也将失效,resource change event
也将无从产生了
反思
-
对于非File级别的资源,为什么同步检查不是很严格呢?
因为在不同操作系统不同类型分区上面,一个文件夹下面的文件资源被修改了,文件夹的时间戳并不保证会及时更新。这是很底层的东西了,就不接着讲了。 如果你想修正这个问题,你可以对目录时间戳做自己的维护(这是可行的,我们产品里面就是这么干的)。 -
ResourceInfo
和IFileInfo
好像不怎么使用?
确实,Ecipse
也不想让开发者去直接使用它。例如:getResourceInfo
是在Resource
中提供的,而不是在IResource
接口中定义的。 -
那如何同步呢?
IResource.refreshLocal(int depth, IProgressMonitor monitor)
- depth 同步深度,可取值有三个
IResource.DEPTH_ZERO
、IResource.DEPTH_ONE
、IResource.DEPTH_INFINITE
。具体含义可参考IResource
中相应文档。 - monitor 进度条,所有实现自
IProgressMonitor
的进度条都可以,null
表示静默同步,即不显示进度条。
常用示例
// 设置同步深度: 文件及其子文件
int depth = IResource.DEPTH_INFINITE;
// 设置进度条: 静默同步
IProgressMonitor monitor = null;
ResourcesPlugin.getWorkspace().getRoot().refreshLocal(depth,monitor)