ARouter源码分析

Arouter 源码解析

我们从常用的功能点入手,基本可了解到ARouter的运转原理

初始化

java 复制代码
 public static void init(Application application) {
        if (!hasInit) {
            logger = _ARouter.logger;
            _ARouter.logger.info(Consts.TAG, "ARouter init start.");
            hasInit = _ARouter.init(application);

            if (hasInit) {
                _ARouter.afterInit();
            }

            _ARouter.logger.info(Consts.TAG, "ARouter init over.");
        }
    }

这里Arouter主要执行了 _ARouter.init(application) 和_ARouter.afterInit() , 有些门面模式的设计意味

_ARouter.init(application)

java 复制代码
    static ILogger logger = new DefaultLogger(Consts.TAG);
    .....
    .....
    //线程执行器,核心线程和最大线程相等,为设备cpu数+1
    private volatile static ThreadPoolExecutor executor = DefaultPoolExecutor.getInstance();
    private static Handler mHandler;
    private static Context mContext;

    private static InterceptorService interceptorService;

    private _ARouter() {
    }

    protected static synchronized boolean init(Application application) {
        mContext = application;  // mContext为application context
        //核心逻辑中心初始化,重点
        LogisticsCenter.init(mContext, executor);
        logger.info(Consts.TAG, "ARouter init success!");
        hasInit = true;
        mHandler = new Handler(Looper.getMainLooper()); // main线程handler

        return true;
    }
    
    ......
    ......

重点是 LogisticsCenter.init(mContext, executor);

java 复制代码
public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException{
  // 记录一下线程池和context
  mContext = context;
  executor = tpe;
  ......
  ......
  Set<String> routerMap;
  //如果版本发生更新或者是调试阶段
  if(ARouter.debuggable()||PackageUtils.isNewVersion(context)){
    //将com.alibaba.android.arouter.routes这个包下面的class加载到内存中,这个包下面的代码就是apt生成的代码
    routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
    ......
    ......  
  }else{
    ......
  }
  // 将apt生成的class分类别处理,这里有3种类型:1.IRouteRoot 2.IInterceptorGroup 3.IProviderGroup
  for (String className : routerMap){
    if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)){
      ((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);
    }else if(className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_INTERCEPTORS)){
       ((IInterceptorGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.interceptorsIndex);
    }else if(className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_PROVIDERS)){
     ((IProviderGroup(Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.providersIndex);
    }
  }
}

将apt生成的代码分成3种类型:1.IRouteRoot ``2.IInterceptorGroup 3.IProviderGroup

IRouteRoot示例:

java 复制代码
public class ARouter$$Root$$main implements IRouteRoot {
  @Override
  public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
    routes.put("module_main", ARouter$$Group$$module_main.class);
  }
}

IRouteRoot加载了一个group对象ARouter$$Group$$module_main.class

ARouter$$Group$$module_main.class

java 复制代码
public class ARouter$$Group$$module_main implements IRouteGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> atlas) {
    atlas.put("/module_main/application", RouteMeta.build(RouteType.PROVIDER, MainApplicationProvider.class, "/module_main/application", "module_main", null, -1, -2147483648));
  }
}

ARouter$$Group$$module_main 加载了真实的RouteMeta

Arouter 通过反射调用IRouteRoot对象的loadInto方法,将IRouteGroup class加载到内存中,而IRouteGroup里面又包含了RouteMeta(我们常用的@Route 注解最终生成的信息)信息 ,**之所以不直接把RouteMeta加载到内存,是一种懒加载的思路 **

同样的方式看IInterceptorGroupIProviderGroup

最终解析的信息结果保存在Warehouse.java

java 复制代码
class Warehouse {
    // Cache route and metas,路由和RouteMeta,现阶段IRouteGroup信息保存groupsIndex中
    static Map<String, Class<? extends IRouteGroup>> groupsIndex = new HashMap<>();
    static Map<String, RouteMeta> routes = new HashMap<>();

    // Cache provider
    static Map<Class, IProvider> providers = new HashMap<>();
    //现阶段,IProviderGroup信息保存在providersIndex中,providers map中没有内容
    static Map<String, RouteMeta> providersIndex = new HashMap<>();

    // Cache interceptor,同理,保存IInterceptorGroup信息
    static Map<Integer, Class<? extends IInterceptor>> interceptorsIndex = new UniqueKeyTreeMap<>("More than one interceptors use same priority [%s]");
    static List<IInterceptor> interceptors = new ArrayList<>();

    static void clear() {
        routes.clear();
        groupsIndex.clear();
        providers.clear();
        providersIndex.clear();
        interceptors.clear();
        interceptorsIndex.clear();
    }
}

_ARouter.afterInit()

java 复制代码
//初始化拦截器,负值interceptorService
static void afterInit() {
        // Trigger interceptor init, use byName.
        interceptorService = (InterceptorService)ARouter.getInstance().build("/arouter/service/interceptor").navigation();                                                                                
    }

好了,初始化完成了apt生成的class的处理,将route信息,privider信息,interceptor信息加载到了内存中,放到Warehouse的属性里面

路由的使用

kotlin 复制代码
ARouter.getInstance().build("/test/activity").withString("name", "ray").navigation();

这个步骤分为几个点:

1.ARouter调用Build生成Postcard,过程是怎样的

2.Postcard是什么

3.Postcard调用navigation是怎样执行的

生成Postcard

java 复制代码
    protected Postcard build(String path) {
        if (TextUtils.isEmpty(path)) {
            throw new HandlerException(Consts.TAG + "Parameter is invalid!");
        } else {
            //ARouter.getInstance().navigation是查找provider,pService = null
            PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
            if (null != pService) {
                path = pService.forString(path);
            }
            return build(path, extractGroup(path), true);
        }
    }

    
    /**
     * Build postcard by path and group
     */
    protected Postcard build(String path, String group, Boolean afterReplace) {
        if (TextUtils.isEmpty(path) || TextUtils.isEmpty(group)) {
            throw new HandlerException(Consts.TAG + "Parameter is invalid!");
        } else {
            if (!afterReplace) {
                PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
                if (null != pService) {
                    path = pService.forString(path);
                }
            }
            // 生成Postcard, 传入path和group , group是路径中的第一个/间的内容
            return new Postcard(path, group);
        }
    }

build生成了Postcard对象

Postcard是什么

首先Postcard继承RouteMeta,RouteMeta中存储的是关于route的一些基础信息,只定位于存储route基础信息。

java 复制代码
public class RouteMeta {
    private RouteType type;         // Type of route
    private Element rawType;        // Raw type of route
    private Class<?> destination;   // Destination
    private String path;            // Path of route
    private String group;           // Group of route
    private int priority = -1;      // The smaller the number, the higher the priority
    private int extra;              // Extra data
    private Map<String, Integer> paramsType;  // Param type
    private String name;

    private Map<String, Autowired> injectConfig;  // Cache inject config.
  
    ...
    
}

Postcard.java

java 复制代码
public final class Postcard extends RouteMeta {
    // Base
    private Uri uri;
    private Object tag;             // A tag prepare for some thing wrong.
    private Bundle mBundle;         // Data to transform
    private int flags = -1;         // Flags of route
    private int timeout = 300;      // Navigation timeout, TimeUnit.Second
    private IProvider provider;     // It will be set value, if this postcard was provider.
    private boolean greenChannel;
    private SerializationService serializationService;

    // Animation
    private Bundle optionsCompat;    // The transition animation of activity
    private int enterAnim = -1;
    private int exitAnim = -1;
  
    ...
      
    public void navigation(Activity mContext, int requestCode, NavigationCallback callback){
        ARouter.getInstance().navigation(mContext, this, requestCode, callback);
    }
  
    ...
      
    public Postcard withString(@Nullable String key, @Nullable String value) {
        mBundle.putString(key, value);
        return this;
    }
  
    ...
      
    public Postcard withTransition(int enterAnim, int exitAnim) {
        this.enterAnim = enterAnim;
        this.exitAnim = exitAnim;
        return this;
    }
  
    ...
}

虽然Postcard中有很多属性,但是此时,build生成的postcard对象中只有path和`group,下面看看Postcard信息是如何被补齐的

Postcard调用navigation是怎样执行的

最终执行_Arouter的navigation

java 复制代码
protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
   .....
   try{
     //补齐Postcard的信息
     LogisticsCenter.completion(postcard);
   } catch(Exception e){
     ......
   }
   ......
   ......  
   //执行拦截器相关逻辑
   interceptorService.doInterceptions(postcard, new InterceptorCallback(){
                @Override
                public void onContinue(Postcard postcard) {
                    _navigation(context, postcard, requestCode, callback);
                }
                @Override
                public void onInterrupt(Throwable exception) {
                    if (null != callback) {
                        callback.onInterrupt(postcard);
                    }
                }
   }
  
   //执行路由相关逻辑
   return _navigation(context, postcard, requestCode, callback);                                   
}

Postcard信息的补齐

java 复制代码
//LogisticsCenter.java
public synchronized static void completion(Postcard postcard){
  ......
  ......
  // 从Warehouse.routes中获取RouteMeta  
  RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());
  if (null == routeMeta){
    //第一次肯定获取RouteMeta不到,因为信息保存在Warehouse的groupsIndex中了,所以,接着从groupsIndex中取
    Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(postcard.getGroup());
    ......
    ......
     //  取出IRouteGroup
     IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance();
     // iGroupInstance内部的信息加载到Warehouse.routes中(用的时候meta加载到内存,懒加载啊)
     iGroupInstance.loadInto(Warehouse.routes);
     Warehouse.groupsIndex.remove(postcard.getGroup());  
  }
  
  ......
  ......
   // postcard在这里初始化,知道了route目标的Destination,Type和其他信息,这里的type是区分目标是activity或者provider等 
   postcard.setDestination(routeMeta.getDestination());
   postcard.setType(routeMeta.getType());
   postcard.setPriority(routeMeta.getPriority());
   postcard.setExtra(routeMeta.getExtra());  
   ......
   ......
   
   switch (routeMeta.getType()){
       // 对于provider类型,先生成provider对象,然后初始化,保存到缓存中,防止多次重复创建,
       case PROVIDER:
       Class<? extends IProvider> providerMeta = (Class<? extends IProvider>) routeMeta.getDestination();
                    IProvider instance = Warehouse.providers.get(providerMeta);
                    if (null == instance) { // There's no instance of this provider
                        IProvider provider;
                        try {
                            provider = providerMeta.getConstructor().newInstance();
                            provider.init(mContext);
                            Warehouse.providers.put(providerMeta, provider);
                            instance = provider;
                        } catch (Exception e) {
                            throw new HandlerException("Init provider failed! " + e.getMessage());
                        }
                    }
                    postcard.setProvider(instance);
                    postcard.greenChannel();    // Provider should skip all of interceptors
        break;
        case FRAGMENT:
             postcard.greenChannel();
       default:
                    break;//
   }  
}

好了,此时我们跳转需要的信息就得到了补齐

执行路由相关逻辑

java 复制代码
//_Arouter.java
private Object _navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback){
   // 初始化当前context,mContext是application的负值
   final Context currentContext = null == context ? mContext : context;
   switch (postcard.getType()){
       //如果是activity,执行跳转
       case ACTIVITY:
             final Intent intent = new Intent(currentContext, postcard.getDestination());
                intent.putExtras(postcard.getExtras());

                // Set flags.
                int flags = postcard.getFlags();
                if (-1 != flags) {
                    intent.setFlags(flags);
                } else if (!(currentContext instanceof Activity)) {    // 如果是application context执行调整,flag需                    要添加Intent.FLAG_ACTIVITY_NEW_TASK
                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                }

                // Set Actions
                String action = postcard.getAction();
                if (!TextUtils.isEmpty(action)) {
                    intent.setAction(action);
                }

                // Navigation in main looper.
                runInMainThread(new Runnable() {
                    @Override
                    public void run() {
                        //主线程去执行
                        startActivity(requestCode, currentContext, intent, postcard, callback);
                    }
                });
                break;
        //如果是provider类型,返回provider对象实例          
        case PROVIDER:
                return postcard.getProvider();
                
       case BOARDCAST:
       case CONTENT_PROVIDER:
       // 如果是Fargment,通过反射创建Fargment对象实例,这种每次会创建新实例 
       case FRAGMENT:
                Class fragmentMeta = postcard.getDestination();
                try {
                    Object instance = fragmentMeta.getConstructor().newInstance();
                    if (instance instanceof Fragment) {
                        ((Fragment) instance).setArguments(postcard.getExtras());
                    } else if (instance instanceof android.support.v4.app.Fragment) {
                        ((android.support.v4.app.Fragment) instance).setArguments(postcard.getExtras());
                    }

                    return instance;
                } catch (Exception ex) {
                    ......
                    ......  
                }
   }
}

拦截器相关逻辑

拦截器初始化:_ARouter.afterInit()
java 复制代码
 static void afterInit() {
        // Trigger interceptor init, use byName.
        interceptorService = (InterceptorService)   ARouter.getInstance().build("/arouter/service/interceptor").navigation();
    }

/arouter/service/interceptor 关联的类在com.alibaba.android.arouter.core.InterceptorServiceImpl, InterceptorService 继承 IProvider ,所以,创建 InterceptorServiceImpl实例时会调用init(context)方法完成初始化

参考上面Postcard信息的补齐步骤

java 复制代码
@Route(path = "/arouter/service/interceptor")
public class InterceptorServiceImpl implements InterceptorService{
  private static boolean interceptorHasInit;
    private static final Object interceptorInitLock = new Object();
  ......
  ......
      @Override
    public void init(final Context context) {
        LogisticsCenter.executor.execute(new Runnable() {
            @Override
            public void run() {
                if (MapUtils.isNotEmpty(Warehouse.interceptorsIndex)) {
                   //循环遍历Warehouse.interceptorsIndex
                    for (Map.Entry<Integer, Class<? extends IInterceptor>> entry : Warehouse.interceptorsIndex.entrySet()) {
                        Class<? extends IInterceptor> interceptorClass = entry.getValue();
                        try {
                          // 反射创建IInterceptor对象实例
                            IInterceptor iInterceptor = interceptorClass.getConstructor().newInstance();
                            //执行拦截器初始化
                            iInterceptor.init(context);
                            Warehouse.interceptors.add(iInterceptor);
                        } catch (Exception ex) {
                            throw new HandlerException(TAG + "ARouter init interceptor error! name = [" + interceptorClass.getName() + "], reason = [" + ex.getMessage() + "]");
                        }
                    }

                    interceptorHasInit = true;

                    logger.info(TAG, "ARouter interceptors init over.");

                    synchronized (interceptorInitLock) {
                        interceptorInitLock.notifyAll();
                    }
                }
            }
        });
    }  
}

Warehouse.interceptorsIndex中的拦截器的class对象是在初始化的时候加载到内存中的

java 复制代码
//LogisticsCenter.java
//执行init的时候执行了IInterceptorGroup的初始化,将interceptor class对象加载到了Warehouse.interceptorsIndex
public synchronized static void init(Context context, ThreadPoolExecutor tpe){
  ......
  ......
  else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_INTERCEPTORS)) {
    ((IInterceptorGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.interceptorsIndex);
  }  
}

示例如下:

java 复制代码
/**
 * DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
public class ARouter$$Interceptors$$app implements IInterceptorGroup {
  @Override
  public void loadInto(Map<Integer, Class<? extends IInterceptor>> interceptors) {
    interceptors.put(3, LoginInterceptor.class);
  }
}

此时Warehouse.interceptorsIndex 中存入了 IInterceptorclass对象 , 这些拦截器在InterceptorServiceImpl.java 中完成了初始化

java 复制代码
//InterceptorServiceImpl.java
//遍历Warehouse.interceptorsIndex,执行IInterceptor初始化
for (Map.Entry<Integer, Class<? extends IInterceptor>> entry : Warehouse.interceptorsIndex.entrySet()){
   Class<? extends IInterceptor> interceptorClass = entry.getValue();
   try{
         //反射创建拦截器对象,执行init方法,并将其保存在Warehouse.interceptors中
         IInterceptor iInterceptor = interceptorClass.getConstructor().newInstance();
         iInterceptor.init(context);
         Warehouse.interceptors.add(iInterceptor);
   }catch(){
     .....
     .....  
   }
 }
拦截器生效

Postcard执行 navigation时触发

java 复制代码
//_ARouter.java
protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
  ......
  ......
          //不是绿色通道,那么需要拦截器的检查
          if (!postcard.isGreenChannel()) {   // It must be run in async thread, maybe interceptor cost too mush time made ANR.
            interceptorService.doInterceptions(postcard, new InterceptorCallback() {
                /**
                 * Continue process
                 *
                 * @param postcard route meta
                 */
                @Override
                public void onContinue(Postcard postcard) {
                    _navigation(context, postcard, requestCode, callback);
                }

                /**
                 * Interrupt process, pipeline will be destory when this method called.
                 *
                 * @param exception Reson of interrupt.
                 */
                @Override
                public void onInterrupt(Throwable exception) {
                    if (null != callback) {
                        callback.onInterrupt(postcard);
                    }

                    logger.info(Consts.TAG, "Navigation failed, termination by interceptor : " + exception.getMessage());
                }
            });
        }  
}

执行interceptorService.doInterceptions , interceptorServiceInterceptorServiceImpl的实例

java 复制代码
//InterceptorServiceImpl.java
public void doInterceptions(final Postcard postcard, final InterceptorCallback callback){
   // 有拦截器,先执行拦截器相关代码
   if (null != Warehouse.interceptors && Warehouse.interceptors.size() > 0) {
     LogisticsCenter.executor.execute(new Runnable() {
       public void run{
         //CancelableCountDownLatch是等待拦截器执行完成或者取消
   CancelableCountDownLatch interceptorCounter = new CancelableCountDownLatch(Warehouse.interceptors.size());
         //执行拦截器相关代码
         _execute(0, interceptorCounter, postcard);
         //等待拦截器执行或者超时
         interceptorCounter.await(postcard.getTimeout(), TimeUnit.SECONDS); 
         // 根据拦截器的执行结果,决定后续的执行流程
         
         //拦截器执行超时,后续执行callback.onInterrupt的方法,最终执行NavigationCallback.onInterrupt方法
          if (interceptorCounter.getCount() > 0) {
            callback.onInterrupt(new HandlerException("The interceptor processing timed out."));
          }else if (null != postcard.getTag()){
            //如果执行结果中postcard.getTag()不为null,也是最终执行NavigationCallback.onInterrupt方法
            callback.onInterrupt(new HandlerException(postcard.getTag().toString()));
          }else{
            //如果拦截器都不拦截,实际执行_navigation(context, postcard, requestCode, callback);
             callback.onContinue(postcard);
          }
       }
     }
   }else{
     //没有拦截器,则执行callback.onContinue,实际执行_navigation(context, postcard, requestCode, callback);
     callback.onContinue(postcard);
   }
}

看看_execute(0, interceptorCounter, postcard);具体执行拦截器的过程

java 复制代码
//InterceptorServiceImpl.java
private static void _execute(final int index, final CancelableCountDownLatch counter, final Postcard postcard) {
   if (index < Warehouse.interceptors.size()) {
     //此时的index等于0,也就是从第一个开始取(拦截器还存在优先级的问题)
     IInterceptor iInterceptor = Warehouse.interceptors.get(index);
     //执行拦截器自身的process方法
     iInterceptor.process(postcard, new InterceptorCallback() {
       @Override
                public void onContinue(Postcard postcard) {
                    //如果拦截器不拦截,执行结果为onContinue,那么0号拦截器执行完成
                    counter.countDown();
                   //启动下一个拦截器的执行(index + 1 )
                    _execute(index + 1, counter, postcard); 
                }
                       @Override
                public void onInterrupt(Throwable exception) {
                  // 如果拦截器执行了拦截,那么取消其他拦截器的等待(counter.cancel()),将postcard的tag赋值
                    postcard.setTag(null == exception ? new HandlerException("No message.") :         exception.getMessage());    // save the exception message for backup.
                    counter.cancel();
                }
     })
   }
}

如果拦截器执行了拦截过程,null != postcard.getTag()true , 最终执行NavigationCallback.onInterrupt方法

java 复制代码
//拦截器执行超时,后续执行callback.onInterrupt的方法,最终执行NavigationCallback.onInterrupt方法
          if (interceptorCounter.getCount() > 0) {
            callback.onInterrupt(new HandlerException("The interceptor processing timed out."));
          }else if (null != postcard.getTag()){
            //如果执行结果中postcard.getTag()不为null,也是最终执行NavigationCallback.onInterrupt方法
            callback.onInterrupt(new HandlerException(postcard.getTag().toString()));
          }else{
            //如果拦截器都不拦截,实际执行_navigation(context, postcard, requestCode, callback);
             callback.onContinue(postcard);
          }
拦截器使用示例

定义拦截器,设置拦截器的内容

java 复制代码
@Interceptor(name = "login", priority = 3)
class LoginInterceptor : IInterceptor {
    override fun init(context: Context?) {
        // do init,context == applicaiton
    }

    override fun process(postcard: Postcard?, callback: InterceptorCallback?) {
        if (!LoginManager.isLogin()) {
            when (postcard?.path) {
                Constants.ARouterPath.MAIN_PATH -> {
                    callback?.onInterrupt(null)
                }
                else -> {
                    callback?.onContinue(postcard)
                }
            }
        } else {
            callback?.onContinue(postcard)
        }
    }
}

触发拦截器后拦截后,执行的业务

kotlin 复制代码
class LoginNavigationCallbackImpl : NavigationCallback {

    override fun onFound(postcard: Postcard?) {
        // onFound
    }

    override fun onLost(postcard: Postcard?) {

    }

    override fun onArrival(postcard: Postcard?) {

    }

    override fun onInterrupt(postcard: Postcard?) {
        val path = postcard?.path
        val bundle = postcard?.extras
        val actions = postcard?.action
        val flags = postcard?.flags ?: -1

        ARouter.getInstance().build(Constants.ARouterPath.LOGIN_PATH)
            .withString(LoginActivity.POSTCARD_PATH, path)
            .withBundle(LoginActivity.POSTCARD_BUNDLE, bundle)
            .withInt(LoginActivity.POSTCARD_FLAGS, flags)
            .withString(LoginActivity.POSTCARD_ACTIONS, actions).navigation()
    }
}

路由跳转使用拦截器

Postcard 没有设置greenChannel , 均会触发拦截器的检查

kotlin 复制代码
 fun openMainActivity(context: Context, userId: String) {
            ARouter.getInstance().build(MAIN_PATH).withString("userId", userId)
                .navigation(context, LoginNavigationCallbackImpl())
        }

不需要拦截,可以设置greenChannel

java 复制代码
ARouter.getInstance().build(MAIN_PATH).withString("userId", userId)
                .greenChannel()
                .navigation(context, LoginNavigationCallbackImpl())
相关推荐
Gary Studio1 小时前
Android AIDL HAL工程结构示例
android
y = xⁿ2 小时前
MySQL八股知识合集
android·mysql·adb
andr_gale2 小时前
04_rc文件语法规则
android·framework·aosp
祖国的好青年3 小时前
VS Code 搭建 React Native 开发环境(Windows 实战指南)
android·windows·react native·react.js
黄林晴4 小时前
警惕!AGP 9.2 别只改版本号,R8 规则与构建链路全线收紧
android·gradle
小米渣的逆袭4 小时前
Android ADB 完全使用指南
android·adb
儿歌八万首4 小时前
Jetpack Compose Canvas 进阶:结合 animateFloatAsState 让自定义图形动起来
android·动画·compose
zhangphil5 小时前
Android Page 3 Flow读sql数据库媒体文件,Kotlin
android·kotlin
神探小白牙5 小时前
echarts,3d堆叠图
android·3d·echarts