1 /* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.layoutlib.bridge; 18 19 import com.android.ide.common.rendering.api.Capability; 20 import com.android.ide.common.rendering.api.DrawableParams; 21 import com.android.ide.common.rendering.api.Features; 22 import com.android.ide.common.rendering.api.LayoutLog; 23 import com.android.ide.common.rendering.api.RenderSession; 24 import com.android.ide.common.rendering.api.ResourceNamespace; 25 import com.android.ide.common.rendering.api.ResourceReference; 26 import com.android.ide.common.rendering.api.Result; 27 import com.android.ide.common.rendering.api.Result.Status; 28 import com.android.ide.common.rendering.api.SessionParams; 29 import com.android.layoutlib.bridge.android.RenderParamsFlags; 30 import com.android.layoutlib.bridge.impl.RenderDrawable; 31 import com.android.layoutlib.bridge.impl.RenderSessionImpl; 32 import com.android.layoutlib.bridge.util.DynamicIdMap; 33 import com.android.ninepatch.NinePatchChunk; 34 import com.android.resources.ResourceType; 35 import com.android.tools.layoutlib.annotations.Nullable; 36 import com.android.tools.layoutlib.create.MethodAdapter; 37 import com.android.tools.layoutlib.create.OverrideMethod; 38 import com.android.util.Pair; 39 40 import android.content.res.BridgeAssetManager; 41 import android.graphics.Bitmap; 42 import android.graphics.FontFamily_Delegate; 43 import android.graphics.Typeface; 44 import android.graphics.Typeface_Builder_Delegate; 45 import android.graphics.Typeface_Delegate; 46 import android.icu.util.ULocale; 47 import android.os.Looper; 48 import android.os.Looper_Accessor; 49 import android.view.View; 50 import android.view.ViewGroup; 51 import android.view.ViewParent; 52 53 import java.io.File; 54 import java.lang.ref.SoftReference; 55 import java.lang.reflect.Field; 56 import java.lang.reflect.Modifier; 57 import java.util.Arrays; 58 import java.util.EnumMap; 59 import java.util.EnumSet; 60 import java.util.HashMap; 61 import java.util.Map; 62 import java.util.WeakHashMap; 63 import java.util.concurrent.locks.ReentrantLock; 64 65 import libcore.io.MemoryMappedFile_Delegate; 66 67 import static android.graphics.Typeface.DEFAULT_FAMILY; 68 import static android.graphics.Typeface.RESOLVE_BY_FONT_TABLE; 69 import static com.android.ide.common.rendering.api.Result.Status.ERROR_UNKNOWN; 70 71 /** 72 * Main entry point of the LayoutLib Bridge. 73 * <p/>To use this bridge, simply instantiate an object of type {@link Bridge} and call 74 * {@link #createSession(SessionParams)} 75 */ 76 public final class Bridge extends com.android.ide.common.rendering.api.Bridge { 77 78 private static final String ICU_LOCALE_DIRECTION_RTL = "right-to-left"; 79 80 public static class StaticMethodNotImplementedException extends RuntimeException { 81 private static final long serialVersionUID = 1L; 82 StaticMethodNotImplementedException(String msg)83 public StaticMethodNotImplementedException(String msg) { 84 super(msg); 85 } 86 } 87 88 /** 89 * Lock to ensure only one rendering/inflating happens at a time. 90 * This is due to some singleton in the Android framework. 91 */ 92 private final static ReentrantLock sLock = new ReentrantLock(); 93 94 /** 95 * Maps from id to resource type/name. This is for com.android.internal.R 96 */ 97 @SuppressWarnings("deprecation") 98 private final static Map<Integer, Pair<ResourceType, String>> sRMap = new HashMap<>(); 99 100 /** 101 * Reverse map compared to sRMap, resource type -> (resource name -> id). 102 * This is for com.android.internal.R. 103 */ 104 private final static Map<ResourceType, Map<String, Integer>> sRevRMap = new EnumMap<>(ResourceType.class); 105 106 // framework resources are defined as 0x01XX#### where XX is the resource type (layout, 107 // drawable, etc...). Using FF as the type allows for 255 resource types before we get a 108 // collision which should be fine. 109 private final static int DYNAMIC_ID_SEED_START = 0x01ff0000; 110 private final static DynamicIdMap sDynamicIds = new DynamicIdMap(DYNAMIC_ID_SEED_START); 111 112 private final static Map<Object, Map<String, SoftReference<Bitmap>>> sProjectBitmapCache = 113 new WeakHashMap<>(); 114 private final static Map<Object, Map<String, SoftReference<NinePatchChunk>>> sProject9PatchCache = 115 new WeakHashMap<>(); 116 117 private final static Map<String, SoftReference<Bitmap>> sFrameworkBitmapCache = new HashMap<>(); 118 private final static Map<String, SoftReference<NinePatchChunk>> sFramework9PatchCache = 119 new HashMap<>(); 120 121 private static Map<String, Map<String, Integer>> sEnumValueMap; 122 private static Map<String, String> sPlatformProperties; 123 124 /** 125 * A default log than prints to stdout/stderr. 126 */ 127 private final static LayoutLog sDefaultLog = new LayoutLog() { 128 @Override 129 public void error(String tag, String message, Object data) { 130 System.err.println(message); 131 } 132 133 @Override 134 public void error(String tag, String message, Throwable throwable, Object data) { 135 System.err.println(message); 136 } 137 138 @Override 139 public void warning(String tag, String message, Object data) { 140 System.out.println(message); 141 } 142 }; 143 144 /** 145 * Current log. 146 */ 147 private static LayoutLog sCurrentLog = sDefaultLog; 148 149 public static boolean sIsTypefaceInitialized; 150 151 private static final int LAST_SUPPORTED_FEATURE = Features.THEME_PREVIEW_NAVIGATION_BAR; 152 153 @Override getApiLevel()154 public int getApiLevel() { 155 return com.android.ide.common.rendering.api.Bridge.API_CURRENT; 156 } 157 158 @SuppressWarnings("deprecation") 159 @Override 160 @Deprecated getCapabilities()161 public EnumSet<Capability> getCapabilities() { 162 // The Capability class is deprecated and frozen. All Capabilities enumerated there are 163 // supported by this version of LayoutLibrary. So, it's safe to use EnumSet.allOf() 164 return EnumSet.allOf(Capability.class); 165 } 166 167 @Override supports(int feature)168 public boolean supports(int feature) { 169 return feature <= LAST_SUPPORTED_FEATURE; 170 } 171 172 @Override init(Map<String,String> platformProperties, File fontLocation, String icuDataPath, Map<String, Map<String, Integer>> enumValueMap, LayoutLog log)173 public boolean init(Map<String,String> platformProperties, 174 File fontLocation, 175 String icuDataPath, 176 Map<String, Map<String, Integer>> enumValueMap, 177 LayoutLog log) { 178 sPlatformProperties = platformProperties; 179 sEnumValueMap = enumValueMap; 180 181 BridgeAssetManager.initSystem(); 182 183 // When DEBUG_LAYOUT is set and is not 0 or false, setup a default listener 184 // on static (native) methods which prints the signature on the console and 185 // throws an exception. 186 // This is useful when testing the rendering in ADT to identify static native 187 // methods that are ignored -- layoutlib_create makes them returns 0/false/null 188 // which is generally OK yet might be a problem, so this is how you'd find out. 189 // 190 // Currently layoutlib_create only overrides static native method. 191 // Static non-natives are not overridden and thus do not get here. 192 final String debug = System.getenv("DEBUG_LAYOUT"); 193 if (debug != null && !debug.equals("0") && !debug.equals("false")) { 194 195 OverrideMethod.setDefaultListener(new MethodAdapter() { 196 @Override 197 public void onInvokeV(String signature, boolean isNative, Object caller) { 198 sDefaultLog.error(null, "Missing Stub: " + signature + 199 (isNative ? " (native)" : ""), null /*data*/); 200 201 if (debug.equalsIgnoreCase("throw")) { 202 // Throwing this exception doesn't seem that useful. It breaks 203 // the layout editor yet doesn't display anything meaningful to the 204 // user. Having the error in the console is just as useful. We'll 205 // throw it only if the environment variable is "throw" or "THROW". 206 throw new StaticMethodNotImplementedException(signature); 207 } 208 } 209 }); 210 } 211 212 // load the fonts. 213 FontFamily_Delegate.setFontLocation(fontLocation.getAbsolutePath()); 214 MemoryMappedFile_Delegate.setDataDir(fontLocation.getAbsoluteFile().getParentFile()); 215 216 // now parse com.android.internal.R (and only this one as android.R is a subset of 217 // the internal version), and put the content in the maps. 218 try { 219 Class<?> r = com.android.internal.R.class; 220 // Parse the styleable class first, since it may contribute to attr values. 221 parseStyleable(); 222 223 for (Class<?> inner : r.getDeclaredClasses()) { 224 if (inner == com.android.internal.R.styleable.class) { 225 // Already handled the styleable case. Not skipping attr, as there may be attrs 226 // that are not referenced from styleables. 227 continue; 228 } 229 String resTypeName = inner.getSimpleName(); 230 ResourceType resType = ResourceType.getEnum(resTypeName); 231 if (resType != null) { 232 Map<String, Integer> fullMap = null; 233 switch (resType) { 234 case ATTR: 235 fullMap = sRevRMap.get(ResourceType.ATTR); 236 break; 237 case STRING: 238 case STYLE: 239 // Slightly less than thousand entries in each. 240 fullMap = new HashMap<>(1280); 241 // no break. 242 default: 243 if (fullMap == null) { 244 fullMap = new HashMap<>(); 245 } 246 sRevRMap.put(resType, fullMap); 247 } 248 249 for (Field f : inner.getDeclaredFields()) { 250 // only process static final fields. Since the final attribute may have 251 // been altered by layoutlib_create, we only check static 252 if (!isValidRField(f)) { 253 continue; 254 } 255 Class<?> type = f.getType(); 256 if (!type.isArray()) { 257 Integer value = (Integer) f.get(null); 258 //noinspection deprecation 259 sRMap.put(value, Pair.of(resType, f.getName())); 260 fullMap.put(f.getName(), value); 261 } 262 } 263 } 264 } 265 } catch (Exception throwable) { 266 if (log != null) { 267 log.error(LayoutLog.TAG_BROKEN, 268 "Failed to load com.android.internal.R from the layout library jar", 269 throwable, null); 270 } 271 return false; 272 } 273 274 return true; 275 } 276 277 /** 278 * Tests if the field is pubic, static and one of int or int[]. 279 */ isValidRField(Field field)280 private static boolean isValidRField(Field field) { 281 int modifiers = field.getModifiers(); 282 boolean isAcceptable = Modifier.isPublic(modifiers) && Modifier.isStatic(modifiers); 283 Class<?> type = field.getType(); 284 return isAcceptable && type == int.class || 285 (type.isArray() && type.getComponentType() == int.class); 286 287 } 288 parseStyleable()289 private static void parseStyleable() throws Exception { 290 // R.attr doesn't contain all the needed values. There are too many resources in the 291 // framework for all to be in the R class. Only the ones specified manually in 292 // res/values/symbols.xml are put in R class. Since, we need to create a map of all attr 293 // values, we try and find them from the styleables. 294 295 // There were 1500 elements in this map at M timeframe. 296 Map<String, Integer> revRAttrMap = new HashMap<>(2048); 297 sRevRMap.put(ResourceType.ATTR, revRAttrMap); 298 // There were 2000 elements in this map at M timeframe. 299 Map<String, Integer> revRStyleableMap = new HashMap<>(3072); 300 sRevRMap.put(ResourceType.STYLEABLE, revRStyleableMap); 301 Class<?> c = com.android.internal.R.styleable.class; 302 Field[] fields = c.getDeclaredFields(); 303 // Sort the fields to bring all arrays to the beginning, so that indices into the array are 304 // able to refer back to the arrays (i.e. no forward references). 305 Arrays.sort(fields, (o1, o2) -> { 306 if (o1 == o2) { 307 return 0; 308 } 309 Class<?> t1 = o1.getType(); 310 Class<?> t2 = o2.getType(); 311 if (t1.isArray() && !t2.isArray()) { 312 return -1; 313 } else if (t2.isArray() && !t1.isArray()) { 314 return 1; 315 } 316 return o1.getName().compareTo(o2.getName()); 317 }); 318 Map<String, int[]> styleables = new HashMap<>(); 319 for (Field field : fields) { 320 if (!isValidRField(field)) { 321 // Only consider public static fields that are int or int[]. 322 // Don't check the final flag as it may have been modified by layoutlib_create. 323 continue; 324 } 325 String name = field.getName(); 326 if (field.getType().isArray()) { 327 int[] styleableValue = (int[]) field.get(null); 328 styleables.put(name, styleableValue); 329 continue; 330 } 331 // Not an array. 332 String arrayName = name; 333 int[] arrayValue = null; 334 int index; 335 while ((index = arrayName.lastIndexOf('_')) >= 0) { 336 // Find the name of the corresponding styleable. 337 // Search in reverse order so that attrs like LinearLayout_Layout_layout_gravity 338 // are mapped to LinearLayout_Layout and not to LinearLayout. 339 arrayName = arrayName.substring(0, index); 340 arrayValue = styleables.get(arrayName); 341 if (arrayValue != null) { 342 break; 343 } 344 } 345 index = (Integer) field.get(null); 346 if (arrayValue != null) { 347 String attrName = name.substring(arrayName.length() + 1); 348 int attrValue = arrayValue[index]; 349 //noinspection deprecation 350 sRMap.put(attrValue, Pair.of(ResourceType.ATTR, attrName)); 351 revRAttrMap.put(attrName, attrValue); 352 } 353 //noinspection deprecation 354 sRMap.put(index, Pair.of(ResourceType.STYLEABLE, name)); 355 revRStyleableMap.put(name, index); 356 } 357 } 358 359 @Override dispose()360 public boolean dispose() { 361 BridgeAssetManager.clearSystem(); 362 363 // dispose of the default typeface. 364 if (sIsTypefaceInitialized) { 365 Typeface.sDynamicTypefaceCache.evictAll(); 366 } 367 sProject9PatchCache.clear(); 368 sProjectBitmapCache.clear(); 369 370 return true; 371 } 372 373 /** 374 * Starts a layout session by inflating and rendering it. The method returns a 375 * {@link RenderSession} on which further actions can be taken. 376 * <p/> 377 * If {@link SessionParams} includes the {@link RenderParamsFlags#FLAG_DO_NOT_RENDER_ON_CREATE}, 378 * this method will only inflate the layout but will NOT render it. 379 * @param params the {@link SessionParams} object with all the information necessary to create 380 * the scene. 381 * @return a new {@link RenderSession} object that contains the result of the layout. 382 * @since 5 383 */ 384 @Override createSession(SessionParams params)385 public RenderSession createSession(SessionParams params) { 386 try { 387 Result lastResult; 388 RenderSessionImpl scene = new RenderSessionImpl(params); 389 try { 390 prepareThread(); 391 lastResult = scene.init(params.getTimeout()); 392 if (lastResult.isSuccess()) { 393 lastResult = scene.inflate(); 394 395 boolean doNotRenderOnCreate = Boolean.TRUE.equals( 396 params.getFlag(RenderParamsFlags.FLAG_DO_NOT_RENDER_ON_CREATE)); 397 if (lastResult.isSuccess() && !doNotRenderOnCreate) { 398 lastResult = scene.render(true /*freshRender*/); 399 } 400 } 401 } finally { 402 scene.release(); 403 cleanupThread(); 404 } 405 406 return new BridgeRenderSession(scene, lastResult); 407 } catch (Throwable t) { 408 // get the real cause of the exception. 409 Throwable t2 = t; 410 while (t2.getCause() != null) { 411 t2 = t2.getCause(); 412 } 413 return new BridgeRenderSession(null, 414 ERROR_UNKNOWN.createResult(t2.getMessage(), t)); 415 } 416 } 417 418 @Override renderDrawable(DrawableParams params)419 public Result renderDrawable(DrawableParams params) { 420 try { 421 Result lastResult; 422 RenderDrawable action = new RenderDrawable(params); 423 try { 424 prepareThread(); 425 lastResult = action.init(params.getTimeout()); 426 if (lastResult.isSuccess()) { 427 lastResult = action.render(); 428 } 429 } finally { 430 action.release(); 431 cleanupThread(); 432 } 433 434 return lastResult; 435 } catch (Throwable t) { 436 // get the real cause of the exception. 437 Throwable t2 = t; 438 while (t2.getCause() != null) { 439 t2 = t.getCause(); 440 } 441 return ERROR_UNKNOWN.createResult(t2.getMessage(), t); 442 } 443 } 444 445 @Override clearCaches(Object projectKey)446 public void clearCaches(Object projectKey) { 447 if (projectKey != null) { 448 sProjectBitmapCache.remove(projectKey); 449 sProject9PatchCache.remove(projectKey); 450 } 451 } 452 453 @Override getViewParent(Object viewObject)454 public Result getViewParent(Object viewObject) { 455 if (viewObject instanceof View) { 456 return Status.SUCCESS.createResult(((View)viewObject).getParent()); 457 } 458 459 throw new IllegalArgumentException("viewObject is not a View"); 460 } 461 462 @Override getViewIndex(Object viewObject)463 public Result getViewIndex(Object viewObject) { 464 if (viewObject instanceof View) { 465 View view = (View) viewObject; 466 ViewParent parentView = view.getParent(); 467 468 if (parentView instanceof ViewGroup) { 469 Status.SUCCESS.createResult(((ViewGroup) parentView).indexOfChild(view)); 470 } 471 472 return Status.SUCCESS.createResult(); 473 } 474 475 throw new IllegalArgumentException("viewObject is not a View"); 476 } 477 478 @Override isRtl(String locale)479 public boolean isRtl(String locale) { 480 return isLocaleRtl(locale); 481 } 482 isLocaleRtl(String locale)483 public static boolean isLocaleRtl(String locale) { 484 if (locale == null) { 485 locale = ""; 486 } 487 ULocale uLocale = new ULocale(locale); 488 return uLocale.getCharacterOrientation().equals(ICU_LOCALE_DIRECTION_RTL); 489 } 490 491 /** 492 * Returns the lock for the bridge 493 */ getLock()494 public static ReentrantLock getLock() { 495 return sLock; 496 } 497 498 /** 499 * Prepares the current thread for rendering. 500 * 501 * Note that while this can be called several time, the first call to {@link #cleanupThread()} 502 * will do the clean-up, and make the thread unable to do further scene actions. 503 */ prepareThread()504 public synchronized static void prepareThread() { 505 // We need to make sure the Looper has been initialized for this thread. 506 // This is required for View that creates Handler objects. 507 if (Looper.myLooper() == null) { 508 synchronized (Looper.class) { 509 // Check if the main looper has been prepared already. 510 if (Looper.getMainLooper() == null) { 511 Looper.prepareMainLooper(); 512 } 513 } 514 } 515 } 516 517 /** 518 * Cleans up thread-specific data. After this, the thread cannot be used for scene actions. 519 * <p> 520 * Note that it doesn't matter how many times {@link #prepareThread()} was called, a single 521 * call to this will prevent the thread from doing further scene actions 522 */ cleanupThread()523 public synchronized static void cleanupThread() { 524 // clean up the looper 525 Looper_Accessor.cleanupThread(); 526 } 527 getLog()528 public static LayoutLog getLog() { 529 return sCurrentLog; 530 } 531 setLog(LayoutLog log)532 public static void setLog(LayoutLog log) { 533 // check only the thread currently owning the lock can do this. 534 if (!sLock.isHeldByCurrentThread()) { 535 throw new IllegalStateException("scene must be acquired first. see #acquire(long)"); 536 } 537 538 if (log != null) { 539 sCurrentLog = log; 540 } else { 541 sCurrentLog = sDefaultLog; 542 } 543 } 544 545 /** 546 * Returns details of a framework resource from its integer value. 547 * 548 * <p>TODO(namespaces): remove this and just do all id resolution through the callback. 549 */ 550 @Nullable resolveResourceId(int value)551 public static ResourceReference resolveResourceId(int value) { 552 Pair<ResourceType, String> pair = sRMap.get(value); 553 if (pair == null) { 554 pair = sDynamicIds.resolveId(value); 555 } 556 557 if (pair != null) { 558 return new ResourceReference(ResourceNamespace.ANDROID, pair.getFirst(), pair.getSecond()); 559 } 560 return null; 561 } 562 563 /** 564 * Returns the integer id of a framework resource, from a given resource type and resource name. 565 * <p/> 566 * If no resource is found, it creates a dynamic id for the resource. 567 * 568 * @param type the type of the resource 569 * @param name the name of the resource. 570 * @return an int containing the resource id. 571 */ getResourceId(ResourceType type, String name)572 public static int getResourceId(ResourceType type, String name) { 573 Map<String, Integer> map = sRevRMap.get(type); 574 Integer value = map == null ? null : map.get(name); 575 return value == null ? sDynamicIds.getId(type, name) : value; 576 } 577 578 /** 579 * Returns the list of possible enums for a given attribute name. 580 */ 581 @Nullable getEnumValues(String attributeName)582 public static Map<String, Integer> getEnumValues(String attributeName) { 583 if (sEnumValueMap != null) { 584 return sEnumValueMap.get(attributeName); 585 } 586 587 return null; 588 } 589 590 /** 591 * Returns the platform build properties. 592 */ getPlatformProperties()593 public static Map<String, String> getPlatformProperties() { 594 return sPlatformProperties; 595 } 596 597 /** 598 * Returns the bitmap for a specific path, from a specific project cache, or from the 599 * framework cache. 600 * @param value the path of the bitmap 601 * @param projectKey the key of the project, or null to query the framework cache. 602 * @return the cached Bitmap or null if not found. 603 */ getCachedBitmap(String value, Object projectKey)604 public static Bitmap getCachedBitmap(String value, Object projectKey) { 605 if (projectKey != null) { 606 Map<String, SoftReference<Bitmap>> map = sProjectBitmapCache.get(projectKey); 607 if (map != null) { 608 SoftReference<Bitmap> ref = map.get(value); 609 if (ref != null) { 610 return ref.get(); 611 } 612 } 613 } else { 614 SoftReference<Bitmap> ref = sFrameworkBitmapCache.get(value); 615 if (ref != null) { 616 return ref.get(); 617 } 618 } 619 620 return null; 621 } 622 623 /** 624 * Sets a bitmap in a project cache or in the framework cache. 625 * @param value the path of the bitmap 626 * @param bmp the Bitmap object 627 * @param projectKey the key of the project, or null to put the bitmap in the framework cache. 628 */ setCachedBitmap(String value, Bitmap bmp, Object projectKey)629 public static void setCachedBitmap(String value, Bitmap bmp, Object projectKey) { 630 if (projectKey != null) { 631 Map<String, SoftReference<Bitmap>> map = 632 sProjectBitmapCache.computeIfAbsent(projectKey, k -> new HashMap<>()); 633 634 map.put(value, new SoftReference<>(bmp)); 635 } else { 636 sFrameworkBitmapCache.put(value, new SoftReference<>(bmp)); 637 } 638 } 639 640 /** 641 * Returns the 9 patch chunk for a specific path, from a specific project cache, or from the 642 * framework cache. 643 * @param value the path of the 9 patch 644 * @param projectKey the key of the project, or null to query the framework cache. 645 * @return the cached 9 patch or null if not found. 646 */ getCached9Patch(String value, Object projectKey)647 public static NinePatchChunk getCached9Patch(String value, Object projectKey) { 648 if (projectKey != null) { 649 Map<String, SoftReference<NinePatchChunk>> map = sProject9PatchCache.get(projectKey); 650 651 if (map != null) { 652 SoftReference<NinePatchChunk> ref = map.get(value); 653 if (ref != null) { 654 return ref.get(); 655 } 656 } 657 } else { 658 SoftReference<NinePatchChunk> ref = sFramework9PatchCache.get(value); 659 if (ref != null) { 660 return ref.get(); 661 } 662 } 663 664 return null; 665 } 666 667 /** 668 * Sets a 9 patch chunk in a project cache or in the framework cache. 669 * @param value the path of the 9 patch 670 * @param ninePatch the 9 patch object 671 * @param projectKey the key of the project, or null to put the bitmap in the framework cache. 672 */ setCached9Patch(String value, NinePatchChunk ninePatch, Object projectKey)673 public static void setCached9Patch(String value, NinePatchChunk ninePatch, Object projectKey) { 674 if (projectKey != null) { 675 Map<String, SoftReference<NinePatchChunk>> map = 676 sProject9PatchCache.computeIfAbsent(projectKey, k -> new HashMap<>()); 677 678 map.put(value, new SoftReference<>(ninePatch)); 679 } else { 680 sFramework9PatchCache.put(value, new SoftReference<>(ninePatch)); 681 } 682 } 683 684 @Override clearFontCache(String path)685 public void clearFontCache(String path) { 686 if (sIsTypefaceInitialized) { 687 final String key = 688 Typeface_Builder_Delegate.createAssetUid(BridgeAssetManager.initSystem(), path, 689 0, null, RESOLVE_BY_FONT_TABLE, RESOLVE_BY_FONT_TABLE, DEFAULT_FAMILY); 690 Typeface.sDynamicTypefaceCache.remove(key); 691 } 692 } 693 } 694