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())
相关推荐
帅得不敢出门9 小时前
安卓设备adb执行AT指令控制电话卡
android·adb·sim卡·at指令·电话卡
我又来搬代码了11 小时前
【Android】使用productFlavors构建多个变体
android
德育处主任12 小时前
Mac和安卓手机互传文件(ADB)
android·macos
芦半山12 小时前
Android“引用们”的底层原理
android·java
迃-幵13 小时前
力扣:225 用队列实现栈
android·javascript·leetcode
大风起兮云飞扬丶13 小时前
Android——从相机/相册获取图片
android
Rverdoser13 小时前
Android Studio 多工程公用module引用
android·ide·android studio
aaajj14 小时前
[Android]从FLAG_SECURE禁止截屏看surface
android
@OuYang14 小时前
android10 蓝牙(二)配对源码解析
android
Liknana14 小时前
Android 网易游戏面经
android·面试