精华内容
下载资源
问答
  • Picasso 是 Square 公司出品的一款十分优秀的开源图片框架,也是目前 Android 开发中十分流行的一款图片加载框架。提到 Square 公司大家一定不会陌生,OkHttp、Retrofit、LeakCanary 等等 Android 开发者...

    img

    个人博客:https://blog.N0tExpectErr0r.cn

    小专栏:https://xiaozhuanlan.com/N0tExpectErr0r

    各位好久不见…已经一两个月没更新了…过完年一直在忙着学校的一些事情和项目上的事,就没太多时间写笔记,现在我回来啦。

    Picasso 是 Square 公司出品的一款十分优秀的开源图片框架,也是目前 Android 开发中十分流行的一款图片加载框架。提到 Square 公司大家一定不会陌生,OkHttp、Retrofit、LeakCanary 等等 Android 开发者十分熟悉的开源库都出自他们之手,个人认为他们公司的开源库都十分值得研究,今天就让我们来研究一下 Picasso 这款图片加载框架。

    Picasso 属于三大图片框架(Glide、Picasso、Fresco)之一。相比其他两个框架,它的特点是轻量,占用的体积更少,同时功能相对来说也比较完善。那么今天就来跟我一起分析一波 Picasso 这个图片选择框架的源码。

    此篇文章的源码解析基于 2.71828 版本。

    初始化

    以我的阅读源码的习惯,都是从使用的时候的入口开始入手,因此我们这里从 Picasso 类入手。旧版本的 Picasso 使用了 with 方法作为入口,而在新版本中 with 方法则被 get 方法所替代,并且不再需要传入 Context 参数。那么它是如何实现的呢?下面我们看到它的 get 方法:

    public static Picasso get() {
      if (singleton == null) {
        synchronized (Picasso.class) {
          if (singleton == null) {
            if (PicassoProvider.context == null) {
              throw new IllegalStateException("context == null");
            }
            singleton = new Builder(PicassoProvider.context).build();
          }
        }
      }
      return singleton;
    }
    

    可以看到,这里是一个单例类,而它的 Context 则由一个没有任何实现的 PicassoProvider 这个 ContentProvider 来提供,从而使用户不再需要传入一个 Context。

    @RestrictTo(LIBRARY)
    public final class PicassoProvider extends ContentProvider {
    
      @SuppressLint("StaticFieldLeak") static Context context;
    
      @Override public boolean onCreate() {
        context = getContext();
        return true;
      }
    	// ...省略 ContentProvider 的默认实现
    }
    

    之后,它调用了 Builder 的 build 方法返回了一个 Picasso 对象。我们先看到 Builder 的构造方法:

    public Builder(@NonNull Context context) {
      if (context == null) {
        throw new IllegalArgumentException("Context must not be null.");
      }
      this.context = context.getApplicationContext();
    }
    

    可以看到仅仅是判空并赋值。接着我们看看 build 方法:

    public Picasso build() {
      Context context = this.context;
      if (downloader == null) {
        downloader = new OkHttp3Downloader(context);
      }
      if (cache == null) {
        cache = new LruCache(context);
      }
      if (service == null) {
        service = new PicassoExecutorService();
      }
      if (transformer == null) {
        transformer = RequestTransformer.IDENTITY;
      }
      Stats stats = new Stats(cache);
      Dispatcher dispatcher = new Dispatcher(context, service, HANDLER, downloader, cache, stats);
      return new Picasso(context, dispatcher, cache, listener, transformer, requestHandlers, stats,
          defaultBitmapConfig, indicatorsEnabled, loggingEnabled);
    }
    

    build 方法中对 downloader、cache 等变量进行了初始化,同时返回了一个新的 Picasso 对象,前面的变量我们先不关心。先看到 Picasso 的构造方法:

    Picasso(Context context, Dispatcher dispatcher, Cache cache, Listener listener,
        RequestTransformer requestTransformer, List<RequestHandler> extraRequestHandlers, Stats stats,
        Bitmap.Config defaultBitmapConfig, boolean indicatorsEnabled, boolean loggingEnabled) {
      this.context = context;
      this.dispatcher = dispatcher;
      this.cache = cache;
      this.listener = listener;
      this.requestTransformer = requestTransformer;
      this.defaultBitmapConfig = defaultBitmapConfig;
      int builtInHandlers = 7; // Adjust this as internal handlers are added or removed.
      int extraCount = (extraRequestHandlers != null ? extraRequestHandlers.size() : 0);
      List<RequestHandler> allRequestHandlers = new ArrayList<>(builtInHandlers + extraCount);
      // ResourceRequestHandler needs to be the first in the list to avoid
      // forcing other RequestHandlers to perform null checks on request.uri
      // to cover the (request.resourceId != 0) case.
      allRequestHandlers.add(new ResourceRequestHandler(context));
      if (extraRequestHandlers != null) {
        allRequestHandlers.addAll(extraRequestHandlers);
      }
      allRequestHandlers.add(new ContactsPhotoRequestHandler(context));
      allRequestHandlers.add(new MediaStoreRequestHandler(context));
      allRequestHandlers.add(new ContentStreamRequestHandler(context));
      allRequestHandlers.add(new AssetRequestHandler(context));
      allRequestHandlers.add(new FileRequestHandler(context));
      allRequestHandlers.add(new NetworkRequestHandler(dispatcher.downloader, stats));
      requestHandlers = Collections.unmodifiableList(allRequestHandlers);
      this.stats = stats;
      this.targetToAction = new WeakHashMap<>();
      this.targetToDeferredRequestCreator = new WeakHashMap<>();
      this.indicatorsEnabled = indicatorsEnabled;
      this.loggingEnabled = loggingEnabled;
      this.referenceQueue = new ReferenceQueue<>();
      this.cleanupThread = new CleanupThread(referenceQueue, HANDLER);
      this.cleanupThread.start();
    }
    

    可以看到,主要是对 requestHandlers 这个 List 进行初始化以及各个变量进行初始化。通过上面的几个名字可以看出来 RequestHandler 就是 Picasso 对各种类型的图片加载的抽象。通过实现 RequestHandler 接口可以实现不同的图片加载策略。

    创建请求

    之后我们调用了 load 方法并传入了具体的参数。它有许多重载,可以传入 Uri、String、File、resourceId 等等类型的数据。

    我们以 load(String) 为例:

    public RequestCreator load(@Nullable String path) {
      if (path == null) {
        return new RequestCreator(this, null, 0);
      }
      if (path.trim().length() == 0) {
        throw new IllegalArgumentException("Path must not be empty.");
      }
      return load(Uri.parse(path));
    }
    

    可以看到,它最终调用的还是 load(Uri) 方法。其实所有的其他重载最后都会指向 load(Uri) 方法,也就是说我们-各种形式的数据源最后都是以 Uri 的形式存在于 Picasso 中。我们下面看到 load(Uri):

    public RequestCreator load(@Nullable Uri uri) {
      return new RequestCreator(this, uri, 0);
    }
    

    它构造了一个 RequestCreator 并返回。接下来我们看到 RequestCreator 的构造方法:

    RequestCreator(Picasso picasso, Uri uri, int resourceId) {
      if (picasso.shutdown) {
        throw new IllegalStateException(
            "Picasso instance already shut down. Cannot submit new requests.");
      }
      this.picasso = picasso;
      this.data = new Request.Builder(uri, resourceId, picasso.defaultBitmapConfig);
    }
    

    它调用了 Request.Builder 的构造方法来为 data 进行赋值,我们看到这个构造方法:

    Builder(Uri uri, int resourceId, Bitmap.Config bitmapConfig) {
      this.uri = uri;
      this.resourceId = resourceId;
      this.config = bitmapConfig;
    }
    

    可以看到,这里主要是对 Bitmap.Config 等属性进行设置。

    配置加载属性

    在我们创建了 RequestCreator 后,可以调用它的 placeholder、error 等等方法为本次加载设置占位图、错误图等等各种属性的设置,下面我们以 placeholder(int) 方法举例:

    public RequestCreator placeholder(@DrawableRes int placeholderResId) {
      if (!setPlaceholder) {
        throw new IllegalStateException("Already explicitly declared as no placeholder.");
      }
      if (placeholderResId == 0) {
        throw new IllegalArgumentException("Placeholder image resource invalid.");
      }
      if (placeholderDrawable != null) {
        throw new IllegalStateException("Placeholder image already set.");
      }
      this.placeholderResId = placeholderResId;
      return this;
    }
    

    其实这里就是为 RequestCreator 中的这些属性赋值。

    那么所有通过 RequestCreator 设定的属性都是放在 RequestCreator 这个类中的么?

    其实不是的,与加载过程有关的属性是放在 RequestCreator 中的,而与图片相关的属性则是放在 Request.Builder 中。

    可能看到这里有点乱,大概解释一下。

    比如 placeholder、error、memoryPolicy 这些属性就是与加载过程有关而与图片无关的

    而比如 resize、centerCrop 这些就是与图片的显示效果有关的属性,也就是图片相关属性。

    我们以 resize 为例来看看整体流程:

    public RequestCreator resize(int targetWidth, int targetHeight) {
      data.resize(targetWidth, targetHeight);
      return this;
    }
    

    我们看到 Request.Builder 中的 resize 方法:

    public Builder resize(@Px int targetWidth, @Px int targetHeight) {
      if (targetWidth < 0) {
        throw new IllegalArgumentException("Width must be positive number or 0.");
      }
      if (targetHeight < 0) {
        throw new IllegalArgumentException("Height must be positive number or 0.");
      }
      if (targetHeight == 0 && targetWidth == 0) {
        throw new IllegalArgumentException("At least one dimension has to be positive number.");
      }
      this.targetWidth = targetWidth;
      this.targetHeight = targetHeight;
      return this;
    }
    

    这里就是将 Request.Builder 中的一些属性进行了赋值。

    加载图片

    当属性都设定完后,我们便可以调用 into 方法来加载图片,我们看到 into(ImageView):

    public void into(ImageView target) {
      into(target, null);
    }
    

    它调用了 into(ImageView, Callback):

    public void into(ImageView target, Callback callback) {
      long started = System.nanoTime();
      // 1
      // 检查是否在主线程
      checkMain();	
      if (target == null) {
        throw new IllegalArgumentException("Target must not be null.");
      }
      if (!data.hasImage()) {
      	// 之前设置的 uri 是否有数据(实际上也是判空)
        picasso.cancelRequest(target);
        if (setPlaceholder) {
          setPlaceholder(target, getPlaceholderDrawable());
        }
        return;
      }
      // 2
      if (deferred) {
      	// 是否自适应 Target 宽高
        if (data.hasSize()) {
          throw new IllegalStateException("Fit cannot be used with resize.");
        }
        int width = target.getWidth();
        int height = target.getHeight();
        if (width == 0 || height == 0) {
          if (setPlaceholder) {
            setPlaceholder(target, getPlaceholderDrawable());
          }
          picasso.defer(target, new DeferredRequestCreator(this, target, callback));
          return;
        }
        data.resize(width, height);
      }
      // 3
      Request request = createRequest(started);
      String requestKey = createKey(request);
      // 4
      if (shouldReadFromMemoryCache(memoryPolicy)) {
      	// 从内存缓存中获取图片
        Bitmap bitmap = picasso.quickMemoryCacheCheck(requestKey);
        if (bitmap != null) {
          // 找到缓存的图片
          picasso.cancelRequest(target);
          setBitmap(target, picasso.context, bitmap, MEMORY, noFade, picasso.indicatorsEnabled);
          if (picasso.loggingEnabled) {
            log(OWNER_MAIN, VERB_COMPLETED, request.plainId(), "from " + MEMORY);
          }
          if (callback != null) {
            callback.onSuccess();
          }
          return;
        }
      }
      // 5
      if (setPlaceholder) {
        setPlaceholder(target, getPlaceholderDrawable());
      }
      // 6
      Action action =
          new ImageViewAction(picasso, target, request, memoryPolicy, networkPolicy, errorResId,
              errorDrawable, requestKey, tag, callback, noFade);
      picasso.enqueueAndSubmit(action);
    }
    

    这里代码比较长,我们慢慢分析,先看看整体大体流程。

    首先在注释 1 处进行了一系列判断操作,具体可看注释

    之后在注释 2 处,是 fit() 的具体实现。如果外部调用了 fit 使图片自适应 target 的大小,则获取 target 的大小并调用 resize 方法进行设置。这里要特别注意的是如果宽高为 0 则说明 ImageView 的尺寸还没有获取到,此时会延时该图片请求直到获取到 ImageView 的宽高。

    之后 3 处构建了一个 Request,并调用 createKey 方法由该 Request 及其各种信息构建了一个 String 类型的 key。

    之后在注释 4 处,在使用内存缓存策略的情况下,先调用 quickMemoryCacheCheck 方法获取到了内存缓存中的 BitMap,如果找到则调用 setBitmap 方法将图片应用到 target 中。

    然后在注释 5 处,如果内存没有缓存,且设置了占位图,则给它添加占位图。

    最后在注释 6 处,构造了一个 Action 对象然后调用了 picasso 的 enqueueAndSubmit 进行网络请求。

    Request 的创建

    首先,我们看看 Request 是如何创建的,看到 createRequest 方法:

    private Request createRequest(long started) {
      int id = nextId.getAndIncrement();
      // 1
      Request request = data.build();
      request.id = id;
      request.started = started;
      boolean loggingEnabled = picasso.loggingEnabled;
      if (loggingEnabled) {
        log(OWNER_MAIN, VERB_CREATED, request.plainId(), request.toString());
      }
      // 2
      Request transformed = picasso.transformRequest(request);
      if (transformed != request) {
      	// 3
        // If the request was changed, copy over the id and timestamp from the orig
        transformed.id = id;
        transformed.started = started;
        if (loggingEnabled) {
          log(OWNER_MAIN, VERB_CHANGED, transformed.logId(), "into " + transformed)
        }
      }
      return transformed;
    }
    

    可以看到,这里首先在注释 1 处调用了 Request.Builder 的 build 方法创建了 Request,之后在注释 2 处调用了 picasso 的 transformRequest 方法对 Request 进行转换。

    获取 Request

    我们先看看 Request.Builder 的 build 方法:

    public Request build() {
      if (centerInside && centerCrop) {
        throw new IllegalStateException("Center crop and center inside can not be used together.");
      }
      if (centerCrop && (targetWidth == 0 && targetHeight == 0)) {
        throw new IllegalStateException(
            "Center crop requires calling resize with positive width and height.");
      }
      if (centerInside && (targetWidth == 0 && targetHeight == 0)) {
        throw new IllegalStateException(
            "Center inside requires calling resize with positive width and height.");
      }
      if (priority == null) {
        priority = Priority.NORMAL;
      }
      return new Request(uri, resourceId, stableKey, transformations, targetWidth, targetHeight,
          centerCrop, centerInside, centerCropGravity, onlyScaleDown, rotationDegrees,
          rotationPivotX, rotationPivotY, hasRotationPivot, purgeable, config, priority);
    }
    

    这里就是创建 Request 对象并将各种 Request.Builder 中的属性传递给这个 Request 对象。

    转换 Request

    然后我们再看看 picasso 的 transformRequest 方法:

    Request transformRequest(Request request) {
      Request transformed = requestTransformer.transformRequest(request);
      if (transformed == null) {
        throw new IllegalStateException("Request transformer "
            + requestTransformer.getClass().getCanonicalName()
            + " returned null for "
            + request);
      }
      return transformed;
    }
    

    这里调用了 requestTransformer 的 transformRequest 方法来进行转换。而这个 requestTrasformer 则是之前在 Picasso.Builder 中的 build 方法中初始化给 transformer 的 RequestTransformer.IDENTITY:

    if (transformer == null) {
      transformer = RequestTransformer.IDENTITY;
    }
    

    我们看看它的 transformRequest 的实现:

    RequestTransformer IDENTITY = new RequestTransformer() {
      @Override public Request transformRequest(Request request) {
        return request;
      }
    };
    

    可以看到这里是返回了原始的 Request。

    既然都是返回默认 Request,为什么 Picasso 还要在创建的时候添加这一步 transform 的过程呢?

    其实这个 transformer 我们是可以通过 Builder 的 requestTransformer 方法来进行设置的。也就是说这里主要是提供给用户对 Request 进行一些特殊处理的渠道,使得我们可以对图片加载的过程进行一定的扩展与定制。这种设计是值得我们去学习的。

    之后我们回到 Request 创建的部分,可以看到这里如果对 Request 进行了修改,在注释 3 处会将原 Request 的 id 和 started 赋值过去,从而防止用户对它们进行修改。

    key 的生成

    我们再来看看 Request 的 key 是如何生成的:

    static String createKey(Request data, StringBuilder builder) {
      if (data.stableKey != null) {
        builder.ensureCapacity(data.stableKey.length() + KEY_PADDING);
        builder.append(data.stableKey);
      } else if (data.uri != null) {
        String path = data.uri.toString();
        builder.ensureCapacity(path.length() + KEY_PADDING);
        builder.append(path);
      } else {
        builder.ensureCapacity(KEY_PADDING);
        builder.append(data.resourceId);
      }
      builder.append(KEY_SEPARATOR);
      if (data.rotationDegrees != 0) {
        builder.append("rotation:").append(data.rotationDegrees);
        if (data.hasRotationPivot) {
          builder.append('@').append(data.rotationPivotX).append('x').append(data.rotationPivotY);
        }
        builder.append(KEY_SEPARATOR);
      }
      if (data.hasSize()) {
        builder.append("resize:").append(data.targetWidth).append('x').append(data.targetHeight);
        builder.append(KEY_SEPARATOR);
      }
      if (data.centerCrop) {
        builder.append("centerCrop:").append(data.centerCropGravity).append(KEY_SEPARATOR);
      } else if (data.centerInside) {
        builder.append("centerInside").append(KEY_SEPARATOR);
      }
      if (data.transformations != null) {
        //noinspection ForLoopReplaceableByForEach
        for (int i = 0, count = data.transformations.size(); i < count; i++) {
          builder.append(data.transformations.get(i).key());
          builder.append(KEY_SEPARATOR);
        }
      }
      return builder.toString();
    }
    

    其实这里就是用一个 StringBuilder 构造了一个 String,将 Request 中的各类信息都存放于 key 中。这个 key 其实就是用于内存缓存中的 key。

    图片的加载

    我们先不去查看内存缓存的部分,留到后面来讲解,我们先看看图片是如何从网络加载的。先看到 into 方法的下面这两句:

    Action action =
        new FetchAction(picasso, request, memoryPolicy, networkPolicy, tag, key, callback);
    picasso.submit(action);
    

    构造 Action

    我们先看到 FetchAction 的构造方法:

    FetchAction(Picasso picasso, Request data, int memoryPolicy, int networkPolicy, Object tag,
        String key, Callback callback) {
      super(picasso, null, data, memoryPolicy, networkPolicy, 0, null, key, tag, false);
      this.target = new Object();
      this.callback = callback;
    }
    

    调用了父类的构造方法:

    Action(Picasso picasso, T target, Request request, int memoryPolicy, int networkPolicy,
        int errorResId, Drawable errorDrawable, String key, Object tag, boolean noFade) {
      this.picasso = picasso;
      this.request = request;
      this.target =
          target == null ? null : new RequestWeakReference<>(this, target, picasso.referenceQueue);
      this.memoryPolicy = memoryPolicy;
      this.networkPolicy = networkPolicy;
      this.noFade = noFade;
      this.errorResId = errorResId;
      this.errorDrawable = errorDrawable;
      this.key = key;
      this.tag = (tag != null ? tag : this);
    }
    

    可以看出来,Action 类实际上就是一个携带了需要的信息的类。

    分发 Action

    接着,调用了 picasso 的 submit 方法:

    void submit(Action action) {
      dispatcher.dispatchSubmit(action);
    }
    

    这里调用了 dispatcher 的 dispatchSubmit 方法:

    void dispatchSubmit(Action action) {
      handler.sendMessage(handler.obtainMessage(REQUEST_SUBMIT, action));
    }
    

    这里用到了一个 DispatcherHandler 类的对象调用 sendMessage 方法发送一条信息。这里的 DispatcherHandler 的作用主要是根据不同的调用将 Action 分发到不同的方法中。

    下面我们看到 DispatcherHandler 的实现,它是 Dispatcher 的一个内部类:

    @Override public void handleMessage(final Message msg) {
      switch (msg.what) {
        case REQUEST_SUBMIT: {
          Action action = (Action) msg.obj;
          dispatcher.performSubmit(action);
          break;
        }
        case REQUEST_CANCEL: {
          Action action = (Action) msg.obj;
          dispatcher.performCancel(action);
          break;
        }
        case TAG_PAUSE: {
          Object tag = msg.obj;
          dispatcher.performPauseTag(tag);
          break;
        }
        case TAG_RESUME: {
          Object tag = msg.obj;
          dispatcher.performResumeTag(tag);
          break;
        }
        case HUNTER_COMPLETE: {
          BitmapHunter hunter = (BitmapHunter) msg.obj;
          dispatcher.performComplete(hunter);
          break;
        }
        case HUNTER_RETRY: {
          BitmapHunter hunter = (BitmapHunter) msg.obj;
          dispatcher.performRetry(hunter);
          break;
        }
        case HUNTER_DECODE_FAILED: {
          BitmapHunter hunter = (BitmapHunter) msg.obj;
          dispatcher.performError(hunter, false);
          break;
        }
        case HUNTER_DELAY_NEXT_BATCH: {
          dispatcher.performBatchComplete();
          break;
        }
        case NETWORK_STATE_CHANGE: {
          NetworkInfo info = (NetworkInfo) msg.obj;
          dispatcher.performNetworkStateChange(info);
          break;
        }
        case AIRPLANE_MODE_CHANGE: {
          dispatcher.performAirplaneModeChange(msg.arg1 == AIRPLANE_MODE_ON);
          break;
        }
        default:
          Picasso.HANDLER.post(new Runnable() {
            @Override public void run() {
              throw new AssertionError("Unknown handler message received: " + msg.what);
            }
          });
      }
    }
    

    这里根据不同的 Message 调用了不同的方法,我们的 submit 方法调用了 Dispatcher 中的 performSubmit 方法:

    void performSubmit(Action action) {
      performSubmit(action, true);
    }
    

    它调用了 performSubmit(Action, boolean) 方法:

    void performSubmit(Action action, boolean dismissFailed) {
      if (pausedTags.contains(action.getTag())) {
        pausedActions.put(action.getTarget(), action);
        if (action.getPicasso().loggingEnabled) {
          log(OWNER_DISPATCHER, VERB_PAUSED, action.request.logId(),
              "because tag '" + action.getTag() + "' is paused");
        }
        return;
      }
      // 1
      BitmapHunter hunter = hunterMap.get(action.getKey());
      if (hunter != null) {
        hunter.attach(action);
        return;
      }
      
      // 2
      if (service.isShutdown()) {
        if (action.getPicasso().loggingEnabled) {
          log(OWNER_DISPATCHER, VERB_IGNORED, action.request.logId(), "because shut down")
        }
        return;
      }
      // 3
      hunter = forRequest(action.getPicasso(), this, cache, stats, action);
      hunter.future = service.submit(hunter);
      hunterMap.put(action.getKey(), hunter);
      if (dismissFailed) {
        failedActions.remove(action.getTarget());
      }
      if (action.getPicasso().loggingEnabled) {
        log(OWNER_DISPATCHER, VERB_ENQUEUED, action.request.logId());
      }
    }
    

    首先,在注释 1 处根据 Action 获取到了其对应的 BitmapHunter。

    之后在注释 2 处检查 service 是否被杀掉。

    然后在注释 3 处,调用了 forRequest 获取到了 Action 对应的 BitmapHunter,然后调用了 service 的 submit 方法。

    之后将该 action 与 BitmapHunter 放入了 hunterMap 中。

    BitmapHunter 的获取

    我们看一下前面的步骤中 BitmapHunter 是如何获取的,来到 forRequest方法:

    static BitmapHunter forRequest(Picasso picasso, Dispatcher dispatcher, Cache cache, Stats stats, Action action) {
      Request request = action.getRequest();
      List<RequestHandler> requestHandlers = picasso.getRequestHandlers();
      // Index-based loop to avoid allocating an iterator.
      //noinspection ForLoopReplaceableByForEach
      for (int i = 0, count = requestHandlers.size(); i < count; i++) {
        RequestHandler requestHandler = requestHandlers.get(i);
        if (requestHandler.canHandleRequest(request)) {
          return new BitmapHunter(picasso, dispatcher, cache, stats, action, requestHandler);
        }
      }
      return new BitmapHunter(picasso, dispatcher, cache, stats, action, ERRORING_HANDLER);
    }
    

    这里主要是依次遍历各个 RequestHandler,找到可以处理该类 Request 的 Handler,并构建 BitmapHunter。

    我们先看看 RequestHunter 是如何判断能否处理该类 Request 的,我们以 NetworkRequestHandler 举例:

    @Override public boolean canHandleRequest(Request data) {
      String scheme = data.uri.getScheme();
      return (SCHEME_HTTP.equals(scheme) || SCHEME_HTTPS.equals(scheme));
    }
    

    可以看到,它是通过判断 uri 的 scheme 来判断能否处理该类 Request 的。

    我们接着看到 BitmapHunter 的构造函数:

    BitmapHunter(Picasso picasso, Dispatcher dispatcher, Cache cache, Stats stats, Action action, RequestHandler requestHandler) {
      this.sequence = SEQUENCE_GENERATOR.incrementAndGet();
      this.picasso = picasso;
      this.dispatcher = dispatcher;
      this.cache = cache;
      this.stats = stats;
      this.action = action;
      this.key = action.getKey();
      this.data = action.getRequest();
      this.priority = action.getPriority();
      this.memoryPolicy = action.getMemoryPolicy();
      this.networkPolicy = action.getNetworkPolicy();
      this.requestHandler = requestHandler;
      this.retryCount = requestHandler.getRetryCount();
    }
    

    可以看到,这里主要是各种变量的赋值。

    接着我们看到 service 的 submit 方法,这里的 service 是 PicassoExecutorService:

    @Override
    public Future<?> submit(Runnable task) {
      PicassoFutureTask ftask = new PicassoFutureTask((BitmapHunter) task);
      execute(ftask);
      return ftask;
    }
    

    这里构建了一个 PicassoFutureTask,然后调用了 execute 方法

    我们先看看 PicassoFutureTask 的构造方法:

    PicassoFutureTask(BitmapHunter hunter) {
      super(hunter, null);
      this.hunter = hunter;
    }
    

    PicassoFutureTask 是 FutureTask 的子类,这里主要是变量的赋值。

    图片资源的获取

    接着我们看到 execute 方法,这里其实是调用了 Java 自带的 ThreadPoolExecutor 的 execute 方法。同时这里也说明了这里是一个异步的过程。

    其实 BitmapHunter 是一个 Runnable,当调用了 execute 方法后便会执行它的 run 方法。我们可以看到它的 run 方法:

    @Override public void run() {
      try {
        updateThreadName(data);
        if (picasso.loggingEnabled) {
          log(OWNER_HUNTER, VERB_EXECUTING, getLogIdsForHunter(this));
        }
        result = hunt();
        if (result == null) {
          dispatcher.dispatchFailed(this);
        } else {
          dispatcher.dispatchComplete(this);
        }
      }
      // 省略后面的 catch
    }
    

    这里调用了 hunt 方法获取到了结果 Bitmap,同时在后面根据不同的结果通过 dispatcher 进行结果的处理:

    Bitmap hunt() throws IOException {
      Bitmap bitmap = null;
      // 1
      if (shouldReadFromMemoryCache(memoryPolicy)) {
        bitmap = cache.get(key);
        if (bitmap != null) {
          stats.dispatchCacheHit();
          loadedFrom = MEMORY;
          if (picasso.loggingEnabled) {
            log(OWNER_HUNTER, VERB_DECODED, data.logId(), "from cache");
          }
          return bitmap;
        }
      }
      // 2
      networkPolicy = retryCount == 0 ? NetworkPolicy.OFFLINE.index : networkPolicy;
      // 3
      RequestHandler.Result result = requestHandler.load(data, networkPolicy);
      if (result != null) {
        loadedFrom = result.getLoadedFrom();
        exifOrientation = result.getExifOrientation();
        bitmap = result.getBitmap();
        // If there was no Bitmap then we need to decode it from the stream.
        // 4
        if (bitmap == null) {
          Source source = result.getSource();
          try {
            bitmap = decodeStream(source, data);
          } finally {
            try {
              //noinspection ConstantConditions If bitmap is null then source is guranteed non-null.
              source.close();
            } catch (IOException ignored) {
            }
          }
        }
      }
      
      // 5
      if (bitmap != null) {
        if (picasso.loggingEnabled) {
          log(OWNER_HUNTER, VERB_DECODED, data.logId());
        }
        stats.dispatchBitmapDecoded(bitmap);
        if (data.needsTransformation() || exifOrientation != 0) {
          synchronized (DECODE_LOCK) {
     				// 6
            if (data.needsMatrixTransform() || exifOrientation != 0) {
              bitmap = transformResult(data, bitmap, exifOrientation);
              if (picasso.loggingEnabled) {
                log(OWNER_HUNTER, VERB_TRANSFORMED, data.logId());
              }
            }
            // 7
            if (data.hasCustomTransformations()) {
              bitmap = applyCustomTransformations(data.transformations, bitmap);
              if (picasso.loggingEnabled) {
                log(OWNER_HUNTER, VERB_TRANSFORMED, data.logId(), "from custom transformations");
              }
            }
          }
          if (bitmap != null) {
            stats.dispatchBitmapTransformed(bitmap);
          }
        }
      }
      return bitmap;
    }
    

    这里代码很长,我们慢慢分析:

    首先在注释 1 处尝试从内存通过 key 获取对应 bitmap,若获取到则直接返回。

    之后在注释 2 处,根据 requestHandler 中的 retryCount 来判断是否是网络请求,从而获取不同的 networkPolicy。若 retryCount 为 0 则为离线策略。

    之后在注释 3 处,通过 requestHandler 的 load 方法进行数据的加载,若数据加载成功则进行一些变量的赋值,并获取 bitmap。

    若 bitmap 为空则说明我们需要在注释4处将其从流中 decode 出来。

    之后在注释 5 处就是 Picasso 的加载过程中支持用户对图片进行定制后再应用的具体实现了。这里首先判断是否需要 transform。

    在注释 6 处判断如果需要进行矩阵变换(旋转,放大缩小等),则调用 transformResult 方法进行变换。

    在注释 7 处判断如果有自定义变换,则调用 applyCustomTransformations 进行自定义变换。

    这里的自定义变换比较类似前面的自定义 Request 转换,用户可以在外部自定义 Transformation,并通过 RequestCreator 的 transform 方法传入,这样就可以在图片应用前对 Bitmap 进行一些自定义 (如高斯模糊等)后再应用于 target。这种设计是我们值得学习的。

    RequestHandler 的实现

    下面我们以网络图片对应的 NetworkRequestHandler 为例看看它们的实现,其他的子类可以自己去了解。让我们看到它的 load 方法:

    @Override public Result load(Request request, int networkPolicy) throws IOException {
    	// 1 
      okhttp3.Request downloaderRequest = createRequest(request, networkPolicy);
      // 2
      Response response = downloader.load(downloaderRequest);
      ResponseBody body = response.body();
      if (!response.isSuccessful()) {
        body.close();
        throw new ResponseException(response.code(), request.networkPolicy);
      }
      // Cache response is only null when the response comes fully from the network. Both completely
      // cached and conditionally cached responses will have a non-null cache response.
      Picasso.LoadedFrom loadedFrom = response.cacheResponse() == null ? NETWORK : DISK;
      // Sometimes response content length is zero when requests are being replayed. Haven't found
      // root cause to this but retrying the request seems safe to do so.
      if (loadedFrom == DISK && body.contentLength() == 0) {
        body.close();
        throw new ContentLengthException("Received response with 0 content-length header.");
      }
      if (loadedFrom == NETWORK && body.contentLength() > 0) {
        stats.dispatchDownloadFinished(body.contentLength());
      }
      return new Result(body.source(), loadedFrom);
    }
    

    可以看到,这里是通过 OkHttp3 来实现的图片的加载。

    首先调用 createRequest 方法创建了 OkHttp 的 Request。然后通过自己实现的 OkHttp3Downloader 的 load 方法来实现对这个 Request 的下载请求。

    之后根据缓存的相应是否是空判断数据的来源是从本地还是网络。

    最终构造了一个 Result 并返回。

    OkHttp3.Request 的 创建

    我们先看看如何将 Request 转换为 OkHttp3.Request。让我们看到 createRequest 方法:

    private static okhttp3.Request createRequest(Request request, int networkPolicy) {
      CacheControl cacheControl = null;
      if (networkPolicy != 0) {
        if (NetworkPolicy.isOfflineOnly(networkPolicy)) {
          cacheControl = CacheControl.FORCE_CACHE;
        } else {
          CacheControl.Builder builder = new CacheControl.Builder();
          if (!NetworkPolicy.shouldReadFromDiskCache(networkPolicy)) {
            builder.noCache();
          }
          if (!NetworkPolicy.shouldWriteToDiskCache(networkPolicy)) {
            builder.noStore();
          }
          cacheControl = builder.build();
        }
      }
      okhttp3.Request.Builder builder = new okhttp3.Request.Builder().url(request.uri.toString());
      if (cacheControl != null) {
        builder.cacheControl(cacheControl);
      }
      return builder.build();
    }
    

    可以看到,首先根据 Request 和 NetworkPolicy 的参数设置缓存的各种参数,之后调用 okhttp3.Request.Builder 的构造函数并传入 uri 创建了 Request。

    OkHttp3 数据的获取

    之后我们看到 OkHttp3Downloader 的 load 方法,看看数据获取是如何实现的:

    @NonNull @Override public Response load(@NonNull Request request) throws IOException {
      return client.newCall(request).execute();
    }
    

    其实就是调用 OkHttpClient 的 newCall 方法并调用 execute 获取一个 Response。

    结果的处理

    前面提到,在 BitmapHunter 的 run 方法中根据 hunt() 返回的结果成功与否调用了 dispatcher 的不同方法来进行的结果处理,让我们看看是如何处理的

    if (result == null) {
       dispatcher.dispatchFailed(this);
      } else {
       dispatcher.dispatchComplete(this);
    }
    

    我们先看到 dispatchComplete 方法,它最终通过 handler 调用到了 performComplete 方法中:

    void performComplete(BitmapHunter hunter) {
      if (shouldWriteToMemoryCache(hunter.getMemoryPolicy())) {
        cache.set(hunter.getKey(), hunter.getResult());
      }
      hunterMap.remove(hunter.getKey());
      batch(hunter);
      if (hunter.getPicasso().loggingEnabled) {
        log(OWNER_DISPATCHER, VERB_BATCHED, getLogIdsForHunter(hunter), "for completion");
      }
    }
    

    可以看到,这里如果获取到了结果,且需要内存缓存,则将其放入内存缓存。然后将这个 BitmapHunter 从 Map 中删除。

    之后我们看到 dispatchFailed 方法,它最终通过 handler 调用到了 performError 方法:

    void performError(BitmapHunter hunter, boolean willReplay) {
      if (hunter.getPicasso().loggingEnabled) {
        log(OWNER_DISPATCHER, VERB_BATCHED, getLogIdsForHunter(hunter),
            "for error" + (willReplay ? " (will replay)" : ""));
      }
      hunterMap.remove(hunter.getKey());
      batch(hunter);
    }
    

    这里它将 BitmapHunter 从 Map 中移除,然后就没有进行其他处理了。

    内存缓存

    为了优化流量消耗,Picasso 加入了内存缓存机制,下面我们来看看 Picasso 内存缓存的实现。

    就像其他部分一样,它的内存缓存也考虑到了扩展性,给了用户自己实现的接口。

    我们可以调用 Picasso 类的 memoryCache 方法为其设置 Cache 接口的子类,从而实现自己的内存缓存。

    若用户不传入指定缓存,则默认使用 Picasso 自己实现的 LruCache。

    具体的 LruCache 的设计这里不深入讲解,有兴趣的各位可以去了解一下 LRU 算法,以后可能可以专门开一篇博客讲讲 LRU 算法。

    Dispatcher 设计

    其实从前面的讲解中,你会发现,其实如图片的加载请求,缓存命中等等事件都是由一个叫 Dispatcher 的类分发的,它内部由 Handler 实现,负责将请求封装,并按优先级排序,之后按照类型分发。

    这种设计也很值得我们学习,它作为一个分发中心管理我们的各类请求。使得我们的设计更为清晰,也使得库更容易维护。

    线程池设计

    之前没有提到的就是 Picasso 对线程池也有一些优化,它自己实现了一个 PicassoExecutorService 类,它可以根据当前的网络状态,采用不同的线程池数量,从而使得网络不会过于拥塞。

    具体可以看下面这个方法:

    void adjustThreadCount(NetworkInfo info) {
      if (info == null || !info.isConnectedOrConnecting()) {
        setThreadCount(DEFAULT_THREAD_COUNT);
        return;
      }
      switch (info.getType()) {
        case ConnectivityManager.TYPE_WIFI:
        case ConnectivityManager.TYPE_WIMAX:
        case ConnectivityManager.TYPE_ETHERNET:
          setThreadCount(4);
          break;
        case ConnectivityManager.TYPE_MOBILE:
          switch (info.getSubtype()) {
            case TelephonyManager.NETWORK_TYPE_LTE:  // 4G
            case TelephonyManager.NETWORK_TYPE_HSPAP:
            case TelephonyManager.NETWORK_TYPE_EHRPD:
              setThreadCount(3);
              break;
            case TelephonyManager.NETWORK_TYPE_UMTS: // 3G
            case TelephonyManager.NETWORK_TYPE_CDMA:
            case TelephonyManager.NETWORK_TYPE_EVDO_0:
            case TelephonyManager.NETWORK_TYPE_EVDO_A:
            case TelephonyManager.NETWORK_TYPE_EVDO_B:
              setThreadCount(2);
              break;
            case TelephonyManager.NETWORK_TYPE_GPRS: // 2G
            case TelephonyManager.NETWORK_TYPE_EDGE:
              setThreadCount(1);
              break;
            default:
              setThreadCount(DEFAULT_THREAD_COUNT);
          }
          break;
        default:
          setThreadCount(DEFAULT_THREAD_COUNT);
      }
    }
    

    可以看到,线程池最大线程个数如下:

    • 在 WIFI 网络下,采用最多 4 个线程的线程池
    • 在 4G 网络下,采用最多 3 个线程的线程池
    • 在 3G 网络下,采用最多 2 个线程的线程池
    • 在 2G 网络下,采用最多 1 个线程的线程池

    总结

    Picasso 是一个非常值得我们学习的轻量级图片加载库,它采用 OkHttp3 来加载网络图片,并使用了二级内存缓存来提高加载速度。它的 Dispatcher 思想以及对外部的扩展开放的思想十分值得我们学习,这次源码的阅读还是给了我很大的启发的。

    当然,由于篇幅有限,这篇文章并没有包含 Picasso 的方方面面,它的代码中还有如下的一些点在本文还没有分析,读者们有兴趣的可以从下面的点去研究这个库:

    • 图片加载的暂停与取消
    • 图片的变换实现
    • 请求的优先级
    • 对整体的监控
    • 本地资源的加载
    展开全文
  • 地球实时卫星图片资源-与当前时间只相差30分钟在这里跟大家分享一个有趣的项目。这个项目提供一个实时地球照片源,通过向其服务器发送请求,能抓取到当前地球的照片。对于图片壁纸类的应用来说是一个不错的图片源...

    地球实时卫星图片资源-与当前时间只相差30分钟

    在这里跟大家分享一个有趣的项目。这个项目提供一个实时地球照片源,通过向其服务器发送请求,能抓取到当前地球的照片。对于图片壁纸类的应用来说是一个不错的图片源选择。有550*5501100*11002200*2200分辨率图片资源,您可以将它们集成在您的应用当中,让它成为您的应用的一个绝对亮点。

    该项目的图片来自卫星实时拍摄的地球照片,更新频率为每十分钟一次,如下所示图样:

    示例图片

    该项目不仅提供这些资源,而且还提供配套的基于HTTP网络协议的接口,同时提供全套主流语言 SDK ,如 C、C++、java、object-c 等,用户可以根据自己的开发语言,方便快捷的接入服务。同时也提供原始的HTTP协议与相关的文档和辅助开发的测试工具。

    该项目的图片云服务端从性能、安全性、可扩展性为不同的应用需求提供了全套可靠的服务。

    1. 在可扩展性方面针对实时图片,该项目可以提供不同分辨率的图片,550 * 5001100 * 11002200 * 2200等,并且提供裁剪、压缩、缩放、格式转换、图片效果(调整亮度、对比度)等等功能服务。
    2. 该项目提供原始无损的PNG图片,同时也支持在服务端将图片转换成JPGBMPwebp等格式的图片。
    3. 在性能上面,该项目提供了BGP高速的云服务器,同时提供全网的CDN加速分发的功能,保证APP在任何网络下面都能够快速的获取图片。
    4. 在安全性方面,该项目以HTTP协议为基础,提供了一套安全体系,保证生成的图片URL有安全验证、有过期时间,确保不被盗链。

    更多的信息请查看:

    官方网站

    www.runimg.com

    演示图片

    文档工具

    SDK

    展开全文
  • 本文的文字及图片来源于网络,仅供学习、交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理 本文章来自腾讯云 作者:Python猫 想要学习Python?有问题得不到第一时间解决?来看看这里...

    本文的文字及图片来源于网络,仅供学习、交流使用,不具有任何商业用途,版权归原作者所有,如有问题请及时联系我们以作处理

    本文章来自腾讯云 作者:Python猫

    想要学习Python?有问题得不到第一时间解决?来看看这里“1039649593”满足你的需求,资料都已经上传至文件中,可以自行下载!还有海量最新2020python学习资料。
    点击查看

    在这里插入图片描述

    nox 的作者在去年的 Pycon US 上,做了一场题为《Break the Cycle: Three excellent Python tools to automate repetitive tasks》的分享(B站观看地址:https://b23.tv/av86640235),她介绍了三个任务自动化工具:tox、nox 和 invoke,本文的话题正好就是最后的 invoke。

    1、invoke 可以做什么?
    invoke 是从著名的远程部署工具 Fabric 中分离出来的,它与 paramiko 一起是 Fabric 的两大最核心的基础组件。

    除了作为命令行工具,它专注于“任务执行”(task execution),可以标注和组织任务,并通过 CLI(command-line interface,即命令行界面) 和 shell 命令来执行任务。

    同样是任务自动化工具,invoke 与我们之前介绍过的 tox/nox 在侧重点上有所不同:

    tox/nox 主要是在打包、测试、持续集成等方面的自动化(当然它们能做的还不止于此)
    invoke 则更具普遍性,可以用在任何需要“执行任务”的场景,可以是无相关性的任务组,也可以是有顺序依赖的分步骤的工作流
    invoke 在 Github 上有 2.7K star,十分受欢迎,接下来我们看看它如何使用?
    在这里插入图片描述

    # 文件名:tasks.py
    from invoke import task
    
    @task
    def hello(c):
        print("Hello world!")
    
    @task
    def greet(c, name):
        c.run(f"echo {name}加油!")
    

    在上述代码中,我们定义了两个任务:

    ”hello“任务调用了 Python 内置的 print 函数,会打印一个字符串“Hello world!”
    “greet”任务调用了上下文参数的 run() 方法,可以执行 shell 命令,同时本例中还可以接收一个参数。在 shell 命令中,echo 可理解成打印,所以这也是一个打印任务,会打印出“xxx加油!”(xxx 是我们传的参数)
    以上代码写在 tasks.py 文件中,首先导入装饰器 from invoke import task,@task 装饰器可以不带参数,也可以带参数(参见下一节),被它装饰了的函数就是一个任务。

    上下文参数(即上例的“c”)必须要显式地指明,如果缺少这个参数,执行时会抛出异常:“TypeError: Tasks must have an initial Context argument!”

    然后在 tasks.py 文件的同级目录中,打开命令行窗口,执行命令。如果执行的位置找不到这个任务文件,则会报错:“Can’t find any collection named ‘tasks’!”

    正常情况下,通过执行inv --list 或者inv -l ,可以看到所有任务的列表(按字母表顺序排序):

    >>> inv -l
    Available tasks:
    
      greet
      hello
    

    我们依次执行这两个任务,其中传参时可以默认按位置参数传参,也可以指定关键字传参。结果是:

    >>> inv hello
    Hello world!
    >>> inv greet 武汉
    武汉加油!
    >>> inv greet --name="武汉"
    武汉加油!
    

    缺少传参时,报错:‘greet’ did not receive required positional arguments: ‘name’;多余传参时,报错:No idea what ‘???’ is!

    3、 如何用好 invoke?
    介绍完 invoke 的简单用法,我们知道了它所需的几项要素,也大致知道了它的使用步骤,接下来是它的其它用法。

    3.1 添加帮助信息
    在上例中,“inv -l”只能看到任务名称,缺少必要的辅助信息,为了加强可读性,我们可以这样写:

    @task(help={'name': 'A param for test'})
    def greet(c, name):
        """
        A test for shell command.
        Second line.
        """
        c.run(f"echo {name}加油!")
    

    其中,文档字符串的第一行内容会作为摘录,在“inv -l”的查询结果中展示,而且完整的内容与 @task 的 help 内容,会对应在“inv --help”中展示:

    >>> inv -l
    Available tasks:
    
      greet   A test for shell command.
    >>> inv --help greet
    Usage: inv[oke] [--core-opts] greet [--options] [other tasks here ...]
    
    Docstring:
      A test for shell command.
      Second line.
    
    Options:
      -n STRING, --name=STRING   A param for test
    

    在这里插入图片描述

    @task
    def clean(c):
        c.run("echo clean")
    
    @task
    def message(c):
        c.run("echo message")
    
    @task(pre=[clean], post=[message])
    def build(c):
        c.run("echo build")
    

    clean 与 message 任务作为子任务,可以单独调用,也可以作为 build 任务的前置与后置任务而组合使用:

    >>> inv clean
    clean
    >>> inv message
    message
    >>> inv build
    clean
    build
    message
    

    这两个参数是列表类型,即可设置多个任务。另外,在默认情况下,@task 装饰器的位置参数会被视为前置任务,接着上述代码,我们写一个:

    @task(clean, message)
    def test(c):
        c.run("echo test")
    

    然后执行,会发现两个参数都被视为了前置任务:

    >>> inv test
    clean
    message
    test
    

    3.3 模块的拆分与整合
    如果要管理很多相对独立的大型任务,或者需要多个团队分别维护各自的任务,那么,就有必要对 tasks.py 作拆分与整合。

    例如,现在有多份 tasks.py,彼此是相对完整而独立的任务模块,不方便把所有内容都放在一个文件中,那么,如何有效地把它们整合起来管理呢?

    invoke 提供了这方面的支持。首先,只能保留一份名为“tasks.py”的文件,其次,在该文件中导入其它改名后的任务文件,最后,使用 invoke 的 Collection 类把它们关联起来。

    我们把本文中第一个示例文件改名为 task1.py,并新建一个 tasks.py 文件,内容如下:

    # 文件名:tasks.py
    from invoke import Collection, task
    import task1
    
    @task
    def deploy(c):
        c.run("echo deploy")
    
    namespace = Collection(task1, deploy)
    

    每个 py 文件拥有独立的命名空间,而在此处,我们用 Collection 可以创建出一个新的命名空间,从而实现对所有任务的统一管理。效果如下:

    >>> inv -l
    Available tasks:
    
      deploy
      task1.greet
      task1.hello
    >>> inv deploy
    deploy
    >>> inv task1.hello
    Hello world!
    >>> inv task1.greet 武汉
    武汉加油!
    

    关于不同任务模块的导入、嵌套、混合、起别名等内容,还有不少细节,请查阅官方文档了解。

    3.4 交互式操作
    某些任务可能需要交互式的输入,例如要求输入“y”,按回车键后才会继续执行。如果在任务执行期间需要人工参与,那自动化任务的能力将大打折扣。

    invoke 提供了在程序运行期的监控能力,可以监听stdout 和stderr ,并支持在stdin 中输入必要的信息。

    例如,假设某个任务(excitable-program)在执行时会提示“Are you ready? [y/n]”,只有输入了“y”并按下回车键,才会执行后续的操作。

    那么,在代码中指定 responses 参数的内容,只要监听到匹配信息,程序会自动执行相应的操作:

    responses = {r"Are you ready? \[y/n\] ": "y\n"}
    ctx.run("excitable-program", responses=responses)
    

    在这里插入图片描述
    在这里插入图片描述

    # tasks.py
    from invoke import task
    
    @task
    def unit(c):
        print("Running unit tests!")
    
    @task
    def integration(c):
        print("Running integration tests!")
    

    然后在程序入口文件中引入它:

    # main.py
    from invoke import Collection, Program
    from tester import tasks
    
    program = Program(namespace=Collection.from_module(tasks), version='0.1.0')
    

    最后在打包文件中声明入口函数:

    # setup.py
    setup(
        name='tester',
        version='0.1.0',
        packages=['tester'],
        install_requires=['invoke'],
        entry_points={
            'console_scripts': ['tester = tester.main:program.run']
        }
    )
    

    如此打包发行的库,就是一个功能齐全的命令行工具了:

    $ tester --version
    Tester 0.1.0
    $ tester --help
    Usage: tester [--core-opts] <subcommand> [--subcommand-opts] ...
    
    Core options:
      ... core options here, minus task-related ones ...
    
    Subcommands:
      unit
      integration
    
    $ tester --list
    No idea what '--list' is!
    $ tester unit
    Running unit tests!
    

    上手容易,开箱即用,invoke 不失为一款可以考虑的命令行工具库。更多详细用法,请查阅文档 。

    4、小结

    invoke 作为从 Fabric 项目中分离出来的独立项目,它自身具备一些完整而强大的功能,除了可用于开发命令行工具,它还是著名的任务自动化工具。

    本文介绍了它的基础用法与 5 个方面的中级内容,相信读者们会对它产生一定的了解。invoke 的官方文档十分详尽,限于篇幅,本文不再详细展开,若感兴趣,请自行查阅文档哦。

    展开全文
  • 我们利用requests库和正则表达式来抓取猫眼电影TOP100的...本节中,我们要提取出猫眼电影TOP100的电影名称、时间、评分、图片等信息,提取的站点URL为http://maoyan.com/board/4,提取的结果会以文件形式保存下来...

    我们利用requests库和正则表达式来抓取猫眼电影TOP100的相关内容。requests比urllib使用更加方便,而且目前我们还没有系统学习HTML解析库,所以这里就选用正则表达式来作为解析工具。

    file

    1. 本节目标

    本节中,我们要提取出猫眼电影TOP100的电影名称、时间、评分、图片等信息,提取的站点URL为http://maoyan.com/board/4,提取的结果会以文件形式保存下来。

    1. 准备工作

    在本节开始之前,请确保已经正确安装好了requests库。如果没有安装,可以参考第1章的安装说明。

    1. 抓取分析

    我们需要抓取的目标站点为http://maoyan.com/board/4,打开之后便可以查看到榜单信息,如图3-11所示。

    file

    排名第一的电影是霸王别姬,页面中显示的有效信息有影片名称、主演、上映时间、上映地区、评分、图片等信息。

    将网页滚动到最下方,可以发现有分页的列表,直接点击第2页,观察页面的URL和内容发生了怎样的变化,如图3-12所示。

    file

    可以发现页面的URL变成http://maoyan.com/board/4?offset=10,比之前的URL多了一个参数,那就是offset=10,而目前显示的结果是排行11~20名的电影,初步推断这是一个偏移量的参数。再点击下一页,发现页面的URL变成了http://maoyan.com/board/4?offset=20,参数offset变成了20,而显示的结果是排行21~30的电影。

    由此可以总结出规律,offset代表偏移量值,如果偏移量为n,则显示的电影序号就是n 1到n 10,每页显示10个。所以,如果想获取TOP100电影,只需要分开请求10次,而10次的offset参数分别设置为0、10、20、…90即可,这样获取不同的页面之后,再用正则表达式提取出相关信息,就可以得到TOP100的所有电影信息了。

    1. 抓取首页

    接下来用代码实现这个过程。首先抓取第一页的内容。我们实现了getonepage()方法,并给它传入url参数。然后将抓取的页面结果返回,再通过main()方法调用。初步代码实现如下:

    import requests
     
    def get_one_page(url):
        response = requests.get(url)
        if response.status_code == 200:
            return response.text
        return None
     
    def main():
        url = 'http://maoyan.com/board/4'
        html = get_one_page(url)
        print(html)
     
    main()

    这样运行之后,就可以成功获取首页的源代码了。获取源代码后,就需要解析页面,提取出我们想要的信息。

    1. 正则提取

    接下来,回到网页看一下页面的真实源码。在开发者模式下的Network监听组件中查看源代码,如图3-13所示。

    file

    注意,这里不要在Elements选项卡中直接查看源码,因为那里的源码可能经过JavaScript操作而与原始请求不同,而是需要从Network选项卡部分查看原始请求得到的源码。

    查看其中一个条目的源代码,如图3-14所示。

    file

    可以看到,一部电影信息对应的源代码是一个dd节点,我们用正则表达式来提取这里面的一些电影信息。首先,需要提取它的排名信息。而它的排名信息是在class为board-index的i节点内,这里利用非贪婪匹配来提取i节点内的信息,正则表达式写为:

    <dd>.*?board-index.*?>(.*?)</i>

    随后需要提取电影的图片。可以看到,后面有a节点,其内部有两个img节点。经过检查后发现,第二个img节点的data-src属性是图片的链接。这里提取第二个img节点的data-src属性,正则表达式可以改写如下:

    <dd>.*?board-index.*?>(.*?)</i>.*?data-src="(.*?)"

    再往后,需要提取电影的名称,它在后面的p节点内,class为name。所以,可以用name做一个标志位,然后进一步提取到其内a节点的正文内容,此时正则表达式改写如下:

    <dd>.*?board-index.*?>(.*?)</i>.*?data-src="(.*?)".*?name.*?a.*?>(.*?)</a>

    再提取主演、发布时间、评分等内容时,都是同样的原理。最后,正则表达式写为:

    <dd>.*?board-index.*?>(.*?)</i>.*?data-src="(.*?)".*?name.*?a.*?>(.*?)</a>.*?star.*?>(.*?)</p>.*?releasetime.*?>(.*?)</p>.*?integer.*?>(.*?)</i>.*?fraction.*?>(.*?)</i>.*?</dd>

    这样一个正则表达式可以匹配一个电影的结果,里面匹配了7个信息。接下来,通过调用findall()方法提取出所有的内容。

    接下来,我们再定义解析页面的方法parseonepage(),主要是通过正则表达式来从结果中提取出我们想要的内容,实现代码如下:

    def parse_one_page(html):
        pattern = re.compile(
            '<dd>.*?board-index.*?>(.*?)</i>.*?data-src="(.*?)".*?name.*?a.*?>(.*?)</a>.*?star.*?>(.*?)</p>.*?releasetime.*?>(.*?)</p>.*?integer.*?>(.*?)</i>.*?fraction.*?>(.*?)</i>.*?</dd>',
            re.S)
        items = re.findall(pattern, html)
        print(items)

    这样就可以成功地将一页的10个电影信息都提取出来,这是一个列表形式,输出结果如下:

        
    [('1', 'http://p1.meituan.net/movie/20803f59291c47e1e116c11963ce019e68711.jpg@160w_220h_1e_1c', '霸王别姬', '\n                主演:张国荣,张丰毅,巩俐\n        ', '上映时间:1993-01-01(中国香港)', '9.', '6'), ('2', 'http://p0.meituan.net/movie/__40191813__4767047.jpg@160w_220h_1e_1c', '肖申克的救赎', '\n                主演:蒂姆·罗宾斯,摩根·弗里曼,鲍勃·冈顿\n        ', '上映时间:1994-10-14(美国)', '9.', '5'), ('3', 'http://p0.meituan.net/movie/fc9d78dd2ce84d20e53b6d1ae2eea4fb1515304.jpg@160w_220h_1e_1c', '这个杀手不太冷', '\n                主演:让·雷诺,加里·奥德曼,娜塔莉·波特曼\n        ', '上映时间:1994-09-14(法国)', '9.', '5'), ('4', 'http://p0.meituan.net/movie/23/6009725.jpg@160w_220h_1e_1c', '罗马假日', '\n                主演:格利高利·派克,奥黛丽·赫本,埃迪·艾伯特\n        ', '上映时间:1953-09-02(美国)', '9.', '1'), ('5', 'http://p0.meituan.net/movie/53/1541925.jpg@160w_220h_1e_1c', '阿甘正传', '\n                主演:汤姆·汉克斯,罗宾·怀特,加里·西尼斯\n        ', '上映时间:1994-07-06(美国)', '9.', '4'), ('6', 'http://p0.meituan.net/movie/11/324629.jpg@160w_220h_1e_1c', '泰坦尼克号', '\n                主演:莱昂纳多·迪卡普里奥,凯特·温丝莱特,比利·赞恩\n        ', '上映时间:1998-04-03', '9.', '5'), ('7', 'http://p0.meituan.net/movie/99/678407.jpg@160w_220h_1e_1c', '龙猫', '\n                主演:日高法子,坂本千夏,糸井重里\n        ', '上映时间:1988-04-16(日本)', '9.', '2'), ('8', 'http://p0.meituan.net/movie/92/8212889.jpg@160w_220h_1e_1c', '教父', '\n                主演:马龙·白兰度,阿尔·帕西诺,詹姆斯·凯恩\n        ', '上映时间:1972-03-24(美国)', '9.', '3'), ('9', 'http://p0.meituan.net/movie/62/109878.jpg@160w_220h_1e_1c', '唐伯虎点秋香', '\n                主演:周星驰,巩俐,郑佩佩\n        ', '上映时间:1993-07-01(中国香港)', '9.', '2'), ('10', 'http://p0.meituan.net/movie/9bf7d7b81001a9cf8adbac5a7cf7d766132425.jpg@160w_220h_1e_1c', '千与千寻', '\n                主演:柊瑠美,入野自由,夏木真理\n        ', '上映时间:2001-07-20(日本)', '9.', '3')]

    但这样还不够,数据比较杂乱,我们再将匹配结果处理一下,遍历提取结果并生成字典,此时方法改写如下:

    def parse_one_page(html):
        pattern = re.compile(
            '<dd>.*?board-index.*?>(.*?)</i>.*?data-src="(.*?)".*?name.*?a.*?>(.*?)</a>.*?star.*?>(.*?)</p>.*?releasetime.*?>(.*?)</p>.*?integer.*?>(.*?)</i>.*?fraction.*?>(.*?)</i>.*?</dd>',
            re.S)
        items = re.findall(pattern, html)
        for item in items:
            yield {
                'index': item[0],
                'image': item[1],
                'title': item[2].strip(),
                'actor': item[3].strip()[3:] if len(item[3]) > 3 else '',
                'time': item[4].strip()[5:] if len(item[4]) > 5 else '',
                'score': item[5].strip()   item[6].strip()
            }

    这样就可以成功提取出电影的排名、图片、标题、演员、时间、评分等内容了,并把它赋值为一个个的字典,形成结构化数据。运行结果如下:

    {'image': 'http://p1.meituan.net/movie/20803f59291c47e1e116c11963ce019e68711.jpg@160w220h1e_1c', 'actor': '张国荣,张丰毅,巩俐', 'score': '9.6', 'index': '1', 'title': '霸王别姬', 'time': '1993-01-01(中国香港)'}{'image': 'http://p0.meituan.net/movie/401918134767047.jpg@160w220h1e_1c', 'actor': '蒂姆·罗宾斯,摩根·弗里曼,鲍勃·冈顿', 'score': '9.5', 'index': '2', 'title': '肖申克的救赎', 'time': '1994-10-14(美国)'}{'image': 'http://p0.meituan.net/movie/fc9d78dd2ce84d20e53b6d1ae2eea4fb1515304.jpg@160w220h1e_1c', 'actor': '让·雷诺,加里·奥德曼,娜塔莉·波特曼', 'score': '9.5', 'index': '3', 'title': '这个杀手不太冷', 'time': '1994-09-14(法国)'}{'image': 'http://p0.meituan.net/movie/23/6009725.jpg@160w220h1e_1c', 'actor': '格利高利·派克,奥黛丽·赫本,埃迪·艾伯特', 'score': '9.1', 'index': '4', 'title': '罗马假日', 'time': '1953-09-02(美国)'}{'image': 'http://p0.meituan.net/movie/53/1541925.jpg@160w220h1e_1c', 'actor': '汤姆·汉克斯,罗宾·怀特,加里·西尼斯', 'score': '9.4', 'index': '5', 'title': '阿甘正传', 'time': '1994-07-06(美国)'}{'image': 'http://p0.meituan.net/movie/11/324629.jpg@160w220h1e_1c', 'actor': '莱昂纳多·迪卡普里奥,凯特·温丝莱特,比利·赞恩', 'score': '9.5', 'index': '6', 'title': '泰坦尼克号', 'time': '1998-04-03'}{'image': 'http://p0.meituan.net/movie/99/678407.jpg@160w220h1e_1c', 'actor': '日高法子,坂本千夏,糸井重里', 'score': '9.2', 'index': '7', 'title': '龙猫', 'time': '1988-04-16(日本)'}{'image': 'http://p0.meituan.net/movie/92/8212889.jpg@160w220h1e_1c', 'actor': '马龙·白兰度,阿尔·帕西诺,詹姆斯·凯恩', 'score': '9.3', 'index': '8', 'title': '教父', 'time': '1972-03-24(美国)'}{'image': 'http://p0.meituan.net/movie/62/109878.jpg@160w220h1e_1c', 'actor': '周星驰,巩俐,郑佩佩', 'score': '9.2', 'index': '9', 'title': '唐伯虎点秋香', 'time': '1993-07-01(中国香港)'}{'image': 'http://p0.meituan.net/movie/9bf7d7b81001a9cf8adbac5a7cf7d766132425.jpg@160w220h1e_1c', 'actor': '柊瑠美,入野自由,夏木真理', 'score': '9.3', 'index': '10', 'title': '千与千寻', 'time': '2001-07-20(日本)'}

    到此为止,我们就成功提取了单页的电影信息。

    1. 写入文件

    随后,我们将提取的结果写入文件,这里直接写入到一个文本文件中。这里通过JSON库的dumps()方法实现字典的序列化,并指定ensure_ascii参数为False,这样可以保证输出结果是中文形式而不是Unicode编码。代码如下:

    def write_to_json(content):
        with open('result.txt', 'a') as f:
            print(type(json.dumps(content)))
            f.write(json.dumps(content, ensure_ascii=False,).encode('utf-8'))

    通过调用writetojson()方法即可实现将字典写入到文本文件的过程,此处的content参数就是一部电影的提取结果,是一个字典。

    1. 整合代码

    最后,实现main()方法来调用前面实现的方法,将单页的电影结果写入到文件。相关代码如下:

    def main():
        url = 'http://maoyan.com/board/4'
        html = get_one_page(url)
        for item in parse_one_page(html):
            write_to_json(item)

    到此为止,我们就完成了单页电影的提取,也就是首页的10部电影可以成功提取并保存到文本文件中了。

    1. 分页爬取

    因为我们需要抓取的是TOP100的电影,所以还需要遍历一下,给这个链接传入offset参数,实现其他90部电影的爬取,此时添加如下调用即可:

    if __name__ == '__main__':
        for i in range(10):
            main(offset=i * 10)

    这里还需要将main()方法修改一下,接收一个offset值作为偏移量,然后构造URL进行爬取。实现代码如下:

    def main(offset):
        url = 'http://maoyan.com/board/4?offset='   str(offset)
        html = get_one_page(url)
        for item in parse_one_page(html):
            print(item)
            write_to_file(item)

    到此为止,我们的猫眼电影TOP100的爬虫就全部完成了,再稍微整理一下,完整的代码如下:

    import json
    import requests
    from requests.exceptions import RequestException
    import re
    import time
     
    def get_one_page(url):
        try:
            response = requests.get(url)
            if response.status_code == 200:
                return response.text
            return None
        except RequestException:
            return None
     
    def parse_one_page(html):
        pattern = re.compile('<dd>.*?board-index.*?>(\d )</i>.*?data-src="(.*?)".*?name"><a'
                               '.*?>(.*?)</a>.*?star">(.*?)</p>.*?releasetime">(.*?)</p>'
                               '.*?integer">(.*?)</i>.*?fraction">(.*?)</i>.*?</dd>', re.S)
        items = re.findall(pattern, html)
        for item in items:
            yield {
                'index': item[0],
                'image': item[1],
                'title': item[2],
                'actor': item[3].strip()[3:],
                'time': item[4].strip()[5:],
                'score': item[5]   item[6]
            }
     
    def write_to_file(content):
        with open('result.txt', 'a', encoding='utf-8') as f:
            f.write(json.dumps(content, ensure_ascii=False)   '\n')
     
    def main(offset):
        url = 'http://maoyan.com/board/4?offset='   str(offset)
        html = get_one_page(url)
        for item in parse_one_page(html):
            print(item)
            write_to_file(item)
     
    if __name__ == '__main__':
        for i in range(10):
            main(offset=i * 10)
            time.sleep(1)

    现在猫眼多了反爬虫,如果速度过快,则会无响应,所以这里又增加了一个延时等待。

    1. 运行结果

    最后,我们运行一下代码,输出结果类似如下:

    {'index': '1', 'image': 'http://p1.meituan.net/movie/20803f59291c47e1e116c11963ce019e68711.jpg@160w220h1e_1c', 'title': '霸王别姬', 'actor': '张国荣,张丰毅,巩俐', 'time': '1993-01-01(中国香港)', 'score': '9.6'}{'index': '2', 'image': 'http://p0.meituan.net/movie/401918134767047.jpg@160w220h1e_1c', 'title': '肖申克的救赎', 'actor': '蒂姆·罗宾斯,摩根·弗里曼,鲍勃·冈顿', 'time': '1994-10-14(美国)', 'score': '9.5'}...{'index': '98', 'image': 'http://p0.meituan.net/movie/76/7073389.jpg@160w220h1e_1c', 'title': '东京物语', 'actor': '笠智众,原节子,杉村春子', 'time': '1953-11-03(日本)', 'score': '9.1'}{'index': '99', 'image': 'http://p0.meituan.net/movie/52/3420293.jpg@160w220h1e_1c', 'title': '我爱你', 'actor': '宋在河,李彩恩,吉海延', 'time': '2011-02-17(韩国)', 'score': '9.0'}{'index': '100', 'image': 'http://p1.meituan.net/movie/443351388470779.jpg@160w220h1e_1c', 'title': '迁徙的鸟', 'actor': '雅克·贝汉,菲利普·拉波洛,Philippe Labro', 'time': '2001-12-12(法国)', 'score': '9.1'}

    这里省略了中间的部分输出结果。可以看到,这样就成功地把TOP100的电影信息爬取下来了。

    这时我们再看下文本文件,结果如图3-15所示。

    file

    可以看到,电影信息也已全部保存到了文本文件中了,大功告成!

    展开全文
  • 图片识别速度慢

    2020-12-30 01:33:22
    <div><p>您好,上一个版本我一张图片需要用十分钟才能识别出来,这个版本耗费时间也差不多,请问我需要怎样处理能够使识别速度快一些呢?图片格式如下所示 ...
  • Curl盗取京东图片

    千次阅读 2013-11-25 19:24:46
    前段时间尝试做购物商城系统上时候需要用到图片材料,之前有听老师说过可以通过curl获取网站...但是朋友:我十分钟能下载几千张图片,你右击试试。还有人可能会说,这个简单,找个小偷采集器不就行了。是的,小偷采集器
  • 隔了没太长时间,又来...下面的图片就是小球在刚启动、运动十分钟和运动了三个钟头时的规整程度,对于一个程序来说,是在是差的天差地远,不知哪位大侠可以解释一下,最后面,是这个小球的完整的代码,有详细的注释...
  • 上一篇《十分钟玩转Flex布局》发布之后,我分享给了大一大二学弟之后,他们给我聊天说确实学到东西了,我听了还是很开心的,初心达到了。 ε=(´ο`*)))唉,我已经好久没更新啦。 不是我偷懒在家睡觉,而是...
  • 虽然 Word 看起来很简单,但是想要真正精通却需要花费时间学习。很多人都认为自己掌握了 Word,所以平时对 Word 的学习并不上心,等到在写论文或是工作中写结案报告时,常常会遇到 Word 插入图片后只显示一半等多种...
  • 经历了二十分钟的时间。 规定了十分钟。如果十分钟只能没有做好视频,那就算了。 闹钟计时十分钟 第十分钟,刚做好一个简陋的PPT,图片也没有找。也没有修正。也没有演练。 第十五分钟,心中盘算了课件。 第十...
  • 网页学堂

    2007-01-07 18:29:43
    发布时间 分类 图片幻灯片切换特效代码 邻居 2006-10-26 12:58 网页学堂 文字图片不间断滚动代码 邻居 2006-10-26 11:00 网页学堂 十分钟学会制LOGO 邻居 2006-08-31 10:13 网页学堂 ...
  • 勾起了儿时的记忆

    2009-10-28 11:30:00
     虽然课间只有短短的十分钟时间,但只要铃声一响,在老师宣传下课之前,便已经做好了准备,然后飞一般的去抢案子(不多的几张水泥乒乓球台)。因为资源有限,一人可能只有三个球的机会,“三光”嘛,直接就下台了,...
  • 2021-01-23

    2021-01-23 16:01:41
    如何十分钟搭建一个网课搜题小程序(傻瓜教程) 词语解释 网课是一种新兴的学习方式,是为学习者提供的以互联网为平台、内容包含视频、图片、文字互动等多种形式的系列学习教程。 网课是服务机构提供的在线学习...
  • 虽然网上关于这个的教程不少,但是写在这里,就当作自己的学习记录吧。 开发工具:Python 3.7 ...分析一下链接的构成规律可知,从D531106/1d/550/后面开始,都是对应的时间日期,使用的是格林威治标准...
  • 文章顶部的图片截取自一组点云数据的某个视角,该数据仅通过一次设备架设进行采集,采集现场位于兰州市区,总体时间小于十分钟,但信息量较为丰富。先来欣赏如下视频: 通过模型化点云数据实现实景三维漫游_腾讯...
  • 190829—DAY11

    2019-09-24 10:15:06
    妈呀一天时间都在配置各种,之前的出了问题我真崩溃;2.编写了登陆和注册的页面。都分是注册用户user还是跑腿admin。 明天的计划:1.准备答辩,但是我的页面还是不好跑起来而且其实我写的也不够多哈哈...
  • 文章目录前言做一道简单的折线图题,自己敲出来代码,那今天的学习就够了做出一个折线图,显示10点到12点的温度,横坐标显示时间,十分钟一个刻度,纵坐标显示温度,要有标题,要有说明,而且要对比今天温度和昨天温度的差异...
  • 本文的文字及图片来源于网络,仅供学习、交流使用,不具有任何商业用途,版权归原...这个解读来源于一个读者的反馈,于是花了几分钟看了下这部分源码,打算用十分钟时间写一下,预计阅读需要 5 分钟。 自定义异常页面 D
  • springboot2+hikari,配置连接池大小是15,连接空闲时间1分钟,池中连接的最长生命周期是10分钟。 ![图片说明](https://img-ask.csdn.net/upload/201908/07/1565145919_379990.png) 1:当项目启动,会创建15...
  • 费尽千辛万苦导入了项目,第二次再导入发现其实只要十分钟不到。心痛我浪费的时间。这篇只是记录帖,方便我以后参考。不要把它当教程哦,认真你就输了!仅供参考仅供参考仅供参考 导入项目 import project 我选的...
  • (QQ2009显IP)V1.7 绿色版

    2009-05-28 03:51:34
    3. 智能的记忆分析功能, 使用时间越长, 显IP能力越强, 慢慢体验吧。 4. 偶尔上线十分钟, 可大大增强IP获取率。 5. 继承了前辈软件老板键的功能, 瞬间隐藏QQ, 再不怕老板突袭检查啦。 6. 继承了前辈软件字体切换的...
  • 如果当前正在浏览图片,那么可以看到图片名、创建时间、创建日期、图片大小、文件路径等信息,其中Title、Time、Date这3项内容可以手工重置。 5. 创建播放列表 无论是播放音乐或是浏览图片,都可以非常方便的...
  • **需求**:用户session十分钟内无请求就会自动删除。无论用户发送什么请求都要被拦截器拦截,并跳转到登录页面。 **问题**:拦截器能拦截到用户的访问行为,可是,跳转语句五法执行。 **我的代码**: ...
  • // 十分钟的游戏时间 while (t==10) { if (count ) {// 限制小球数量为400 Ball ball = new Ball();// 创建新的小球对象 Point icon= e.getPoint();// 获取鼠标当前位置 ball.setLocation(icon);// ...
  • 一个牛人提供的GIS源码(很好)

    千次下载 热门讨论 2010-08-25 22:49:21
    我倒还是很愿意花上十分钟时间来阅读您的Mail并将我知道的所答复给您。当然如果有邮件不回复,那么基本上我是不懂而无法回答或者觉得没有任何答复的价值,请多多的包涵。 以前写的部分日志以及提供的下载文件整理...
  • 响应式图片101.epub 响应式设计快速指南.epub 图像处理算法.epub 图解 Flux.epub 图解 Monad.epub 基于Qt的词典开发.epub 大型網站架構學習筆記.epub 大数据开源框架.epub 大数据管理系统LAXCUS.epub 奇舞周刊.epub ...

空空如也

空空如也

1 2
收藏数 31
精华内容 12
关键字:

十分钟时间图片