美文网首页
Glide的使用与解析

Glide的使用与解析

作者: Aisen | 来源:发表于2018-10-20 13:06 被阅读25次

    Glide的介绍与使用

    Glide是一个非常强大、优秀的图片加载框架,不但使用简单,而且加入了Activity和Fragment生命周期的管理。

    Glide支持拉取,解码和展示视频快照,图片和GIF动画。Glide的Api非常灵活,开发者甚至可以插入和替换成自己喜爱的任何网络栈。默认情况下,Glide使用的是一个定制化的基于HttpUrlConnection的栈,但同时也提供了与Google Volley和Square OkHttp快速集成的工具库。

    Glide的GitHub官网是https://github.com/bumptech/glide

    Glide的使用如下:

    1、在项目model的build.gradle(如app/build.gradle)文件当中添加依赖

    dependencies {
        implementation 'com.github.bumptech.glide:glide:3.7.0'
    }
    
    

    如果需要网络功能,那么在AndroidMainfest.xml中声明一下网络权限:

    <uses-permission android:name="android.permission.INTERNET" />
    
    

    2、直接调用接口

    String url = "http://cn.bing.com/az/hprichbg/rb/Dongdaemun_ZH-CN10736487148_1920x1080.jpg";
    ImageView mImageView = (ImageView) findViewById(R.id.image);
    
    Glide.with(this)
            .load(url)
            .into(mImageView);
    

    这是Glide最基本的使用方式:通过三步:with、load、into。

    其中with的参数可以是Activity、Fragment或者Context。如果传入是Activity或者Fragment实例,那么图片的加载可以跟着Activity或Fragment的生命周期执行,当这个Activity或Fragment销毁时,图片加载也会停止。如果传入ApplicationContext实例,那么只有当应用程序被杀掉的时候,图片加载才会停止。

    load的参数除了上述字符串url,还有其他图片资源的类型,如本地图片、应用资源、二进制流、Uri对象等。

    // 加载本地图片
    File file = new File(getExternalCacheDir() + "/image.jpg");
    Glide.with(this).load(file).into(imageView);
    
    // 加载应用资源
    int resource = R.drawable.image;
    Glide.with(this).load(resource).into(imageView);
    
    // 加载二进制流
    byte[] image = getImageBytes();
    Glide.with(this).load(image).into(imageView);
    
    // 加载Uri对象
    Uri imageUri = getImageUri();
    Glide.with(this).load(imageUri).into(imageView);
    

    into的参数是ImageView类型,即让图片显示在哪个ImageView控件上。当然,这个参数还支持Target类型,例子如下

    Glide.with(this)
            .load(url)
            .into(new SimpleTarget<GlideDrawable>() {
                @Override
                public void onResourceReady(GlideDrawable resource, GlideAnimation<? super GlideDrawable> glideAnimation) {
                    //resource即Glide加载出来的图片对象,可以进行处理成圆形、圆角等的drawable
                    mImageView.setImageDrawable(resource);
                }
            });
    

    3、其他API接口用法

    上述第2点是最基本用法,还可以使用Glide的扩展内容,一般就是基本用法中插入其他的API方法。例子如下:

    int resource = R.mipmap.ic_launcher;
    int resourceError = R.mipmap.ic_launcher_round;
    
    Glide.with(this)
            .load(url)
            .asBitmap()
            .placeholder(resource)
            .error(resourceError)
            .diskCacheStrategy(DiskCacheStrategy.NONE)
            .override(200, 200)
            .into(mImageView);
    
    

    asBitmap 指定图片格式为Bitmap,对应有GIF格式的设置asGif

    placeholder 占位图,指在图片的加载过程中先显示一张临时的图片,等图片加载出来了再替换成要加载的图片。

    error 异常占位图,当发生异常时使用的占位图,如url地址不存在,没有网络等异常。

    diskCacheStrategy 设置缓存功能。DiskCacheStrategy.NONE表示禁用Glide的缓存功能。

    override 指定图片大小,单位为px。

    以上是一些扩展的API方法。

    Glide的源码简析

    Glide的源代码比较复杂,详细的分析可以参考郭霖的专栏Glide最全解析。越复杂的代码,越要抓住主线去分析,可以先忽略细节的东西。Glide源码特别难懂的原因之一,是它用到的很多对象很早之前就初始化,在初始化的时候你可能完全就没有留意过它,因为一时半会根本就用不着,但是真正需要用到的时候你却找不到了。这时候可以先记录对象代表的什么,走完整个主线流程,具体细节,如初始化、特殊情况等,可以先忽略,后续有时间再回头分析。

    本节主要带着以下几点问题分析源码:

    1、Glide下载过程如何绑定Activity或者Fragment的生命周期?

    2、Glide的网络请求过程是什么?

    3、Glide获取的资源怎么显示出来?

    1、Glide下载过程如何绑定Activity或者Fragment的生命周期

    入口方法是with()

    //Glide.java
    public static RequestManager with(FragmentActivity activity) {
        RequestManagerRetriever retriever = RequestManagerRetriever.get();
        return retriever.get(activity);
    }
    
    //RequestManagerRetriever.java
    public RequestManager get(FragmentActivity activity) {
        if (Util.isOnBackgroundThread()) {
            return get(activity.getApplicationContext());
        } else {
            assertNotDestroyed(activity);
            FragmentManager fm = activity.getSupportFragmentManager();
            return supportFragmentGet(activity, fm);
        }
    }
    
    RequestManager supportFragmentGet(Context context, FragmentManager fm) {
        SupportRequestManagerFragment current = getSupportRequestManagerFragment(fm);
        RequestManager requestManager = current.getRequestManager();
        if (requestManager == null) {
            requestManager = new RequestManager(context, current.getLifecycle(), current.getRequestManagerTreeNode());
            current.setRequestManager(requestManager);
        }
        return requestManager;
    }
    
    //RequestManager.java
    RequestManager supportFragmentGet(Context context, FragmentManager fm) {
        SupportRequestManagerFragment current = getSupportRequestManagerFragment(fm);
        RequestManager requestManager = current.getRequestManager();
        if (requestManager == null) {
            requestManager = new RequestManager(context, current.getLifecycle(), current.getRequestManagerTreeNode());
            current.setRequestManager(requestManager);
        }
        return requestManager;
    }
    
    public class RequestManager implements LifecycleListener {
        //...
        RequestManager(Context context, final Lifecycle lifecycle, RequestManagerTreeNode treeNode,
                RequestTracker requestTracker, ConnectivityMonitorFactory factory) {
            //...
            if (Util.isOnBackgroundThread()) {
                new Handler(Looper.getMainLooper()).post(new Runnable() {
                    @Override
                    public void run() {
                        lifecycle.addListener(RequestManager.this);
                    }
                });
            } else {
                lifecycle.addListener(this);
            }
        }
        
       //...
    
        @Override
        public void onStart() {
            resumeRequests();
        }
    
        @Override
        public void onStop() {
            pauseRequests();
        }
    
        @Override
        public void onDestroy() {
            requestTracker.clearRequests();
        }
    
        //...
    }
    
    
    

    可以看出,无论with的参数是Activity还是Fragment,Glide都会调用supportFragmentGet。Glide并没有办法知道Activity的生命周期,于是就使用了添加隐藏Fragment的这种小技巧,因为Fragment的生命周期和Activity是同步的,如果Activity被销毁了,Fragment是可以监听到的,这样Glide就可以捕获这个事件并停止图片加载了。

    2、Glide的网络请求过程是什么?

    入口方法是.load(url).into(mImageView)

    (1)先看load方法:

    //RequestManager.java
    public DrawableTypeRequest<String> load(String string) {
        return (DrawableTypeRequest<String>) fromString().load(string);
    }
    
    public DrawableTypeRequest<String> fromString() {
        return loadGeneric(String.class);
    }
    
    //DrawableRequestBuilder.java (DrawableTypeRequest的父类)
    public DrawableRequestBuilder<ModelType> load(ModelType model) {
        super.load(model);
        return this;
    }
    

    load方法主要做一些初始化、封装API的工作。在DrawableRequestBuilder类中,除了load方法,还有placeholdererror等接口方法。

    在Glide类中,有以下代码

    register(String.class, ParcelFileDescriptor.class, new FileDescriptorStringLoader.Factory());
    register(String.class, InputStream.class, new StreamStringLoader.Factory());
    
    register(Uri.class, ParcelFileDescriptor.class, new FileDescriptorUriLoader.Factory());
    register(Uri.class, InputStream.class, new StreamUriLoader.Factory());
    
    register(GlideUrl.class, InputStream.class, new HttpUrlGlideUrlLoader.Factory());
    

    由此可知,fromString执行loadGeneric(String.class)后,从buildStreamModelLoader获取的数据流ModelLoader是StreamStringLoader。如果url字符串是带有http/https,那么StreamStringLoader调用getResourceFetcher()方法会得到一个HttpUrlFetcher对象(执行网络请求的对象)。

    (备注:StreamStringLoader > StreamUriLoader > HttpUrlGlideUrlLoader 一层一层传递,因此最后是调用HttpUrlGlideUrlLoader.getResourceFetcher,即返回HttpUrlFetcher)

    (2)接着看into方法

    //GenericRequestBuilder.java (上面DrawableRequestBuilder的父类)
    public Target<TranscodeType> into(ImageView view) {
        //...
        return into(glide.buildImageViewTarget(view, transcodeClass));
    }
    
    public <Y extends Target<TranscodeType>> Y into(Y target) {
        //Request是用来发出加载图片请求的,是Glide中非常关键的一个组件
        Request request = buildRequest(target);
        
        //...
        requestTracker.runRequest(request);
    
        return target;
    }
    

    上面是加载图片请求的入口,先初始化buildRequest,再执行runRequest,我们分开看这两步源代码,即(3)(4)。

    (3)GenericRequestBuilder的buildRequest

    private Request buildRequest(Target<TranscodeType> target) {
        if (priority == null) {
            priority = Priority.NORMAL;
        }
        return buildRequestRecursive(target, null);
    }
    
    private Request buildRequestRecursive(Target<TranscodeType> target, ThumbnailRequestCoordinator parentCoordinator) {
        if (thumbnailRequestBuilder != null) {
            //...
        } else if (thumbSizeMultiplier != null) {
           //...
        } else {
           // Base case: no thumbnail.
           return obtainRequest(target, sizeMultiplier, priority, parentCoordinator);
        }
    }
    
    private Request obtainRequest(Target<TranscodeType> target, float sizeMultiplier, Priority priority,
            RequestCoordinator requestCoordinator) {
        return GenericRequest.obtain(...);
    }
    

    buildRequest最后里调用了GenericRequest的obtain()方法,返回一个Request对象。注意这个obtain()方法需要传入非常多的参数,如placeholderId、errorPlaceholder等等。我们可以猜测,刚才load()方法所在类DrawableRequestBuilder中封装的API参数,如placeholdererror,其实都是在这里组装到Request对象当中的。

    (4)requestTracker.runRequest(request)

    //RequestTracker.java
    public void runRequest(Request request) {
        //...
        if (!isPaused) {
            request.begin();
        } else {
            //...
        }
    }
    
    //GenericRequest.java
    public void begin() {
        //...
        status = Status.WAITING_FOR_SIZE;
        if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
            onSizeReady(overrideWidth, overrideHeight);//执行onSizeReady方法
        } else {
            target.getSize(this);//最后也会回调onSizeReady方法
        }
    
        //...
    }
    
    @Override
    public void onSizeReady(int width, int height) {
        //...
        ModelLoader<A, T> modelLoader = loadProvider.getModelLoader();
        final DataFetcher<T> dataFetcher = modelLoader.getResourceFetcher(model, width, height);
    
        //...
        ResourceTranscoder<Z, R> transcoder = loadProvider.getTranscoder();
        
        loadStatus = engine.load(signature, width, height, dataFetcher, loadProvider, transformation, transcoder,
                priority, isMemoryCacheable, diskCacheStrategy, this);//最后UI的显示会回调this的onResourceReady接口方法
                
        //...
    }
    
    

    以上方法中大部分参数是第(1)步DrawableTypeRequest类的构造函数中传入的,例如:

    loadProvider 对应 DrawableTypeRequest构造函数中buildProvider返回的FixedLoadProvider。

    modelLoader 对应 ImageVideoModelLoader。

    dataFetcher 对应 new ImageVideoFetcher(streamFetcher, fileDescriptorFetcher),用于数据流的加载。

    transcoder 对应 GifBitmapWrapperDrawableTranscoder,用于数据的转码。

    接下来继续看engine.load方法

    (5)Engine的load方法

    public <T, Z, R> LoadStatus load(Key signature, int width, int height, DataFetcher<T> fetcher,
            DataLoadProvider<T, Z> loadProvider, Transformation<Z> transformation, ResourceTranscoder<Z, R> transcoder,
            Priority priority, boolean isMemoryCacheable, DiskCacheStrategy diskCacheStrategy, ResourceCallback cb) {
        //...
    
        EngineJob engineJob = engineJobFactory.build(key, isMemoryCacheable);
        DecodeJob<T, Z, R> decodeJob = new DecodeJob<T, Z, R>(key, width, height, fetcher, loadProvider, transformation,
                transcoder, diskCacheProvider, diskCacheStrategy, priority);
        EngineRunnable runnable = new EngineRunnable(engineJob, decodeJob, priority);
        
        engineJob.addCallback(cb);
        engineJob.start(runnable);
    
        return new LoadStatus(cb, engineJob);
    }
    
    class EngineRunnable implements Runnable, Prioritized {
        //...
        
        @Override
        public void run() {
            //...
            resource = decode();
    
            //...
            if (resource == null) {
                onLoadFailed(exception);
            } else {
                onLoadComplete(resource);
            }
        }   
        //...
    }
    

    load主要流程是engineJob.start(runnable),然后执行EngineRunnable里的run方法。

    在run方法中,先执行decode方法,返回结果是Resource<?>对象,然后通过onLoadComplete(resource)显示在ImageView上。

    因此,这里的关键代码是decode,下面继续分析。

    (6)EngineRunnable的decode方法

    //EngineRunnable.java
    private Resource<?> decode() throws Exception {
         //...
         return decodeFromSource();
    }
    
    private Resource<?> decodeFromSource() throws Exception {
        return decodeJob.decodeFromSource();
    }
    
    //DecodeJob.java
    public Resource<Z> decodeFromSource() throws Exception {
        Resource<T> decoded = decodeSource();
        return transformEncodeAndTranscode(decoded);
    }
    

    EngineRunnable.decode方法最终会调用DecodeJob.decodeSource方法获取Resource<T>对象,然后执行transformEncodeAndTranscode(decoded)进行编码转换,最后返回结果也是Resource<Z>对象,返回上一步(5)的onLoadComplete(resource)显示使用。

    因此,这里的关键代码是DecodeJob.decodeSource,下面继续分析。

    (7)DecodeJob的decodeSource方法

    //DecodeJob.java
    private Resource<T> decodeSource() throws Exception {
        Resource<T> decoded = null;
        try {
            //... 
            final A data = fetcher.loadData(priority);
            //...      
            decoded = decodeFromSourceData(data);
        } finally {
            fetcher.cleanup();
        }
        return decoded;
    }
    

    这里fetcher是第(4)步的ImageVideoFetcher,这里源码先执行fetcher.loadData加载数据,
    然后再执行decodeFromSourceData解析数据,这两步都很重要,下面(8)(9)分别分析。最后返回结果是Resource<T>对象,返回给上一步(6)的transformEncodeAndTranscode(decoded)编码转换使用。

    (8)ImageVideoFetcher的loadData方法 (网络请求方法)

    //ImageVideoFetcher.java
    public ImageVideoWrapper loadData(Priority priority) throws Exception {
        InputStream is = null;
        if (streamFetcher != null) {
           //...
            is = streamFetcher.loadData(priority);
        }
        //...
        return new ImageVideoWrapper(is, fileDescriptor);
    }
    

    这里的streamFetcher就是(1)中提到的HttpUrlFetcher,从这个类的实现可以看出,这个是网络请求的类,采用HttpURLConnection请求API。最后返回的是数据流InputStream,提供下面使用。

    (9)DecodeJob的decodeFromSourceData方法

    //DecodeJob.java
    private Resource<T> decodeFromSourceData(A data) throws IOException {
        final Resource<T> decoded;
        if (diskCacheStrategy.cacheSource()) {
           // ...
        } else {
             //getSourceDecoder对应的是GifBitmapWrapperResourceDecoder类
            decoded = loadProvider.getSourceDecoder().decode(data, width, height);
        }
        return decoded;
    }
    
    //GifBitmapWrapperResourceDecoder.java
    public Resource<GifBitmapWrapper> decode(ImageVideoWrapper source, int width, int height) throws IOException {
        //...
        GifBitmapWrapper wrapper = null;
        //...
        wrapper = decode(source, width, height, tempBytes);
    
        return wrapper != null ? new GifBitmapWrapperResource(wrapper) : null;
    }
    
    private GifBitmapWrapper decode(ImageVideoWrapper source, int width, int height, byte[] bytes) throws IOException {
        final GifBitmapWrapper result;
        if (source.getStream() != null) {
            result = decodeStream(source, width, height, bytes);
        } else {
            result = decodeBitmapWrapper(source, width, height);
        }
        return result;
    }
    
    private GifBitmapWrapper decodeStream(ImageVideoWrapper source, int width, int height, byte[] bytes)
            throws IOException {
        //获取第(8)步的InputStream
        InputStream bis = streamFactory.build(source.getStream(), bytes);
        bis.mark(MARK_LIMIT_BYTES);
        ImageHeaderParser.ImageType type = parser.parse(bis);
        bis.reset();
    
        GifBitmapWrapper result = null;
        //...
        if (result == null) {
            //...
            ImageVideoWrapper forBitmapDecoder = new ImageVideoWrapper(bis, source.getFileDescriptor());
            result = decodeBitmapWrapper(forBitmapDecoder, width, height);
        }
        return result;
    }
    
    private GifBitmapWrapper decodeBitmapWrapper(ImageVideoWrapper toDecode, int width, int height) throws IOException {
        GifBitmapWrapper result = null;
    
         // ImageVideoBitmapDecoder,开始解析InputStream数据
        Resource<Bitmap> bitmapResource = bitmapDecoder.decode(toDecode, width, height);
        if (bitmapResource != null) {
            result = new GifBitmapWrapper(bitmapResource, null);
        }
    
        return result;
    }
    
    //ImageVideoBitmapDecoder.java
    public Resource<Bitmap> decode(InputStream source, int width, int height) {
        Bitmap bitmap = downsampler.decode(source, bitmapPool, width, height, decodeFormat);
        return BitmapResource.obtain(bitmap, bitmapPool);
    }
    
    //Downsampler.java
    public Bitmap decode(InputStream is, BitmapPool pool, int outWidth, int outHeight, DecodeFormat decodeFormat) {
        //...
        final Bitmap downsampled =
                downsampleWithSize(invalidatingStream, bufferedStream, options, pool, inWidth, inHeight, sampleSize, decodeFormat);
    
        Bitmap rotated = null;
        if (downsampled != null) {
            rotated = TransformationUtils.rotateImageExif(downsampled, pool, orientation);
    
            if (!downsampled.equals(rotated) && !pool.put(downsampled)) {
                downsampled.recycle();
            }
        }
    
        return rotated;
    } 
    
    private Bitmap downsampleWithSize(MarkEnforcingInputStream is, RecyclableBufferedInputStream  bufferedStream,
            BitmapFactory.Options options, BitmapPool pool, int inWidth, int inHeight, int sampleSize,
            DecodeFormat decodeFormat) {
        //... 
        return decodeStream(is, bufferedStream, options);
    }
    
    private static Bitmap decodeStream(MarkEnforcingInputStream is, RecyclableBufferedInputStream bufferedStream,
            BitmapFactory.Options options) {
        //...
        final Bitmap result = BitmapFactory.decodeStream(is, null, options);
        return result;
    }
    
    

    从最后跟踪的代码可以看出,最后调用BitmapFactory.decodeStream解析上一步(8)的InputStream,返回Bitmap,再封装成Resource<Bitmap>、 GifBitmapWrapper、Resource<GifBitmapWrapper>,即第(7)步的返回结果。

    这就是Glide的网络请求流程。

    3、Glide获取的资源怎么显示出来?

    在上述第(5)步提到了显示UI的回调函数onLoadComplete(resource).

    class EngineRunnable implements Runnable, Prioritized {
        //...
        
        @Override
        public void run() {
            //...
            resource = decode();
    
            //...
            if (resource == null) {
                onLoadFailed(exception);
            } else {
                onLoadComplete(resource);
            }
        }   
        //...
        
        private void onLoadComplete(Resource resource) {
            //manager即EngineJob对象
           manager.onResourceReady(resource);
        }
    }
    
    //EngineJob.java
    public void onResourceReady(final Resource<?> resource) {
        this.resource = resource;
        MAIN_THREAD_HANDLER.obtainMessage(MSG_COMPLETE, this).sendToTarget();
    }
    
    private void handleResultOnMainThread() {
        //...
        for (ResourceCallback cb : cbs) {
            if (!isInIgnoredCallbacks(cb)) {
                engineResource.acquire();
                cb.onResourceReady(engineResource);
            }
        }
    }
    

    因此,最后显示UI会切换到主线程,然后通过回调函数显示,这里的ResourceCallback回调是在GenericRequest类中,执行engine.load(..., this)通过this传入的,源码如下。

    public void onSizeReady(int width, int height) {
        //...
        loadStatus = engine.load(signature, width, height, dataFetcher, loadProvider, transformation, transcoder,
                priority, isMemoryCacheable, diskCacheStrategy, this);
        //...
    }
    
    @Override
    public void onResourceReady(Resource<?> resource) {
        //...
        onResourceReady(resource, (R) received);
    }
    
    private void onResourceReady(Resource<?> resource, R result) {
        //...
        if (requestListener == null || !requestListener.onResourceReady(result, model, target, loadedFromMemoryCache,
                isFirstResource)) {
            GlideAnimation<R> animation = animationFactory.build(loadedFromMemoryCache, isFirstResource);
            target.onResourceReady(result, animation);
        }
        //...
    }
    

    这里的target就是into方法传入的参数,glide.buildImageViewTarget最后得到的是GlideDrawableImageViewTarget对象,源码如下。

    //GenericRequestBuilder.java
    public Target<TranscodeType> into(ImageView view) {
         //...      
        return into(glide.buildImageViewTarget(view, transcodeClass));
    }
    

    在GlideDrawableImageViewTarget类中,onResourceReady接口实现如下:

    public void onResourceReady(GlideDrawable resource, GlideAnimation<? super GlideDrawable> animation) {
        //...
        super.onResourceReady(resource, animation);
    }
    
    //父类
    @Override
    public void onResourceReady(Z resource, GlideAnimation<? super Z> glideAnimation) {
        if (glideAnimation == null || !glideAnimation.animate(resource, this)) {
            setResource(resource);
        }
    }
    
    @Override
    protected void setResource(GlideDrawable resource) {
        view.setImageDrawable(resource);
    }
    

    Glide获取的资源就这样显示ImageView控件上。

    扩展小点:

    1、 Glide缓存的实现方式是什么?

    详细的分析可以参考郭霖的Android图片加载框架最全解析(三),深入探究Glide的缓存机制,文章讲解原理后,介绍了一个Glide缓存的高级用法。

    Glide使用缓存的方式有两种:内存缓存和硬盘缓存。

    内存缓存的实现是常见的LruCache算法,加上弱引用的机制,共同完成的。具体是,正在使用中的图片使用弱引用来进行缓存,不再使用中的图片使用LruCache来进行缓存。

    LruCache算法(Least Recently Used),即近期最少使用算法。它的主要算法原理就是把最近使用的对象用强引用存储在LinkedHashMap中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。

    硬盘缓存的实现也是使用的LruCache算法,具体使用是Google的工具类DiskLruCache。

    2、与Picasso的对比(参考这篇文章)

    Glide和Picasso有90%的相似度,准确的说,就是Picasso的克隆版本。但是在细节上还是有不少区别的。

    (1)Pisson默认加载Bitmap格式是RGB_8888,而Glide是RGB_565。

    结果是Glide的内存开销比Picasso小。

    原因在于Picasso是加载了全尺寸的图片到内存,然后让GPU来实时重绘大小。而Glide加载的大小和ImageView的大小是一致的,因此更小。

    (2)磁盘缓存策略不同。Picasso缓存的是全尺寸的,而Glide缓存的是跟ImageView尺寸相同的。

    结果是Glide加载显示比Picasso快。

    (3)Glide可以加载GIF动态图,而Picasso不能。

    参考

    Glide最全解析

    Google推荐的图片加载库Glide介绍

    Android Glide源码解析

    相关文章

      网友评论

          本文标题:Glide的使用与解析

          本文链接:https://www.haomeiwen.com/subject/ttxmzftx.html