博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Glide-源码分析(二)
阅读量:7063 次
发布时间:2019-06-28

本文共 20203 字,大约阅读时间需要 67 分钟。

前言

前面一篇文章对Glide第一次加载网络图片的流程通过源码的方式一步步的带大家看了下,至少对Glide的整个框架有了一个大致的认识。这篇文章我们继续深入。

概要

这一篇主要介绍下,第一次加载图片缓存磁盘后,重新启动app,再次加载的时候,从磁盘读取的过程。中间还会涉及到Glide其他的一些源码内容。

正文

从磁盘加载文件,那么肯定就有把图片存起来的这个步骤。我们先来看看Glide是如何把图片存起来的。

上篇文章已经简单的介绍了,就是图片加载成功之后重新又调用了SourceGeneratorstartNext方法。

public boolean startNext() {    if (dataToCache != null) {      Object data = dataToCache;      dataToCache = null;      cacheData(data);    }...  }复制代码

重点就是看下cacheData这个方法,如何去缓存图片。上一篇文章,重在流程,所以一笔带过了,这次我们就继续深入。

private void cacheData(Object dataToCache) {    long startTime = LogTime.getLogTime();    try {      Encoder encoder = helper.getSourceEncoder(dataToCache);      DataCacheWriter writer =          new DataCacheWriter<>(encoder, dataToCache, helper.getOptions());      originalKey = new DataCacheKey(loadData.sourceKey, helper.getSignature());      helper.getDiskCache().put(originalKey, writer);     ...  }复制代码

其实最主要的就是这4行代码,我们一个个来解析。

  1. 获取编码器
Encoder
getSourceEncoder(X data) throws Registry.NoSourceEncoderAvailableException { return glideContext.getRegistry().getSourceEncoder(data); }...public
Encoder
getSourceEncoder(@NonNull X data) throws NoSourceEncoderAvailableException { Encoder
encoder = encoderRegistry.getEncoder((Class
) data.getClass()); if (encoder != null) { return encoder; } throw new NoSourceEncoderAvailableException(data.getClass()); }...public synchronized
Encoder
getEncoder(@NonNull Class
dataClass) { for (Entry
entry : encoders) { if (entry.handles(dataClass)) { return (Encoder
) entry.encoder; } } return null; }复制代码

一步步跟入,就是这3个方法,总结来说,就是一开始的时候在EncoderRegistry对象中注册多个编码器,通过key-value的形式保存起来,可以通过传入的数据类型来获取到自己对应的编码器。

这里就直接说下结果。前面网络请求返回对象是ContentLengthInputStream 类型的。

自己debug一下,就会比较清楚,encoders里面注册了2个Encoder,分别用来解析ByteBufferInputStream

很显然,这里返回了StreamEncoder来处理。

  1. 创建DataCacheWriter 把第一步获取到的StreamEncoder当作参数传入。只是创建个对象,没什么好说的。

  2. 创建缓存的DataCacheKey key-value的方式缓存数据,这里的key就是DataCacheKey

  3. 缓存数据 这里就是重点了,开始进行磁盘缓存数据。

helper.getDiskCache().put(originalKey, writer);复制代码

首先我们要知道helper.getDiskCache()获取到的对象是什么。因为三方框架中会使用很多接口,所以我们有时间直接看代码并不能马上知道具体对应是哪个实现。 这里有2个办法。

  1. debug跟入直接看
  2. 不停往前找,用ctrl+左键一直往前找引用,找赋值的地方

这里我就不带大家这样一步步找了,直接看关键的几个地方。 DecodeHelper->diskCacheProvider=> DecodeJob->diskCacheProvider=> DecodeJobFactory->diskCacheProvider=> Engine->diskCacheProvider这里就发现了

this.diskCacheProvider = new LazyDiskCacheProvider(diskCacheFactory);复制代码

但是这里又有一个参数diskCacheFactory 我们还需要看这个是那里来的,继续往前找 Engine->diskCacheFactory=> GlideBuilder->diskCacheFactory这里发现了

if (diskCacheFactory == null) {      diskCacheFactory = new InternalCacheDiskCacheFactory(context);    }复制代码

在构建Glide对象的时候如果没有传入diskCacheFactory,那么这里就会默认生成一个InternalCacheDiskCacheFactory

好了这样我们就可以重新再来看下

helper.getDiskCache().put(originalKey, writer);复制代码

其实就是

helper.getDiskCache().put(originalKey, writer);...DiskCache getDiskCache() {    return diskCacheProvider.getDiskCache();  }... @Override    public DiskCache getDiskCache() {      if (diskCache == null) {        synchronized (this) {          if (diskCache == null) {            diskCache = factory.build();          }          if (diskCache == null) {            diskCache = new DiskCacheAdapter();          }        }      }      return diskCache;    }复制代码

前面我们已经一鼓作气,把factory也找出来了,也就是InternalCacheDiskCacheFactory

public final class InternalCacheDiskCacheFactory extends DiskLruCacheFactory//build方法在DiskLruCacheFactory里面public DiskCache build() {    File cacheDir = cacheDirectoryGetter.getCacheDirectory();    if (cacheDir == null) {      return null;    }    if (!cacheDir.mkdirs() && (!cacheDir.exists() || !cacheDir.isDirectory())) {      return null;    }    return DiskLruCacheWrapper.create(cacheDir, diskCacheSize);  }复制代码

这里就是创建缓存的文件。

public final class InternalCacheDiskCacheFactory extends DiskLruCacheFactory {    public InternalCacheDiskCacheFactory(Context context) {        this(context, "image_manager_disk_cache", 262144000L);    }    public InternalCacheDiskCacheFactory(Context context, long diskCacheSize) {        this(context, "image_manager_disk_cache", diskCacheSize);    }    public InternalCacheDiskCacheFactory(final Context context, final String diskCacheName, long diskCacheSize) {        super(new CacheDirectoryGetter() {            public File getCacheDirectory() {                File cacheDirectory = context.getCacheDir();                if (cacheDirectory == null) {                    return null;                } else {                    return diskCacheName != null ? new File(cacheDirectory, diskCacheName) : cacheDirectory;                }            }        }, diskCacheSize);    }}复制代码

InternalCacheDiskCacheFactory可以看出图片缓存的文件路径。

就是在context.getCacheDir()里面的image_manager_disk_cache文件夹。 我们可以发现,我这个设备里面已经缓存了一个文件。

如果说,你不想存在这个文件,你就自定义一个DiskLruCacheFactory

在前面我们会发现最后在build方法中创建的是

return DiskLruCacheWrapper.create(cacheDir, diskCacheSize);复制代码

一个DiskLruCacheWrapper对象,所以前面缓存的时候也是调用了DiskLruCacheWrapperput方法。

public void put(Key key, Writer writer) {        DiskLruCache diskCache = getDiskCache();        Value current = diskCache.get(safeKey);        if (current != null) {          return;        }    ...        DiskLruCache.Editor editor = diskCache.edit(safeKey);     ...          File file = editor.getFile(0);          if (writer.write(file)) {            editor.commit();          }       ...    } finally {      writeLocker.release(safeKey);    }  }...//writer.writepublic boolean write(@NonNull File file) {    return encoder.encode(data, file, options);  }复制代码

DiskLrcCache具体的逻辑这里先不介绍,只要知道了是通过key-value缓存到了本地。后面可以直接通过key获取到缓存的数据。

接下来,我们尝试下,重启App,让Glide从磁盘加载图片。前面的步骤肯定都是一样的。我们就直接从DecodeJobrun方法开始看。

public void run() {  ...      runWrapped();   ...  } private void runWrapped() {    switch (runReason) {      case INITIALIZE:        stage = getNextStage(Stage.INITIALIZE);        currentGenerator = getNextGenerator();        runGenerators();        break;     ...    }  }private void runGenerators() {    ...    while (!isCancelled && currentGenerator != null        && !(isStarted = currentGenerator.startNext())) {      stage = getNextStage(stage);      currentGenerator = getNextGenerator();      if (stage == Stage.SOURCE) {        reschedule();        return;      }    }    ...  }复制代码

之前是先在ResourceGeneratorDataCacheGenerator里面去找里面的loadData能不能处理这个请求。 在第一次加载网络图片的时候,前面2个都不能处理。但是经过了前面的磁盘缓存后。我们再进入DataCacheGenerator来看下里面的逻辑。

public boolean startNext() {    while (modelLoaders == null || !hasNextModelLoader()) {      sourceIdIndex++;      if (sourceIdIndex >= cacheKeys.size()) {        return false;      }      Key sourceId = cacheKeys.get(sourceIdIndex);      Key originalKey = new DataCacheKey(sourceId, helper.getSignature());      cacheFile = helper.getDiskCache().get(originalKey);      if (cacheFile != null) {        this.sourceKey = sourceId;        modelLoaders = helper.getModelLoaders(cacheFile);        modelLoaderIndex = 0;      }    }    loadData = null;    boolean started = false;    while (!started && hasNextModelLoader()) {      ModelLoader
modelLoader = modelLoaders.get(modelLoaderIndex++); loadData = modelLoader.buildLoadData(cacheFile, helper.getWidth(), helper.getHeight(), helper.getOptions()); if (loadData != null && helper.hasLoadPath(loadData.fetcher.getDataClass())) { started = true; loadData.fetcher.loadData(helper.getPriority(), this); } } return started; }复制代码

简单的来讲就是创建了一个DataCacheKey,然后helper.getDiskCache().get获取到缓存的数据。然后再交给对应的loadData去处理。

这里值得注意的是cacheKeysmodelLoaders

我们先来看cacheKeys,一步步往上找 DataCacheGenerator->cacheKeys=> DecodeHelper->cacheKeys 最终是这里创建的,看下代码

List
getCacheKeys() { ... List
> loadData = getLoadData(); for (int i = 0, size = loadData.size(); i < size; i++) { LoadData
data = loadData.get(i); if (!cacheKeys.contains(data.sourceKey)) { cacheKeys.add(data.sourceKey); } ... } } } return cacheKeys; }复制代码

cacheKeys保存的其实是LoadDatasourceKey. 这里看下List<LoadData<?>>是如何获取到的

List
> getLoadData() { ... List
> modelLoaders = glideContext.getRegistry().getModelLoaders(model); for (int i = 0, size = modelLoaders.size(); i < size; i++) { ModelLoader
modelLoader = modelLoaders.get(i); LoadData
current = modelLoader.buildLoadData(model, width, height, options); if (current != null) { loadData.add(current); } } } return loadData; }复制代码

LoadData其实ModelLoader. buildLoadData生成的,所以,我们就继续往下看ModelLoader是哪里来的。

List
> modelLoaders = glideContext.getRegistry().getModelLoaders(model);//Registry.javapublic
List
> getModelLoaders(@NonNull Model model) { List
> result = modelLoaderRegistry.getModelLoaders(model); ... return result; }//ModelLoadRegistry.javapublic
List
> getModelLoaders(@NonNull A model) { List
> modelLoaders = getModelLoadersForClass(getClass(model)); ... }//ModelLoadRegistry.javaprivate synchronized
List
> getModelLoadersForClass( @NonNull Class
modelClass) { List
> loaders = cache.get(modelClass); if (loaders == null) { loaders = Collections.unmodifiableList(multiModelLoaderFactory.build(modelClass)); cache.put(modelClass, loaders); } return loaders; }//MultiModelLoaderFactory.javasynchronized
List
> build(@NonNull Class
modelClass) { try { List
> loaders = new ArrayList<>(); for (Entry
entry : entries) { if (alreadyUsedEntries.contains(entry)) { continue; } if (entry.handles(modelClass)) { alreadyUsedEntries.add(entry); loaders.add(this.
build(entry)); alreadyUsedEntries.remove(entry); } } return loaders; } catch (Throwable t) { alreadyUsedEntries.clear(); throw t; } }复制代码

路径很深,大家可以直接看MultiModelLoaderFactory.build方法. 值得注意的是这里有个变量是entries,我们看看下是什么东西。

private final List
> entries = new ArrayList<>();复制代码

我们使用ctrl+鼠标左键找下引用的地方,看看哪里往里面添加东西了,添加了什么东西。

private 
void add( @NonNull Class
modelClass, @NonNull Class
dataClass, @NonNull ModelLoaderFactory
factory, boolean append) { Entry
entry = new Entry<>(modelClass, dataClass, factory); entries.add(append ? entries.size() : 0, entry); }复制代码

ctrl+鼠标左键,看哪里调用了add,一步步往上找。

//MultiModelLoaderFactory.javasynchronized 
void append( @NonNull Class
modelClass, @NonNull Class
dataClass, @NonNull ModelLoaderFactory
factory) { add(modelClass, dataClass, factory, /*append=*/ true); }//ModelLoaderRegistry.java public synchronized
void append( @NonNull Class
modelClass, @NonNull Class
dataClass, @NonNull ModelLoaderFactory
factory) { multiModelLoaderFactory.append(modelClass, dataClass, factory); cache.clear(); }//Registry.java public
Registry append( @NonNull Class
modelClass, @NonNull Class
dataClass, @NonNull ModelLoaderFactory
factory) { modelLoaderRegistry.append(modelClass, dataClass, factory); return this; }复制代码

最终找到,调用的地方是Glide的构造方法。

由于方法实在是太长了,这里就直接贴出图片。

我们再来看下方法,注意下参数名。

public 
Registry append(Class
modelClass, Class
dataClass, ModelLoaderFactory
factory) { modelLoaderRegistry.append(modelClass, dataClass, factory); return this; }复制代码

modelClass->传入的数据class,这里我们传入的是http字符串,也就是String.class

dataClass->处理后得到的数据class,前面文章介绍了第一次加载图片得到的其实是InputStream的子类

factory->创建处理传入数据是modelClass这种类型,得到数据是dataClass这种类型的处理器的工厂

我们继续回过头看下

synchronized 
List
> build(@NonNull Class
modelClass) { try { List
> loaders = new ArrayList<>(); for (Entry
entry : entries) { ... if (alreadyUsedEntries.contains(entry)) { continue; } if (entry.handles(modelClass)) { alreadyUsedEntries.add(entry); loaders.add(this.
build(entry)); alreadyUsedEntries.remove(entry); } } return loaders; } catch (Throwable t) { alreadyUsedEntries.clear(); throw t; } }复制代码

这里就看起来清晰多了,先调用entry.handles(modelClass)看看这个entry能不能处理modelClass这个类型的请求,如果可以就调用build方法构建一个ModelLoader

我们一步步来,先看handles方法

public boolean handles(@NonNull Class
modelClass) { return this.modelClass.isAssignableFrom(modelClass); }复制代码

非常简单,就看modelClass是不是之前注册的modelClass的子类。

那么继续看build方法。

private 
ModelLoader
build(@NonNull Entry
entry) { return (ModelLoader
) Preconditions.checkNotNull(entry.factory.build(this)); }复制代码

其实就是调用entry.factory.build(this)。前面已经介绍过了,factory其实就是前面注册的第三个参数。那么我们就可以看看之前,modelClass为String的对应的几个ModelLoaderFactory 根据前面的介绍是在Glide构造函数中添加的。我们就看下具体是哪几个。

.append(String.class, InputStream.class, new DataUrlLoader.StreamFactory
()).append(String.class, InputStream.class, new StringLoader.StreamFactory()).append(String.class, ParcelFileDescriptor.class, new StringLoader.FileDescriptorFactory()).append(String.class, AssetFileDescriptor.class, new StringLoader.AssetFileDescriptorFactory())复制代码

所以得到的loaders就是4个上面的factory调用build之后所创建的ModelLoader。这里就以StringLoader.StreamFactory.build方法为例子介绍下.

public ModelLoader
build( @NonNull MultiModelLoaderFactory multiFactory) { return new StringLoader<>(multiFactory.build(Uri.class, InputStream.class)); }复制代码

这里就有个比较巧妙的地方,继续调用了multiFactory也就是MultiModelLoaderFactorybuild方法,但是这次和前面不一样,这里传了2个参数,Uri.class对应modelClassInputStream对应dataClass,举一反三。前面已经介绍了传了一个参数,就是找到前面注册的modelClass为String.class的factory。这里也是一样,只不过这里要找到modelClass为Uri.classs,dataClass为InputStream的factory。然后使用factory构建出对应的ModelLoader

这里我们只需要知道,前面创建的StringLoader,内部有一个参数是uriLoader,而这个uriLoader就是处理modelClass为Uri,dataClass为InputStream的ModelLoader。那么我们可以在Glide的构造方法内找一下对应的factory。

.append(Uri.class, InputStream.class, new DataUrlLoader.StreamFactory
()).append(Uri.class, InputStream.class, new HttpUriLoader.Factory()).append(Uri.class, InputStream.class, new AssetUriLoader.StreamFactory(context.getAssets())).append(Uri.class, InputStream.class, new MediaStoreImageThumbLoader.Factory(context)).append(Uri.class, InputStream.class, new MediaStoreVideoThumbLoader.Factory(context)).append(Uri.class, InputStream.class, new UriLoader.StreamFactory(contentResolver)) ...复制代码

有点多,我就不一一写出了。

接下来就往前面看。

//ModelLoaderRegistry.javapublic  List
> getModelLoaders(@NonNull A model) { //这里获取到了4个 List
> modelLoaders = getModelLoadersForClass(getClass(model)); int size = modelLoaders.size(); boolean isEmpty = true; List
> filteredLoaders = Collections.emptyList(); for (int i = 0; i < size; i++) { ModelLoader
loader = modelLoaders.get(i); //这里过滤掉了一个 if (loader.handles(model)) { if (isEmpty) { filteredLoaders = new ArrayList<>(size - i); isEmpty = false; } filteredLoaders.add(loader); } } return filteredLoaders; }复制代码

大家可以注意下上面的注释,就是loader.handlers过滤了一个ModelLoader,这里就直接说了,过滤了DataUrlLoader

public boolean handles(@NonNull Model model) {    return model.toString().startsWith(DATA_SCHEME_IMAGE);  }复制代码

显然我们传入的字符串不是以这个为开头,所以为false。 所以最后传回去的是3个ModelLoader。继续往前看。

List
> getLoadData() { if (!isLoadDataSet) { isLoadDataSet = true; loadData.clear(); //这边就是前面得到的3个ModelLoader List
> modelLoaders = glideContext.getRegistry().getModelLoaders(model); for (int i = 0, size = modelLoaders.size(); i < size; i++) { ModelLoader
modelLoader = modelLoaders.get(i); LoadData
current = modelLoader.buildLoadData(model, width, height, options); if (current != null) { loadData.add(current); } } } return loadData; }复制代码

前面其实已经介绍过了,返回的其实是3个StringLoader,只不过是里面的uriLoader不太一样罢了。

public LoadData buildLoadData(@NonNull String model, int width, int height,      @NonNull Options options) {    Uri uri = parseUri(model);    if (uri == null || !uriLoader.handles(uri)) {      return null;    }    return uriLoader.buildLoadData(uri, width, height, options);  }...//MultiModelLoader.javapublic boolean handles(@NonNull Model model) {    for (ModelLoader
modelLoader : modelLoaders) { if (modelLoader.handles(model)) { return true; } } return false; }复制代码

uriLoader是一个MultiModelLoader,其实也是遍历一下,看看MultiModelLoader内部的ModelLoader能不能处理。

//HttpUriLoader

private static final Set
SCHEMES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("http", "https")));public boolean handles(@NonNull Uri model) { return SCHEMES.contains(model.getScheme()); }复制代码

找到HttpUriLoader能够处理。然后调用HttpUriLoader.buildLoaderData

public LoadData
buildLoadData(@NonNull Uri model, int width, int height, @NonNull Options options) { return urlLoader.buildLoadData(new GlideUrl(model.toString()), width, height, options); }复制代码

这里很奇怪,这里又来了一个urlLoader是什么东西。 创建HttpUriLoader的时候是根据一个factory创建的,

public static class Factory implements ModelLoaderFactory
{... public ModelLoader
build(MultiModelLoaderFactory multiFactory) { return new HttpUriLoader(multiFactory.build(GlideUrl.class, InputStream.class)); }... }复制代码

这段代码感觉又非常熟悉,跟前面很像,只不过这里的modelClassGlideUrl,dataClassInputStream,我们在Glide构造方法里面找一下

.append(GlideUrl.class, InputStream.class, new HttpGlideUrlLoader.Factory())复制代码

就这一个。 所以刚才urlLoader其实就是由HttpGlideUrlLoader.Factory()构建的HttpGlideUrlLoader。那么我们来看下HttpGlideUrlLoader.buildLoadData

public LoadData
buildLoadData(@NonNull GlideUrl model, int width, int height, @NonNull Options options) {GlideUrl url = model; ... return new LoadData<>(url, new HttpUrlFetcher(url, timeout)); }复制代码

HttpUrlFetcher其实就是真正发起http请求获取数据的fetcher。这里就不在深入了。

这里我们重新再看下这行代码

urlLoader.buildLoadData(new GlideUrl(model.toString()), width, height, options)复制代码

我们可以看下GlideUrl

public class GlideUrl implements Key复制代码

它实现了Key接口。所以前面获取DiskCacheKey传入的参数其实就是GlideUrl。那么我们就重新再回到最前面。DataCacheGenerator

public boolean startNext() {    while (modelLoaders == null || !hasNextModelLoader()) {     ...      Key sourceId = cacheKeys.get(sourceIdIndex);     ...      Key originalKey = new DataCacheKey(sourceId, helper.getSignature());      cacheFile = helper.getDiskCache().get(originalKey);      if (cacheFile != null) {        this.sourceKey = sourceId;        modelLoaders = helper.getModelLoaders(cacheFile);        modelLoaderIndex = 0;      }    }    loadData = null;    boolean started = false;    while (!started && hasNextModelLoader()) {      ModelLoader
modelLoader = modelLoaders.get(modelLoaderIndex++); loadData = modelLoader.buildLoadData(cacheFile, helper.getWidth(), helper.getHeight(), helper.getOptions()); if (loadData != null && helper.hasLoadPath(loadData.fetcher.getDataClass())) { started = true; loadData.fetcher.loadData(helper.getPriority(), this); } } return started; }复制代码

经过前面介绍如何获取到ModelLoader已经Key是什么之后,再来看下这段代码,就会发现有不太一样的认识。

cacheFile这里已经不为null。 然后继续往下,从helper.getModelLoaders(cacheFile),其实就是找到modelClassFilefactory

.append(File.class, ByteBuffer.class, new ByteBufferFileLoader.Factory()).append(File.class, InputStream.class, new FileLoader.StreamFactory()).append(File.class, File.class, new FileDecoder()).append(File.class, ParcelFileDescriptor.class, new FileLoader.FileDescriptorFactory()).append(File.class, File.class, UnitModelLoader.Factory.
getInstance())复制代码

然后反正就根据我刚才那样一步步往下走就好了,就会找到对应的ModelLoader然后生成对应的LoadData,这里就直接不再跟入了, 这里LoadData其实是ByteBufferFileLoader

private static final class ByteBufferFetcher implements DataFetcher
{... @Override public void loadData(@NonNull Priority priority, @NonNull DataCallback
callback) { ByteBuffer result; ... result = ByteBufferUtil.fromFile(file); ... callback.onDataReady(result); }复制代码

里面的fetcher就是ByteBufferFetcher,然后调用loadData方法读取到数据。

总结

这篇文章主要是对磁盘缓存数据还有获取数据的分析,以及ModelLoader的分析。后续还会继续深入分析Glide

转载地址:http://huill.baihongyu.com/

你可能感兴趣的文章
MongoDB又不加密,8.09亿条个人详细记录泄露
查看>>
《引领转型》访谈录
查看>>
用Git虚拟文件系统来解决大型存储问题
查看>>
一行代码迁移TensorFlow 1.x到TensorFlow 2.0
查看>>
明文存密码成惯例?Facebook 6 亿用户密码可被 2 万员工直接看
查看>>
我看到的前端
查看>>
火掌柜iOS端基于CocoaPods的组件二进制化实践
查看>>
强化学习遭遇瓶颈!分层RL将成为突破的希望
查看>>
华泰证券:如何自研高效可靠的交易系统通信框架?
查看>>
Grafana 6.0正式发布!新增查询工作流,全新独立Gauge面板
查看>>
精益企业中架构师的角色
查看>>
区块链技术精华:四十种智能合约支持平台(四)
查看>>
美团点评CTO罗道锋确认离职,新东家是快手?
查看>>
Kubernetes首爆严重安全漏洞,请升级你的Kubernetes
查看>>
Scrum丰田之道
查看>>
渔村小厂,如何成长为5G霸王
查看>>
GitHub推出更多课程
查看>>
InfoQ播客:Tal Weiss谈JVM的可观测性、插桩、以及字节码操作
查看>>
独家!支付宝小程序技术架构全解析
查看>>
1100名达摩院“扫地僧”加持,阿里云的下一个十年
查看>>