Как ждать, пока не завершится вся серия вложенных CompletableFutures? - PullRequest
0 голосов
/ 11 апреля 2020

У меня есть приложение дополненной реальности, где ARObject - это POJO:

  class ARObject {
    CompletableFuture<Texture> texture;
    CompletableFuture<Material> material;
    ModelRenderable renderable;

    void setTexture(CompletableFuture<Texture> texture) {
      this.texture = texture;
    }

    CompletableFuture<Texture> getTexture() {
      return texture;
    }

    void setMaterial(CompletableFuture<Material> material) {
      this.material = material;
    }

    CompletableFuture<Material> getMaterial() {
      return material;
    }
  }

Сцена составлена ​​в режиме реального времени. Во время этой процедуры необходимо построить Texture объекты, а затем Material объекты на основе Texture объектов. Как только Material готов, тогда ShapeFactory может использоваться для порождения реальных объектов AR (как форма Renderable). Это означает, что логика сборки c содержит два CompletableFuture, вложенных друг в друга для каждого объекта AR:

for (ARObject arObject : arObjects) {
  Texture.Builder textureBuilder = Texture.builder();
  textureBuilder.setSource(context, arObject.resourceId);
  CompletableFuture<Texture> texturePromise = textureBuilder.build();  // Future #1
  arObject.setTexture(texturePromise);
  texturePromise.thenAccept(texture -> {
    CompletableFuture<Material> materialPromise =
            MaterialFactory.makeOpaqueWithTexture(context, texture);  // Future #2
    arObject.setMaterial(materialPromise);
  });
}

Один из способов завершить построение сцены - дождаться, пока все CompletableFuture Сделано, и тогда может наступить шаг ShapeFactory.

Я пытался использовать .get() на Future s, но это не просто полностью уничтожило бы параллелизм, предлагаемый вызовами asyn c, но и заблокировало приложение, потому что я предполагаю, что вызвал ожидание в потоке пользовательского интерфейса.

Arrays.stream(arObjectList).forEach(a -> {
  try {
    a.getTexture().get();
  } catch (ExecutionException | InterruptedException e) {
    Log.e(TAG, "Texture CompletableFuture waiting problem " + e.toString());
  }
});
Arrays.stream(arObjectList).forEach(a -> {
  try {
    a.getMaterial().get();
  } catch (ExecutionException | InterruptedException e) {
    Log.e(TAG, "Material CompletableFuture waiting problem " + e.toString());
  }
});

Я разбил процедуру сборки до нескольких функций, которые вызывают друг друга в цепочке вызовов. Цепочка следующая:

  1. populateScene
  2. afterTexturesLoaded
  3. afterTexturesSet
  4. waitForMaterials
  5. afterMaterialsLoaded
  private void afterMaterialsLoaded() {
    // Step 3: composing scene objects
    // Get a handler that can be used to post to the main thread
    Handler mainHandler = new Handler(context.getMainLooper());
    for (ARObject arObject : arObjectList) {
      try {
        Material textureMaterial = arObject.getMaterial().get();

        RunnableShapeBuilder shapeBuilder = new RunnableShapeBuilder(arObject, this, textureMaterial);
        mainHandler.post(shapeBuilder);
      }
      catch (ExecutionException | InterruptedException e) {
        Log.e(TAG, "Scene populating exception " + e.toString());
      }
    }
  }

  private Long waitForMaterials() {
    while (!Stream.of(arObjectList).allMatch(arObject -> arObject.getMaterial() != null)) {
      try {
        Thread.sleep(100);
      } catch (InterruptedException e) {
      }
    }
    return 0L;
  }

  private void afterTexturesSet() {
    boolean materialsDone = Stream.of(arObjectList).allMatch(arObject -> arObject.getMaterial() != null && arObject.getMaterial().isDone());
    // If any of the materials are not loaded, then recurse until all are loaded.
    if (!materialsDone) {
      CompletableFuture<Texture>[] materialPromises =
        Stream.of(arObjectList).map(ARObject::getMaterial).toArray(CompletableFuture[]::new);

      CompletableFuture.allOf(materialPromises)
        .thenAccept((Void aVoid) -> afterMaterialsLoaded())
        .exceptionally(
          throwable -> {
            Log.e(TAG, "Exception building scene", throwable);
            return null;
          });
    } else {
      afterMaterialsLoaded();
    }
  }

  private void afterTexturesLoaded() {
    // Step 2: material loading
    CompletableFuture materialsSetPromise = CompletableFuture.supplyAsync(this::waitForMaterials);
    CompletableFuture.allOf(materialsSetPromise)
      .thenAccept((Void aVoid) -> afterTexturesSet())
      .exceptionally(
        throwable -> {
          Log.e(TAG, "Exception building scene", throwable);
          return null;
        });
  }

  /**
   * Called when the AugmentedImage is detected and should be rendered. A Sceneform node tree is
   * created based on an Anchor created from the image.
   */
  @SuppressWarnings({"AndroidApiChecker", "FutureReturnValueIgnored"})
  void populateScene() {
    // Step 1: texture loading
    boolean texturesDone = Stream.of(arObjectList).allMatch(arObject -> arObject.getTexture() != null && arObject.getTexture().isDone());
    // If any of the textures are not loaded, then recurse until all are loaded.
    if (!texturesDone) {
      CompletableFuture<Texture>[] texturePromises =
        Stream.of(arObjectList).map(ARObject::getTexture).toArray(CompletableFuture[]::new);

      CompletableFuture.allOf(texturePromises)
        .thenAccept((Void aVoid) -> afterTexturesLoaded())
        .exceptionally(
          throwable -> {
            Log.e(TAG, "Exception building scene", throwable);
            return null;
          });
    } else {
      afterTexturesLoaded();
    }
  }

Есть несколько проблем с этим. Во-первых, это все еще не соответствует асинхронной природе. В идеальной ситуации соответствующая пара текстуры материала будет независимо загружаться и генерироваться из других пар. В этой последней версии есть много точек встречи в потоке выполнения, что не соответствует идеальному независимому сценарию. Второе: я даже не мог избежать шага waitForMaterials, где у меня некрасиво Thread.sleep(). В-третьих: код по-прежнему не работает в целом, потому что на последнем этапе, когда нужно наконец построить фигуры из загруженных текстур и материалов, у меня появляется ошибка java.lang.IllegalStateException: Must be called from the UI thread.. Из-за этого я вставил еще один поворот: RunnableShapeBuilder. В этом нет исключений, но на сцене ничего не отображается, а код становится еще сложнее.

  class RunnableShapeBuilder implements Runnable {
    ARObject arObject;
    AnchorNode parentNode;
    Material textureMaterial;

    RunnableShapeBuilder(ARObject arObject, AnchorNode parentNode, Material textureMaterial) {
      this.arObject = arObject;
      this.parentNode = parentNode;
      this.textureMaterial = textureMaterial;
    }

    @Override
    public void run() {
      arObject.renderable = ShapeFactory.makeCube(
        new Vector3(0.5f, 1, 0.01f),
        new Vector3(0.0f, 0.0f, 0.0f),
        textureMaterial
      );
      ...
    }
  }
...