問題
資源不同步的問題對于插件開發的哥們應該都不是很陌生,記得剛到兩年前剛接觸插件開發的時候,由于産品中代碼中很多都是用
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
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
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)