精华内容
下载资源
问答
  • 不一样视角的Glide剖析(一) Glide是一个快速高效的Android图片加载库,注重于平滑的滚动。Glide提供了易用的API,高性能、可扩展的图片解码管道,以及自动的资源池技术。为了让用户拥有良好的App使用体验,图片...

     

     

    推荐阅读:

     

    滴滴Booster移动App质量优化框架-学习之旅 一

     

    Android 模块Api化演练

     

    不一样视角的Glide剖析(一)

     

     

    Glide是一个快速高效的Android图片加载库,注重于平滑的滚动。Glide提供了易用的API,高性能、可扩展的图片解码管道,以及自动的资源池技术。为了让用户拥有良好的App使用体验,图片不仅要快速加载,而且还不能因为过多的主线程I/O或频繁的垃圾回收导致页面的闪烁和抖动现象。

    Glide使用了多个步骤来确保在Android上加载图片尽可能的快速和平滑:

    1.自动、智能地下采样(downsampling)和缓存(caching),以最小化存储开销和解码次数;

    2.积极的资源重用,例如字节数组和Bitmap,以最小化昂贵的垃圾回收和堆碎片影响;

    3.深度的生命周期集成,以确保仅优先处理活跃的Fragment和Activity的请求,并有利于应用在必要时释放资源以避免在后台时被杀掉。

     

    本文将依次分析Glide如下问题:

    1.Glide图片加载的大致加载流程

    2.Glide图片加载的生命周期的集成

    3.Glide的图片缓存机制

    4.对象池优化,减少内存抖动

    5.Bitmap的解码

    6.网络栈的切换

     

    1.Glide图片加载的大致加载流程

    Glide使用简明的流式语法Api,大部分情况下一行代码搞定图片显示,比如:

    Glide.with(activity).load(url).into(imageView)

     

    就以上述调用简易分析图片加载流程,如下图:

                                                                                                Glide图片加载流程图

     

    ResourceDiskCache包含了降低采样、转换的图片资源,DataDiskCache为原始图片资源。RemoteSurce即从远端服务器拉取资源。从ResourceCache、RemoteSource 加载图片都涉及到ModelLoader、 解码与转码,以及多层回调才到显示图片,流程比较复杂,这里就不详述了。

     

    2.Glide图片加载的生命周期的集成

    从Glide.with(host)调用出发,跟踪创建RequestManager的过程,可以推断了解到RequestManager的实例化,最终由RequestManagerRetriever一系列重载函数get()完成,最终根据host不同类型(Application和非ui线程除外),由supportFragmentGet或fragmentGet方法构建,其实现如下:

    @NonNull
    private RequestManager supportFragmentGet(@NonNull Context context, @NonNull androidx.fragment.app.FragmentManager fm, @Nullable Fragment parentHint, boolean isParentVisible) {
         SupportRequestManagerFragment current = this.getSupportRequestManagerFragment(fm, parentHint, isParentVisible);
        RequestManager requestManager = current.getRequestManager();
         if (requestManager == null) {
              Glide glide = Glide.get(context);
              requestManager = this.factory.build(glide, current.getGlideLifecycle(), current.getRequestManagerTreeNode(), context);
              current.setRequestManager(requestManager);
         }
    
        return requestManager;
    }
    
    private RequestManager fragmentGet(@NonNull Context context, @NonNull FragmentManager fm, @Nullable android.app.Fragment parentHint, boolean isParentVisible) {
        RequestManagerFragment current = this.getRequestManagerFragment(fm, parentHint, isParentVisible);
        RequestManager requestManager = current.getRequestManager();
        if (requestManager == null) {
                Glide glide = Glide.get(context);
            requestManager = this.factory.build(glide, current.getGlideLifecycle(), current.getRequestManagerTreeNode(), context);
            current.setRequestManager(requestManager);
        }
    
        return requestManager;
    }
    
    

    都会创建并添加一个可不见的SupportRequestManagerFragment或者RequestManagerFragment,而这两个Fragment都有添加LifecycleListener的功能,在其生命周期函数中都会调用listener对应的生命周期函数,代码如下:

    SupportRequestManagerFragment\RequestManagerFragment{
      //对LifecycleListener 进行了封装
      private final ActivityFragmentLifecycle lifecycle;
      @Override
      public void onStart() {
        super.onStart();
        lifecycle.onStart();
      }
    
      @Override
      public void onStop() {
        super.onStop();
        lifecycle.onStop();
      }
    
      @Override
      public void onDestroy() {
        super.onDestroy();
        lifecycle.onDestroy();
        unregisterFragmentWithRoot();
      }
    }
    
    class ActivityFragmentLifecycle implements Lifecycle {
       void onStart() {
        isStarted = true;
        for (LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) {
          lifecycleListener.onStart();
        }
      }
    
      void onStop() {
        isStarted = false;
        for (LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) {
          lifecycleListener.onStop();
        }
      }
    
      void onDestroy() {
        isDestroyed = true;
        for (LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) {
          lifecycleListener.onDestroy();
        }
      }
    }

     

    再查看RequestManager的相关代码实现,如下:

    public class RequestManager implements LifecycleListener, ...{
      RequestManager(...,Lifecycle lifecycle,...) {
        ...
        this.lifecycle = lifecycle;
        ...
        // If we're the application level request manager, we may be created on a background thread.
        // In that case we cannot risk synchronously pausing or resuming requests, so we hack around the
        // issue by delaying adding ourselves as a lifecycle listener by posting to the main thread.
        // This should be entirely safe.
        if (Util.isOnBackgroundThread()) {
          mainHandler.post(addSelfToLifecycle);
        } else {
          lifecycle.addListener(this);
        }
        lifecycle.addListener(connectivityMonitor);
    
    
        ...
      }
    
    @Override
      public synchronized void onStart() {
        resumeRequests();
        targetTracker.onStart();
      }
    
      /**
       * Lifecycle callback that unregisters for connectivity events (if the
       * android.permission.ACCESS_NETWORK_STATE permission is present) and pauses in progress loads.
       */
      @Override
      public synchronized void onStop() {
        pauseRequests();
        targetTracker.onStop();
      }
    
      /**
       * Lifecycle callback that cancels all in progress requests and clears and recycles resources for
       * all completed requests.
       */
      @Override
      public synchronized void onDestroy() {
        ...
        requestTracker.clearRequests();
        lifecycle.removeListener(this);
        lifecycle.removeListener(connectivityMonitor);
        ..
      }
    }

     

    RequestManager实现了LifecycleListener接口,并在构造器中给lifecycle添加listener,而这里lifecycle正好对应了RequestManagerFragment中的lifecycle,就这样RequestManager可以只能感知RequestManagerFragment的生命周期,也就感知其中host Activity或者Fragment的生命周期。

    RequestManager管理所有的其对应host中所有的请求,requestTracker对跟踪Request的封装,具有暂停、重启、清空请求的功能。

    至此就可以知道Glide图片加载可以智能感知Activity、Fragment的生命周期函数进行重启,暂停,清除。

     

    3.Glide的图片缓存机制

    在Glide图片加载流程图中,可以知道真正开始加载图片的地方从Engine.load(),大致代码如下:

     

    public synchronized <R> LoadStatus load(...) {
    
        EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,
            resourceClass, transcodeClass, options);
        //活动资源
        EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
        if (active != null) {
          cb.onResourceReady(active, DataSource.MEMORY_CACHE);
          }
          return null;
        }
    
        //内存缓存
        EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
        if (cached != null) {
          cb.onResourceReady(cached, DataSource.MEMORY_CACHE);
          return null;
        }
    
        EngineJob<?> current = jobs.get(key, onlyRetrieveFromCache);
        if (current != null) {
          current.addCallback(cb, callbackExecutor);
          return new LoadStatus(cb, current);
        }
    
        EngineJob<R> engineJob = engineJobFactory.build(...);
        //解码工作任务 由线程池调度启动,ResourceDiskcache DataDiskCached都这里加载编码
        DecodeJob<R> decodeJob = decodeJobFactory.build(...);
    
        jobs.put(key, engineJob);
    
        engineJob.addCallback(cb, callbackExecutor);
        //开始DecodeJob,由线程池调度启动
        engineJob.start(decodeJob);
    
        return new LoadStatus(cb, engineJob);
      }

     

    DecodeJob decode state有如下几种:

    /**
       * Where we're trying to decode data from.
       */
    //DecodeJob内部枚举类
    private enum Stage {
        /** The initial stage. */
        INITIALIZE,
        /** Decode from a cached resource. */
        RESOURCE_CACHE,
        /** Decode from cached source data. */
        DATA_CACHE,
        /** Decode from retrieved source. */
        SOURCE,
        /** Encoding transformed resources after a successful load. */
        ENCODE,
        /** No more viable stages. */
        FINISHED,
      }

    State.ENCODE代表成功从Source中加载数据后,把transformed的资源保存到DiskCache。

    由此可以看出Glide分为四级缓存:

    1. 活动资源 (ActiveResources)

    2. 内存缓存 (MemoryCache)

    3. 资源类型(Resource DiskCache)

    4. 原始数据 (Data DiskCache)

    活动资源:如果当前对应的图片资源正在使用,则这个图片会被Glide放入活动缓存。 

    内存缓存:如果图片最近被加载过,并且当前没有使用这个图片,则会被放入内存中 。

    资源类型: 被解码后的图片写入磁盘文件中,解码的过程可能修改了图片的参数(如: inSampleSize、inPreferredConfig)。

    原始数据: 图片原始数据在磁盘中的缓存(从网络、文件中直接获得的原始数据)。

    Glide加载图片依次从四级缓存中获取图片资源的时序图如下:

     

     

    活动资源ActiveResources

    ActiveResources维护着弱引用EngineResource map集合,当有垃圾回收时,弱引用关联的EngineResource 会被存放到ReferenceQueue中,ActiveResources在实例化时开启线程监控清理被回收的EngineResource 该EngineResource 又会转移到MemoryCache中去,具体代码如下:

    final class ActiveResources {
     ... 
    
      final Map<Key, ResourceWeakReference> activeEngineResources = new HashMap<>();
      private final ReferenceQueue<EngineResource<?>> resourceReferenceQueue = new ReferenceQueue<>();
    
      private ResourceListener listener;
    
      ...
    
      ActiveResources(
          boolean isActiveResourceRetentionAllowed, Executor monitorClearedResourcesExecutor) {
        this.isActiveResourceRetentionAllowed = isActiveResourceRetentionAllowed;
        this.monitorClearedResourcesExecutor = monitorClearedResourcesExecutor;
    
        monitorClearedResourcesExecutor.execute(
            new Runnable() {
              @Override
              public void run() {
                cleanReferenceQueue();
              }
        });
      }
    } 
    
    void cleanupActiveReference(@NonNull ResourceWeakReference ref) {
        // Fixes a deadlock where we normally acquire the Engine lock and then the ActiveResources lock
        // but reverse that order in this one particular test. This is definitely a bit of a hack...
        synchronized (listener) {
          synchronized (this) {
            activeEngineResources.remove(ref.key);
    
            if (!ref.isCacheable || ref.resource == null) {
              return;
            }
            EngineResource<?> newResource =
                new EngineResource<>(ref.resource, /*isCacheable=*/ true, /*isRecyclable=*/ false);
            newResource.setResourceListener(ref.key, listener);
            //Engine实现了ResourceListener接口,最终会调用Resource.recycle()方法
            listener.onResourceReleased(ref.key, newResource);
          }
        }
     }
    
    //Engine类
    public synchronized void onResourceReleased(Key cacheKey, EngineResource<?> resource) {
        activeResources.deactivate(cacheKey);
        if (resource.isCacheable()) {
          cache.put(cacheKey, resource);
        } else {
          resourceRecycler.recycle(resource);
        }
     }

    EngineResource是对Resource一种包装,新增了引用计数功能,每当一个地方获取该资源时,引用计数acquired就会加1,当EngineResource被release时引用计数acquired减1,当acquired==0也会回调EngineResource从ActiveResources回收到MemeryCache中去。

     

    那引用计数在哪些情况下加1

    情况一、 资源在ActiveResources中命中,acquired++,代码如下:

    private EngineResource<?> loadFromActiveResources(Key key, boolean isMemoryCacheable) {
        if (!isMemoryCacheable) {
          return null;
        }
        EngineResource<?> active = activeResources.get(key);
        if (active != null) {
          active.acquire();//acquire++
        }
    
        return active;
      }

     

    情况二、资源在MemoryCache中命中,资源从MemoryCach转移到ActiveResources,acquired++,代码如下:

    private EngineResource<?> loadFromCache(Key key, boolean isMemoryCacheable) {
        if (!isMemoryCacheable) {
          return null;
        }
         EngineResource<?> cached = getEngineResourceFromCache(key);
        if (cached != null) {
          cached.acquire();
           //cached 转移到activeResources
          activeResources.activate(key, cached);
        }
        return cached;
      }

     

    情况三、 资源从DiskCache、RemoteSource加载也会acquired++,拉取的资源也会加入到ActiveResources。通过DecodeJob加载的资源,最终都会回调DecodeJob的decodeFromRetrievedData()方法,最终辗转到EngineJob的notifyCallbacksOfResult()方法,其代码如下:

    void notifyCallbacksOfResult() {
     
         ...
         //listener 为Engine,
        //EngineonEngineJobComplete方法中调用了activeResources.activate()
        listener.onEngineJobComplete(this, localKey, localResource);
    
        //CallResourceReady.run方法调用
        for (final ResourceCallbackAndExecutor entry : copy) {
          entry.executor.execute(new CallResourceReady(entry.cb));
        }
        decrementPendingCallbacks();
    }
    
    //Engine
    public synchronized void onEngineJobComplete(
          EngineJob<?> engineJob, Key key, EngineResource<?> resource) {
    
        if (resource != null) {
          resource.setResourceListener(key, this);
    
          if (resource.isCacheable()) {
            //把加载的资源加入到activeResources中去
            activeResources.activate(key, resource);
          }
        }
    
        jobs.removeIfCurrent(key, engineJob);
      }
    
    
    private class CallResourceReady implements Runnable {
        ... 
    
        @Override
        public void run() {
          synchronized (EngineJob.this) {
            if (cbs.contains(cb)) {
              // Acquire for this particular callback.
              engineResource.acquire(); //acquire++
              callCallbackOnResourceReady(cb);
              removeCallback(cb);
            }
            decrementPendingCallbacks();
          }
        }
      }

     

    引用计数减一情况

     

    在Glide图片加载的生命周期的集成部分,已分析RequestManeger能感知Activity,Fragment生命周期函数,由RequetTracker跟踪Request,具有暂停、重启,清除Request的功能。

    RequestManeger生命回调函数onStop、onDestory代码如下:

    public synchronized void onStop() {
        pauseRequests();//暂停所有请求
        targetTracker.onStop();
      }
    
      /**
       * Lifecycle callback that cancels all in progress requests and clears and recycles resources for
       * all completed requests.
       */
      @Override
      public synchronized void onDestroy() {
        targetTracker.onDestroy();
        for (Target<?> target : targetTracker.getAll()) {
          clear(target);
        }
        targetTracker.clear();
        requestTracker.clearRequests();//清除所有请求
        lifecycle.removeListener(this);
        lifecycle.removeListener(connectivityMonitor);
        mainHandler.removeCallbacks(addSelfToLifecycle);
        glide.unregisterRequestManager(this);
      }

     

    requestTracker的clearRequests()和pauseRequests()方法都调用了request.clear()方法,而真正的请求实例为SingleRequest,其clear方法代码如下:

    public synchronized void clear() {
        ...
        cancel();
        // Resource must be released before canNotifyStatusChanged is called.
        if (resource != null) {
          releaseResource(resource);
        }
        ...
    }
    
    private void releaseResource(Resource<?> resource) {
        engine.release(resource);//EngineResoure.release
        this.resource = null;
    }

     

    调用了EngineResoure.release()方法,代码如下:

    void release() {
        // To avoid deadlock, always acquire the listener lock before our lock so that the locking
        // scheme is consistent (Engine -> EngineResource). Violating this order leads to deadlock
        // (b/123646037).
        synchronized (listener) {
          synchronized (this) {
            if (acquired <= 0) {
              throw new IllegalStateException("Cannot release a recycled or not yet acquired resource");
            }
            if (--acquired == 0) {//减一操作
              //Engine.onResourceReleased
              listener.onResourceReleased(key, this);
            }
          }
        }
      }

     

    当acquired == 0时会回调Engine. onResourceReleased方法,把资源从activeResources中移除,加入带MemoryCache中去,其代码如下:

    public synchronized void onResourceReleased(Key cacheKey, EngineResource<?> resource) {
        //从activeResources 移除该资源
        activeResources.deactivate(cacheKey);
        if (resource.isCacheable()) {
          //放入MemoryCache中
          cache.put(cacheKey, resource);
        } else {
          resourceRecycler.recycle(resource);
        }
      }

     

    内存缓存MemoryCache

     

    Glide中MemoryCache默认情况下,为LruResourceCache,继承了LruCache,使用了最近最少算法管理内存资源,同时对外提供了trimMemory ,clearMemory接口,代码如下:

    /**
     * An LRU in memory cache for {@link com.bumptech.glide.load.engine.Resource}s.
     */
    public class LruResourceCache extends LruCache<Key, Resource<?>> implements MemoryCache {
      private ResourceRemovedListener listener;
    
      ...
    
      @Override
      public void setResourceRemovedListener(@NonNull ResourceRemovedListener listener) {
        this.listener = listener;
      }
    
      @Override
      protected void onItemEvicted(@NonNull Key key, @Nullable Resource<?> item) {
        if (listener != null && item != null) {
          listener.onResourceRemoved(item);
        }
      }
    
      @Override
      protected int getSize(@Nullable Resource<?> item) {
        if (item == null) {
          return super.getSize(null);
        } else {
          return item.getSize();
        }
      }
    
      @SuppressLint("InlinedApi")
      @Override
      public void trimMemory(int level) {
        if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
          // Entering list of cached background apps
          // Evict our entire bitmap cache
          clearMemory();
        } else if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN
            || level == android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) {
          // The app's UI is no longer visible, or app is in the foreground but system is running
          // critically low on memory
          // Evict oldest half of our bitmap cache
          trimToSize(getMaxSize() / 2);
        }
      }
    }

     

    MemoryCache对外提供了资源删除监听接口,通过搜索可以知道Engine实现了ResourceRemovedListener接口,并设置给MemoryCache,Engine资源删除回调函数Engine.onResourceRemoved相关代码如下:

    @Override
    public void onResourceRemoved(@NonNull final Resource<?> resource) {
        resourceRecycler.recycle(resource);
    }
    
    //ResourceRecycler
    synchronized void recycle(Resource<?> resource) {
        if (isRecycling) {
          // If a resource has sub-resources, releasing a sub resource can cause it's parent to be
          // synchronously evicted which leads to a recycle loop when the parent releases it's children.
          // Posting breaks this loop.
          handler.obtainMessage(ResourceRecyclerCallback.RECYCLE_RESOURCE, resource).sendToTarget();
        } else {
          isRecycling = true;
          resource.recycle();
          isRecycling = false;
        }
      }

     

     

    onResourceRemoved回调函数对资源进行Recycle,MemoryCache的Resource实际上为EngineResource,最终对被包裹的资源进行Recycle,而Resource的实现类有如下图这些:

     

     

    对跟Bitmap有关的BitmapResource,BitmapDrawableResource进行分析,其recycle方法实现如下:

    //BitmapResource
    public void recycle() {
        bitmapPool.put(bitmap);
    }
    //BitmapDrawableResource
    public void recycle() {
        bitmapPool.put(drawable.getBitmap());
    }

     

    把从MemoryCache删除资源关联的bitmap回收到BitmapPool中,注意这里删除是指资源被MemoryCache被逐出,触发onItemEvicted回调了。

     

    那么有些地方会触发了onItemEvicted动作了?

    情况一、MemoryCache进行put操作时,old的资源被新的资源覆盖时,oldResource被逐出,和size超过了maxSize,会逐出最近最少使用的资源,都会触发onItemEvicted,最终资源关联的Bitmap回收到BitmapPool中。

    情况二、Glide对面提供了trimMemory,clearMemory接口(通常会在Activity.trimMemory方法中调用),对最终MemoryCache资源进行清理,触发onItemEvicted回调,资源关联的Bitmap回收到BitmapPool中。

     

    这里讲到MemoryCache资源被Evicted的情况,其他情况还没讲到,其实前文已经提到了,资源在MemoryCache中命中了,被remove,且转移到ActiveResources中;在资源请求被暂停、取消、删除以及ActiveResources自身资源清除监控线程进行清除时,也会是相关的资源从ActiveResources转移到MemoryCache。

     

    4.对象池优化,减少内存抖动

     

    Glide大量使用对象池Pools来对频繁需要创建和销毁的代码进行优化。

     

    以下就是使用对象池的情况:

     

    1.每次图片加载都会涉及到Request对象,可能涉及到EncodeJob,DecodeJob对象,在加载大量图片的加载情况下,这会频繁创建和销毁对象,造成内存抖动,至此使用FactoryPools(android support包 Pool)。

     

    2.对MemoryCache的资源回收,使用BitmapPool池对资源关联的Bitmap回收;解码图片生成Bitmap(大对象)时,复用了BitmapPool池中的Bitmap。

     

    3.解码图片生成Bitmap时,配置了inTempStorage,使用了ArrayPool技术复用了byte[](64kb)。

     

    FactoryPools

     

    FactoryPools是基于android support包中的对象池存取的辅助类Pools,先看Pools源码:

    public final class Pools {
    
        /**
         * Interface for managing a pool of objects.
         *
         * @param <T> The pooled type.
         */
        public static interface Pool<T> {
    
            /**
             * @return An instance from the pool if such, null otherwise.
             */
            public T acquire();
    
            /**
             * Release an instance to the pool.
             *
             * @param instance The instance to release.
             * @return Whether the instance was put in the pool.
             *
             * @throws IllegalStateException If the instance is already in the pool.
             */
            public boolean release(T instance);
        }
    
        private Pools() {
            /* do nothing - hiding constructor */
        }
    
        /**
         * Simple (non-synchronized) pool of objects.
         *
         * @param <T> The pooled type.
         */
        public static class SimplePool<T> implements Pool<T> {
            private final Object[] mPool;
    
            private int mPoolSize;
    
            /**
             * Creates a new instance.
             *
             * @param maxPoolSize The max pool size.
             *
             * @throws IllegalArgumentException If the max pool size is less than zero.
             */
            public SimplePool(int maxPoolSize) {
                if (maxPoolSize <= 0) {
                    throw new IllegalArgumentException("The max pool size must be > 0");
                }
                mPool = new Object[maxPoolSize];
            }
    
            @Override
            @SuppressWarnings("unchecked")
            public T acquire() {
                if (mPoolSize > 0) {
                    final int lastPooledIndex = mPoolSize - 1;
                    T instance = (T) mPool[lastPooledIndex];
                    mPool[lastPooledIndex] = null;
                    mPoolSize--;
                    return instance;
                }
                return null;
            }
    
            @Override
            public boolean release(T instance) {
                if (isInPool(instance)) {
                    throw new IllegalStateException("Already in the pool!");
                }
                if (mPoolSize < mPool.length) {
                    mPool[mPoolSize] = instance;
                    mPoolSize++;
                    return true;
                }
                return false;
            }
            private boolean isInPool(T instance) {
                for (int i = 0; i < mPoolSize; i++) {
                    if (mPool[i] == instance) {
                        return true;
                    }
                }
                return false;
            }
        }
        /**
         * Synchronized) pool of objects.
         *
         * @param <T> The pooled type.
         */
        public static class SynchronizedPool<T> extends SimplePool<T> {
            private final Object mLock = new Object();
    
            /**
             * Creates a new instance.
             *
             * @param maxPoolSize The max pool size.
             *
             * @throws IllegalArgumentException If the max pool size is less than zero.
             */
            public SynchronizedPool(int maxPoolSize) {
                super(maxPoolSize);
            }
    
            @Override
            public T acquire() {
                synchronized (mLock) {
                    return super.acquire();
                }
            }
    
            @Override
            public boolean release(T element) {
                synchronized (mLock) {
                    return super.release(element);
                }
            }
        }
    }

    定义了Pool池接口类,包含两个方法acquire(从池中取出对象),release(回收对象,存入对象池),提供了两个实现简单实现类SimplePool,SynchronizedPool,SimplePool用数组维护这个对象池,有个缺点就是不能动态扩容,SynchronizedPool对SimplePool进行了同步。

    FactoryPools类中定义了FactoryPool类,FactoryPool使用装饰者模式,对Pool扩展了对象工厂创建、回收对象重置功能;提供了一些创建Pool的静态方法。源码搜索FactoryPools,就看到哪些地方用FactoryPools了,如图:

     

    这里就不详细陈述EncodeJob、DecodeJob、SingRequest,是在什么时机,被回收到FactoryPool中了,基本上都在其release方法中进行回收操作。

     

    BitmapPool

     

     

    Bitmap的创建是申请内存昂贵的,大则占用十几M,使用进行BitmapPool回收复用,可以显著减少内存消耗和抖动。BitmapPool定义了如下接口:

    public interface BitmapPool {
    
      long getMaxSize();
    
      void setSizeMultiplier(float sizeMultiplier);
    
      void put(Bitmap bitmap);
    
      Bitmap get(int width, int height, Bitmap.Config config);
    
      Bitmap getDirty(int width, int height, Bitmap.Config config);
    
      void clearMemory();
    
      void trimMemory(int level);
    }

     

    Glide的默认BitmapPool实现为LruBitmapPool,从GlideBuild.build方法可以看出,LruBitmapPool把Bitmap缓存的维护委托给LruPoolStrategy,大致代码如下:

    public class LruBitmapPool implements BitmapPool {
      ...
      @Override
      public synchronized void put(Bitmap bitmap) {
    
        ... 
    
        final int size = strategy.getSize(bitmap);
        strategy.put(bitmap);
        tracker.add(bitmap);
    
        puts++;
        currentSize += size;
        ...
        evict();
      }
    
      private void evict() {
        trimToSize(maxSize);
      }
    
    
      public Bitmap get(int width, int height, Bitmap.Config config) {
        Bitmap result = getDirtyOrNull(width, height, config);
        if (result != null) {
          result.eraseColor(Color.TRANSPARENT);
        } else {
          result = createBitmap(width, height, config);
        }
    
        return result;
      }
    
      public Bitmap getDirty(int width, int height, Bitmap.Config config) {
        Bitmap result = getDirtyOrNull(width, height, config);
        if (result == null) {
          result = createBitmap(width, height, config);
        }
        return result;
      }
    
      private synchronized Bitmap getDirtyOrNull(
          int width, int height, @Nullable Bitmap.Config config) {
        assertNotHardwareConfig(config);
    
        final Bitmap result = strategy.get(width, height, config != null  config : DEFAULT_CONFIG);
        if (result == null) {
          misses++;
        } else {
          hits++;
          currentSize -= strategy.getSize(result);
          tracker.remove(result);
          normalize(result);
        }
    
        dump();
    
        return result;
     }
    
      public void clearMemory() {
        ...
        trimToSize(0);
      }
    
    
      public void trimMemory(int level) {
    
        if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
          clearMemory();
        } else if (level >= android.content.ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN
            || level == android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL) {
          trimToSize(getMaxSize() / 2);
        }
      }
    
      private synchronized void trimToSize(long size) {
        while (currentSize > size) {
          final Bitmap removed = strategy.removeLast();
          ...
          currentSize -= strategy.getSize(removed);
          evictions++;
          ...     
          removed.recycle();
        }
      }

     

    Bitmap缓存策略接口默认实现有AttributeStrategy和SizeConfigStrategy,前者在Api<19情况下使用,两者实现功能相同,只不过前者是严格要个图片的width、height、Bitmap.Config完全匹配,才算命中缓存,后者不严格要求图片的大小size和Config完全匹配,获取到的缓存Bitmap的size,可能会大于要求图片的size,再通过Bitmap.reconfigure()重新配置成符合要求的图片,其具体代码如下:

     public Bitmap get(int width, int height, Bitmap.Config config) {
        int size = Util.getBitmapByteSize(width, height, config);
        Key bestKey = findBestKey(size, config);
    
        Bitmap result = groupedMap.get(bestKey);
        if (result != null) {
          // Decrement must be called before reconfigure.
          decrementBitmapOfSize(bestKey.size, result);
          //重新配置成符合要求的图片
          result.reconfigure(width, height, config);
        }
        return result;
      }
    
      private Key findBestKey(int size, Bitmap.Config config) {
        Key result = keyPool.get(size, config);
        for (Bitmap.Config possibleConfig : getInConfigs(config)) {
          NavigableMap<Integer, Integer> sizesForPossibleConfig = getSizesForConfig(possibleConfig);
          //获取>= size的最小值
          Integer possibleSize = sizesForPossibleConfig.ceilingKey(size);
          if (possibleSize != null && possibleSize <= size * MAX_SIZE_MULTIPLE) {
            if (possibleSize != size
                || (possibleConfig == null ? config != null : !possibleConfig.equals(config))) {
              keyPool.offer(result);
              result = keyPool.get(possibleSize, possibleConfig);
            }
            break;
          }
        }
        return result;
      }

     

    这两个缓存策略类都是使用GroupedLinkedMap来维护Bitmap缓存,GroupedLinkedMap内部使用了一个名为head的双向链表,链表的key是由bitmap size和config构成的Key,value是一个由bitmap构成的list。这样GroupedLinkedMap中的每个元素就相当于是一个组,这个组中的bitmap具有相同的size和config,同时,为了加快查找速度,添加了keyToEntry的Hashmap,将key和链表中的LinkedEntry对应起来。

    GroupedLinkedMap进行get操作时,会把该组移动链头,返回并移除该组的最后一个元素;put操作会把该组移动链尾,添加到该组尾部;进行trimToSize操作,优先删除链尾的对应组的最后一个元素,当该组没有元素时,删除该组。这里与访问排序的LinkedHashMap有区别了,get和put操作都是把节点移至到链尾,LruCache trimToSize操作时优先删除链头。

     

    ArrayPool

    ArrayPool用于存储不同类型数组的数组池的接口,默认实现LruArrayPool只支持int[],byte[]池化,内部也是使用GroupedLinkedMap维护着,由size和class构成key,获取数组资源时,跟SizeConfigStrategy类似,获取到Array的size,可能会大于要求的size。在图片Bimtap解码的时候有使用到ArrayPool。

     

    五、Bitmap的解码

    先介绍下加载本地资源和远程资源的流程(从DecodeJob中算起)大致如下:

    通常情况下,远程图片通过ModelLoaders拉取图片,返回inoutStream/ByteBuffer等,供后续对应的ResourceDecoder解码器、transformations、ResourceTranscoders转码器、ResourceEncoder编码器处理。

     

    5.Bitmap的解码

     

    图片加载不管源自网络、本地文件都会通过ResourceDecoder编码器对inputStream、ByteBuffer等进行下采样、解码工作,由Downsampler辅助ResourceDecoder完成,Downsampler相关的decode方法如下:

     

    public Resource<Bitmap> decode(..) throws IOException {
        //从ArrayPool获取byte[]资源,设置给inTempStorage
        byte[] bytesForOptions = byteArrayPool.get(ArrayPool.STANDARD_BUFFER_SIZE_BYTES, byte[].class);
        BitmapFactory.Options bitmapFactoryOptions = getDefaultOptions();
        bitmapFactoryOptions.inTempStorage = bytesForOptions;
    
        ...
    
        try {
          Bitmap result = decodeFromWrappedStreams(is, bitmapFactoryOptions,
              downsampleStrategy, decodeFormat, isHardwareConfigAllowed, requestedWidth,
              requestedHeight, fixBitmapToRequestedDimensions, callbacks);
          return BitmapResource.obtain(result, bitmapPool);
        } finally {
          releaseOptions(bitmapFactoryOptions);
          //byte[]资源回收到ArrayPool
          byteArrayPool.put(bytesForOptions);
        }
      }
    
     private Bitmap decodeFromWrappedStreams(...) throws IOException {
         ...
         //获取原图片的宽高
         int[] sourceDimensions = getDimensions(is, options, callbacks, bitmapPool);
    
        //计算BitmapFactory.Options 缩放相关参数 
        //比如 inSampleSize、inScaled、inDensity、inTargetDensity
        calculateScaling(...);
    
        //设置inPreferredConfig、inDither
        calculateConfig(...);
    
        boolean isKitKatOrGreater = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
    
        if ((options.inSampleSize == 1 || isKitKatOrGreater) && shouldUsePool(imageType)) {
    
          if (expectedWidth > 0 && expectedHeight > 0) {
            //从bitmapPool获取bitmap资源,设置options.inBitmap
           //options.inBitmap = bitmapPool.getDirty(width, height, expectedConfig)
            setInBitmap(options, bitmapPool, expectedWidth, expectedHeight);
          }
        }
        Bitmap downsampled = decodeStream(is, options, callbacks, bitmapPool);
        callbacks.onDecodeComplete(bitmapPool, downsampled);
    
       ...
    
        Bitmap rotated = null;
        if (downsampled != null) {
          // If we scaled, the Bitmap density will be our inTargetDensity. Here we correct it back to
          // the expected density dpi. 对bitmap 设置Density
          downsampled.setDensity(displayMetrics.densityDpi);
          //对图片进行旋转
          rotated = TransformationUtils.rotateImageExif(bitmapPool, downsampled, orientation);
          if (!downsampled.equals(rotated)) {
            //rotated后的Bitmap不是原Bitmap,回收原Bitmap
            bitmapPool.put(downsampled);
          }
        }
    
        return rotated;
      }
    
    private static Bitmap decodeStream(...) throws IOException {
        ...
        final Bitmap result;
        TransformationUtils.getBitmapDrawableLock().lock();
        try {
          result = BitmapFactory.decodeStream(is, null, options);
        } catch (IllegalArgumentException e) {
          IOException bitmapAssertionException =
              newIoExceptionForInBitmapAssertion(e, sourceWidth, sourceHeight, outMimeType, options);
          //重用已经存在的Bitmap,失败,在尝试不重用已经存在的bitmap,
          //且把该bitmap回收到bitmapPool
          if (options.inBitmap != null) {
            try {
              is.reset();
              bitmapPool.put(options.inBitmap);
              options.inBitmap = null;
              return decodeStream(is, options, callbacks, bitmapPool);
            } catch (IOException resetException) {
              throw bitmapAssertionException;
            }
          }
          throw bitmapAssertionException;
        } finally {
          TransformationUtils.getBitmapDrawableLock().unlock();
        }
    
        if (options.inJustDecodeBounds) {
          is.reset();
    
        }
        return result;
      }

     

    对inputStream(ByteBuffer等也雷同)的decode过程分析如下:

    1.给BitmapFactory.Options选项设置了inTempStorage

    inTempStorage为Bitmap解码过程中需要缓存空间,就算我们没有配置这个,系统也会给我们配置,相关代码如下:

     

    private static Bitmap decodeStreamInternal(@NonNull InputStream is,
                @Nullable Rect outPadding, @Nullable Options opts) {
           // ASSERT(is != null);
            byte [] tempStorage = null;
            if (opts != null) tempStorage = opts.inTempStorage;
            if (tempStorage == null) tempStorage = new byte[DECODE_BUFFER_SIZE];
            return nativeDecodeStream(is, tempStorage, outPadding, opts);
    }

     

    不过,这里每次decode过程,就会申请和释放DECODE_BUFFER_SIZE的内存空间,多次docode可能会造成频繁gc和内存抖动;而Glide却从ArrayPool获取,设置给inTempStorage,decode完成后,又会回收到ArrayPool中,可以减少内存抖动。

    2.获取图片原始宽高

    获取图片资源的原始宽高,设置参数inJustDecodeBounds为True即可,没什么特别的,然后对inputStream进行reset,以便后续的真正的decode动作。

    3.计算缩放因子,配置Options的inSampleSize、inScaled、inDensity、inTargetDensity

    4.配置Options的inPreferredConfig、inDither,首先判断是否允许设置硬件位图,允许则inPreferredConfig设置为HARDWARE,inMutable为false,否则再解析流中的ImageHeader数据,假如有透明通道,inPreferredConfig设置为ARGB_8888,没有则为RGB_565,同时inDither置为True。

     

    注意HARDWARE的bitmap不能被回收到BitmapPool,具体查看LruBitmapPool的put方法;其相应的像素数据只存在于显存中,并对图片仅在屏幕上绘制的场景做了优化,具体详述查看Glide官方文档-硬件位图。

     

    5.设置inBitmap,如果为硬件位图配置,则不设置inBitmap。其他情况,从BitmapPool获取Bitmap,设置给inBitmap。

     

    6.配置完Options后,就真正调用BitmapFactory的decode方法,解码失败再尝试一次取消inBitmap进行解码,并对inBitmap回收BitmapPool。然后setDensity(绘制时缩放,decodedBtimap本身占用内存没有变化),decodedBtimap最后根据exifOrientation,旋转位图。

     

    6、网络栈的切换

     

    Glide最终使用的网络加载ModelLoader为HttpGlideUrlLoader,其对应的DataFetcher为HttpUrlFetcher,使用HttpURLConnection进行网络请求。

     

    Glide可以自由定制加载器ModelLoader,资源解码器ResourceDecoder,资源编码器ResourceEncoder,这里想进行底层网络库切换,定制ModelLoader即可,教材可以参考Glide文档,官方提供了OkHttp和Volley 集成库。

     

    定制的加载器,解码器,编码器自动注入到Glide的原理如下:

     

    1.定制LibraryGlideModule类,通过其 registerComponents()方法的形参Registry登记所有定制的加载器ModelLoader,资源解码器Decoder,资源编码器Encoder,给定制的LibraryGlideModule类添加@GlideModule注解,编译期间自动在AndroidManifest.xml文件中添加该LibraryGlideModule相关的元数据。

     

    2.在Glide初始化时,会从功能配置文件AndroidManifest.xml中获取相关GlideModule元数据,并通过反射实例化所有的GlideModule,再迭代所有定制的GlideModule调用registerComponents方法,这样那些定制的加载器ModelLoader,解码器Decoder,编码器Encoder就自动注入到Glide了。关键源码如下:

     

    private static void initializeGlide(@NonNull Context context, @NonNull GlideBuilder builder) {
        Context applicationContext = context.getApplicationContext();
        GeneratedAppGlideModule annotationGeneratedModule = getAnnotationGeneratedGlideModules();
        List<com.bumptech.glide.module.GlideModule> manifestModules = Collections.emptyList();
        if (annotationGeneratedModule == null || annotationGeneratedModule.isManifestParsingEnabled()) {
          manifestModules = new ManifestParser(applicationContext).parse();
        }
    
       ...
    
        for (com.bumptech.glide.module.GlideModule module : manifestModules) {
          module.applyOptions(applicationContext, builder);
        }
        if (annotationGeneratedModule != null) {
          annotationGeneratedModule.applyOptions(applicationContext, builder);
        }
        Glide glide = builder.build(applicationContext);
        for (com.bumptech.glide.module.GlideModule module : manifestModules) {
          module.registerComponents(applicationContext, glide, glide.registry);
        }
    
        ...
    }

     

    至此,6个问题都解析完毕,相信能对Glide有更深刻的整体认识。

     

    参考资料

     Glide v4 快速高效的Android图片加载库(官方)

    Glide高级详解—缓存与解码复用

    [Glide4源码解析系列] — 3.Glide数据解码与转码

    android.support.v4.util.Pools源码解析

     Glide4.8源码拆解(四)Bitmap解析之"下采样"浅析

     

     如果您对博主的更新内容持续感兴趣,请关注公众号!

     

    转载于:https://www.cnblogs.com/sihaixuan/p/10925585.html

    展开全文
  • 不同视角(不同基向量)下...(注意人类视角和panda视角虽然都是[0,1] [1,0],但其实是不一样的) 目前已知:人类视角基向量[0,1] [1,0],panda的基向量在人类视角下为[3,1],[1,1],有一个向量在人类视角下为[5,2],那么...

    不同视角(不同基向量)下的向量转换如下图:

    在这里插入图片描述
    图片注释:在二维空间中,人类视角基向量为[0,1] [1,0],在这个空间中熊猫panda也有一组基向量[0,1][1,0] 。(注意人类视角和panda视角虽然都是[0,1] [1,0],但其实是不一样的)
    目前已知:人类视角基向量[0,1] [1,0],panda的基向量在人类视角下为[3,1],[1,1],有一个向量在人类视角下为[5,2],那么在panda视角下这组向量坐标为多少?

    具体算法如下:
    在这里插入图片描述
    若已知向量a在panda的坐标系中为1/2[3,1],那么若要求在人类眼中向量a的坐标?
    即:将panda视角向量——》人类视角向量,此时用到panda基座标在人类基座标中的表示[3 1 ,1 1].
    同理:人类视角向量——》panda视角向量,此时用到了人类基座标在panda基座标下的表示,也就是[3 1 ,1 1]的逆矩阵。

    展开全文
  • 欢迎来到theFlyer的博客—希望你有不一样的感悟 前言:这是一篇要讲的论文,以下内容是基于个人理解写的,从简单的NMF讲到论文的多视角方法,由于之前做了PPT所以以下图片可能比较多。 论文名称 Diverse ...

    欢迎来到theFlyer的博客—希望你有不一样的感悟

    前言:这是一篇要讲的论文,以下内容是基于个人理解写的,从简单的NMF讲到论文的多视角方法,由于之前做了PPT所以以下图片可能比较多。

    论文名称 Diverse Non-Negative Matrix Factorization for Multiview Data Representation

    1.矩阵分解

    ##1.1传统矩阵分解

    矩阵分解在很多领域获得了广泛的应用.
    在应用统计学领域, 通过矩阵分解得到原数据矩阵的低秩逼近, 从而可以发现数据的内在结构特征.

    传统矩阵分解
    ##1.2非负矩阵分解

    选择非负矩阵分解原因
    在数学上,从计算的观点看,分解结果存在负值是正确的,但负值元素在实际结果往往没有意义,例如图像数据没有负值像素点。

    非负矩阵分解

    ###1.2.1NMF简介
    矩阵分解最初用在推荐上,有一个矩阵V行是用户,列是商品,矩阵上面的值代表着商品的打分。但是矩阵有些商品用户是没有打分的,但是作为商家想知道用户的喜好。 于是利用一个矩阵V可以分成两个矩阵W,H相乘。(V上面的一个点 是另外两个矩阵某行和某列相乘) 。–开始,我们随机初始化W,H,发现WH上某行与某列相乘的结果与商品打分V矩阵上的值是不同的有误差,所有我们就需要更新WH来减少误差。一般采取梯度下降和乘法规则。

    NMF分解的目的是为非负矩阵V,寻找适当的非负基矩阵W和非负系数矩阵H,使它们的乘积近似于原始非负矩阵V,可写为如下形式:

    这里写图片描述
    ###1.2.2NMF最小化目标函数

    1.对于平方距离的损失函数
    2.对于KL散度的损失函数

    这里写图片描述
    ###1.2.3不同损失的乘法更新规则

    1.对于平方距离的损失函数
    2.对于KL散度的损失函数

    这里写图片描述
    ###1.2.4乘法更新规则和梯度下降是等价的

    乘法规则主要是为了计算的过程中保证非负,而基于 梯度下降的方法中加减运算无法保证非负,其实上述乘法更新规则与基于梯度下降的算法是等价的,下面以平方距离为损失函数说明上述过程的等价性。

    这里写图片描述
    ###1.2.5非负矩阵分解及参数更新举例

    利用乘法更新规则更新

    这里写图片描述

    2.论文讲述-多视角的非负矩阵

    ###2.1论文的主要工作

    文章主要完成四件事

    这里写图片描述
    ###2.2论文的主要目的

    1.传统非负矩阵分解
    这些方法一个主要的局限是多视角学习的数据表示有相互冗余信息,且缺乏不同的信息。利用由多视角分享的共同信息而忽略了多视角的差异性。
    2.DiNMF
    DiNMF能通过数据表示捕捉到不同的信息。最后H*不仅有有已经存在方法捕捉到的共同的信息,还保存了不同视角的独特信息,因此更加综合和精确。

    这里写图片描述
    这里写图片描述
    ###2.3DiNMF多样非负矩阵分解

    DiNMF相比普通矩阵分解如何提取特征呢?
    一个让人满意的多视角非负矩阵分解方法需满足两个条件:
    1.为了学习的综合性和精确性,能够通过多视角数据表示开拓不同的信息
    2.当数据和唯独很大时,是可伸缩的
    --------文章使用L0范式、L1范式来保证差异

    这里写图片描述
    这里写图片描述
    ###2.4DiNMF的算法

    算法流程和初始化

    这里写图片描述
    ###2.5DiNMF算法及更新举例

    迭代方法和之前的非负矩阵分解类似,假设目前已经得到优化后H

    这里写图片描述
    这里写图片描述
    ###2.6DiNMF改进LP-DiNMF

    提出改进的原因是为了揭露隐藏的语义并关注内在的几何结构,找到一个紧凑的表示。下式利用了图正则的思想,考虑了数据集携带的几何信息。

    这里写图片描述

    后记

    努力不会徒劳,伟大并非凑巧。最近因为一些事,也有些感慨,下面是三毛的一句话,
    刻意去找的东西,往往是找不到的。天下万物的来和去,都有他的时间。

    欢迎关注个人公众号欢迎关注个人公众号-女娲之心

    展开全文
  • Maya建模首先需要把MAYA的操作视图切换到特定的视图下,每个视图对应的视角不一样,(按空格键和移动鼠标进行视图切换)点开需要放置参考图的窗口点击"视图";建模小知识点击导入图像:游戏建模找到参考图:建模...

    4e1b74611bca1594ef10627066d8e9a7.png

    之前上传过一个史迪仔的制作视频,然后就有很多小伙伴问到我图片如何导进去,诺,教程这不是来了吗,学会了记得双击666给个关注哦!

    bfb1f710d93aaf31f26b5ce2b57560b0.png

    Maya建模
    首先需要把MAYA的操作视图切换到特定的视图下,每个视图对应的视角都不一样,(按空格键和移动鼠标进行视图切换)
    点开需要放置参考图的窗口点击"视图";

    b00540f80bfea3eda080e6504c2a7817.png

    建模小知识
    点击导入图像:

    297343290cf75be28c6a7fab532efbba.png

    游戏建模
    找到参考图:

    30b75b1792d881e31555c65171a64462.png

    建模设计
    导入图片后可进行自由缩放旋转;最后因为图片是可以被框选的所以为了避免误操作,可以进行图片固定,需要zb快捷键的小伙伴点击这里
    下一篇分享Maya快捷键大全。
    如下:黄色箭头按键创建图层;青色箭头按键隐藏、显示图层;红色箭头按键到R课固定图层;

    01fef66f1d900621f17c963a05dfe6fc.png


    这一步是新手同学们的必经之路
    每日学习一点点 进步一点点 ,
    也能创建自己的模型啦!
    这个小知识大家get到了吗?
    学到的同学记得点关注哦
    还有很多就暂时不展示了,希望你可以在我的视频获取知识一起成长,如果有建模上的问题也可以找我解决,我会尽力帮助大家快速掌握Maya软件。那么怎么获取这套视频呢,你只需要转发本篇文章然后私信叮当"学习礼包 就可以领取全套课程啦,后续还会有更多的干货带给大家!喜欢的记得收藏哦。

    更多3D软件及模型素材教程资源获取方法,点击:一起交流学习

    c7a658140546ff9aa5b16c47e792ca6b.png

    阅读全文

    展开全文
  • 有时算法本身乍一看是不一样的,而且很不一样,比较结构特点,看不出来有什么共性 如果我们转换下看问题的视角,是否能够找到共性呢 1 ) 对比分书问题和八皇后问题 备注:图片托管于github,请确保网络的可...
  • 我们也不是《城市画报》那样受到都市人的追捧,我们只是在讲述我们用不一样的视角看到的故事,我们只是想让我们自己还有懂我们的人在夜深的时候,听着我们我们的音乐,看着我们的图片,品读我们的文字,跟随我们的...
  • 没经历过深度开荒ue4项目,所以里面很多经验是基于unity说,实际有可能不一样; 都是白话,不纠结融合混合transitionblend过渡这些字眼,结合上下文 里面所有基于事实和目的推测都有可能只是人家出bug了没...
  • 但是因为不同相机之间特性参数不一样,会产生色彩一致性(color consistency)和色感一致性(color constancy)问题。由于色感是人主观感受,不易进行量化比较。所以这里只解决色彩不一致性问题。 如下图所...
  • 为了实现第一人称视角下面壁时手臂怼到墙里效果,我使用两个摄像机:FPCamera 和 GunCamera,使用Camera组件Culling Mask属性让FPCamera拍摄除手臂之外所有场景,GunCamera只拍摄...
  • //如果灰度图没有创建,就创建一个和原图一样大小灰度图(8位色深,单通道) //if (grayFrame.empty()) { grayFrame = cv::Mat(frame.size(), IPL_DEPTH_8U, 1); //} //原图转灰度图 //cv::cvtColor(frame, ...
  • 好图看看 v1.0.9.8

    2019-03-20 14:46:25
    从另一个视角看自己,看到不一样的自己,体验新的看图神器不一样的功能。 界面优化,大小随心 界面大小随心切换,无时无刻伴在您身边。 速度优化,秒开大图 速度提升,给您全新的快速体验。 享你所想 更多贴心使用...
  • chapter3-震荡

    2020-01-05 14:30:19
    效果演示 运动规律 刚看到这张图片,感觉是两个“8”字形原点绕着一个中心进行旋转。...绑定两个点运动频率一样,振幅不一样,相位不一样。每相邻两组点都有一定相位差。知道了这一点,我已经掌握了他运...
  • 720度全景图片坚信很多小伙伴们都... 全景图能够让客户根据显示屏或VR眼镜亲临其境体会当场具体情况,而且客户能够随意挪动角度,收看720度全景图的不一样视角。也就是说720度全景图是能够追随客户角度而随意挪
  • 仅仅是画漫画的问题,画其他原画插画都是一样的。首先你想学的是漫画,需要了解漫画是需要画成什么样的一个形式。漫画基本分几个重点需要学习的地方,一个明确的线稿,一个是视角和分镜,一个是故事剧情的走向。...
  • OGRE动画

    2017-12-25 19:33:29
    动画是由若干静态画面,快速交替显示而成。因人眼睛会产生视角暂留,对上一个画面感知还末消失,下...不过Ogre本身并能像人一样了解场景中动画角色动作具体含义,例如它能明确知道什么是抬手或者走路,只能
  • 全景拍摄怎么操作?

    2020-08-04 15:55:02
    无论是航拍还是地面拍摄,原理都是一样的,用镜头进行全方位的拍摄,简单来讲,就是在拍摄一个视角以后,旋转摄像镜头拍摄周边场景,两张图片的拼接处低于25%。最后拍摄一张天空和地面的图片。 VR全景视频后期...
  • OGRE 动画

    2013-07-25 15:27:59
    动画是由若干静态画面,快速交替显示而成。因人眼睛会产生视角暂留,对上一个画面感知还末消失,下...不过Ogre本身并能像人一样了解场景中动画角色动作具体含义,例如它能明确知道什么是抬手或者走路,只能
  • 车道线检测在课题当中起着很重要的作用,但是大部分论文都是基于固定摄像头的车道线检测,而本课题是无人机视角,也许会有不一样,但是先拿一张近似的图片去仿真,然后找出问题难点,解决他,再试飞无人机去拍摄。...
  • ogre动画

    2010-12-08 19:55:00
    OGRE 动画收藏 动画是由若干静态画面,快速交替显示而成。因人眼睛会产生视角暂留,对上一个画面感知还末消失,...不过Ogre本身并能像人一样了解场景中动画角色动作具体含义,例如它能明确知道什么是...
  • 很多游戏场景,我们都是直接给定一张背景图片作为其游戏背景,图片背景添加方式和角色添加方式一样,具体步骤这里就做演示了,大家自己动手试试。 图4-2-1-1 在2D斜视角游戏中,常常将地图区域拆分成...
  • 但通俗又不失内涵,简洁又不简陋,非常适合对计算机网络知识有向往但又有惧怕的网络编程爱好者们阅读,希望能给你带来不一样的网络知识入门视角。本篇将运用通俗易懂的语言,配上细致精确的图片动画,循序渐进地引导...
  • 22、 界面设计支持添加任意图像、动画swf和视频(比如透明背景解说人物视频),界面元素也与热点一样,支持点击事件,包括:切换全景场景、打开URL、打开子窗口、播放和暂停。 23、 可自定义界面语言 显示文字;
  • 简单 Java图片加水印,支持旋转和透明度设置 摘要:Java源码,文件操作,图片水印 util实现Java图片水印添加功能,有添加图片水印和文字水印,可以设置水印位置,透明度、设置对线段锯齿状边缘处理、水印图片的路径,...
  • JAVA上百实例源码以及开源项目

    千次下载 热门讨论 2016-01-03 17:37:40
     Java绘制图片火焰效果,源代码相关注释:前景和背景Image对象、Applet和绘制火焰效果Image对象、Applet和绘制火焰效果Graphics对象、火焰效果线程、Applet高度,图片图片装载器、绘制火焰效果X坐标...

空空如也

空空如也

1 2
收藏数 37
精华内容 14
关键字:

一样的图片不一样的视角