这里我不阐述ARouter,组件化的作用和使用方式,大家可以自行百度。android组件化开发实战
思路一
我们可以让每个module去依赖同一个module(定义为arouter),之后新建一个类将每个activity添加到一个HashMap集合中,key是每个路径,value是类的名字
public class ARouter {
private Map<String, Class<? extends Activity>> activityList;
private static class Holder {
private static ARouter aRouter = new ARouter();
}
private ARouter() {
activityList = new HashMap<>();
}
public static ARouter getInstance() {
return Holder.aRouter;
}
//activity对象存入List
public void putActivity(String path, Class<? extends Activity> clazz) {
if (path == null || clazz == null) {
return;
}
activityList.put(path, clazz);
}
}
新建一个接口IRoute(为什么定义接口,后面会说到)
public interface IRoute {
void putActivity();
}
此时我们可以让依赖aroute包的module(假设登录module,有个loginActivity)新建一个ActivityUtils工具类,此时工具类可以这么写
public class ActivityUtils implements IRoute {
@Override
public void putActivity() {
ARouter.getInstance().putActivity("/login/loginActivity", com.peakmain.loginmodule.LoginActivity.class);
}
}
问题:每个module都需要写一个工具类,且每次新增一个activity就需要去写一行ARouter.getInstance().putActivity()方法,且当删除一个activity的时候还需要去这个类下删除相关的activity
思路二
- 目的:我们不用自己去写ActivityUtils工具类,我们让程序自动生成
- 技术:用注解+编译器方式
- 首先新建两个module:animation(注解),animation-compiler(编译器)
新建注解Path
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)//编译时注解
public @interface Path {
String path();
}
animation-compile需要添加依赖
implementation 'com.google.auto.service:auto-service:1.0-rc3'
注解生成器代码
@AutoService(Processor.class)
public class AnnotationCompiler extends AbstractProcessor {
//生成文件对象
Filer filer;
@Override
public synchronized void init(ProcessingEnvironment processingEnvironment) {
super.init(processingEnvironment);
filer = processingEnvironment.getFiler();
}
//声明返回要处理哪个注解
@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new HashSet<>();
types.add(Path.class.getCanonicalName());
return types;
}
//支持Java版本
@Override
public SourceVersion getSupportedSourceVersion() {
return processingEnv.getSourceVersion();
}
//注解处理器的核心
@Override
public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
//拿到该模块所有path注解的节点
Set<? extends Element> elementsAnnotatedWith = roundEnvironment.getElementsAnnotatedWith(Path.class);
//结构化数据
Map<String, String> map = new HashMap<>();
for (Element element : elementsAnnotatedWith) {
//实际就是类节点
TypeElement typeElement = (TypeElement) element;
Path annotation = typeElement.getAnnotation(Path.class);
//读取到key
String key = annotation.path();
//包名+类名
String activityName = typeElement.getQualifiedName().toString();
map.put(key, activityName);
}
if (map.size() > 0) {
//开始写文件
Writer writer = null;
String utilsName = "ActivityUtils" ;
try {
JavaFileObject javaFileObject = filer.createSourceFile("com.peakmain.utils." + utilsName);
writer = javaFileObject.openWriter();
writer.write("package com.peakmain.utils;\n" +
"\n"
+ "import com.peakmain.arouter.ARouter;\n"
+ "import com.peakmain.arouter.IRoute;\n"
+ "\n"
+ "public class " + utilsName + " implements IRoute {\n"
+ "\n" +
" @Override\n" +
" public void putActivity() {"
+ "\n");
Iterator<String> iterator = map.keySet().iterator();
while (iterator.hasNext()) {
String path = iterator.next();
String value = map.get(path);
writer.write("ARouter.getInstance().putActivity(\"" + path + "\","
+ value + ".class);\n");
}
writer.write("}\n}");
} catch (IOException e) {
e.printStackTrace();
} finally {
if (writer != null) {
try {
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
return false;
}
}
代码看起来应该还是较简单,主要的目的就是生成工具类ActivityUtils相关代码
之后需要每个module去依赖annmtion和annmtion-compiler
思路三
生成相关工具类代码之后,我们可以在之前ARouter类中写个跳转方法
//跳转
public void jupmActivity(String path, Bundle bundle) {
Class<? extends Activity> aClass = activityList.get(path);
if (aClass == null) {
return;
}
Intent intent = new Intent().setClass(context, aClass);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
if (bundle != null) {
intent.putExtra("bundle", bundle);
}
context.startActivity(intent);
}
此时还不能跳转,因为activityList还是空的,这时候我们需要通过指定包名,扫描下面所有的类名,继续在ARouter类中写
private Context context;
public void init(Application application) {
this.context = application;
try {
Set<String> className = ClassUtils.getFileNameByPackageName(context,"com.peakmain.utils");
for (String name : className) {
try {
Class<?> aClass = Class.forName(name);
//判断当前类是否是IRouter的实现类
if(IRoute.class.isAssignableFrom(aClass)){
IRoute iRoute= (IRoute) aClass.newInstance();
iRoute.putActivity();
}
} catch (Exception e) {
e.printStackTrace();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
这里我们就可以知道为什么要用一个接口,一、因为可能com.peakmain.utils下会有很多类。二、方便用于直接执行
至于工具类大家可以直接拷贝ARouter里面的,这里我为了方便大家,我直接贴一份代码了
ClassUtils
public class ClassUtils {
private static final String EXTRACTED_NAME_EXT = ".classes";
private static final String EXTRACTED_SUFFIX = ".zip";
private static final String SECONDARY_FOLDER_NAME = "code_cache" + File.separator + "secondary-dexes";
private static final String PREFS_FILE = "multidex.version";
private static final String KEY_DEX_NUMBER = "dex.number";
private static final int VM_WITH_MULTIDEX_VERSION_MAJOR = 2;
private static final int VM_WITH_MULTIDEX_VERSION_MINOR = 1;
private static SharedPreferences getMultiDexPreferences(Context context) {
return context.getSharedPreferences(PREFS_FILE, Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB ? Context.MODE_PRIVATE : Context.MODE_PRIVATE | Context.MODE_MULTI_PROCESS);
}
/**
* 通过指定包名,扫描包下面包含的所有的ClassName
*
* @param context U know
* @param packageName 包名
* @return 所有class的集合
*/
public static Set<String> getFileNameByPackageName(Context context, final String packageName) throws PackageManager.NameNotFoundException, IOException, InterruptedException {
final Set<String> classNames = new HashSet<>();
List<String> paths = getSourcePaths(context);
final CountDownLatch parserCtl = new CountDownLatch(paths.size());
for (final String path : paths) {
DefaultPoolExecutor.getInstance().execute(new Runnable() {
@Override
public void run() {
DexFile dexfile = null;
try {
if (path.endsWith(EXTRACTED_SUFFIX)) {
//NOT use new DexFile(path), because it will throw "permission error in /data/dalvik-cache"
dexfile = DexFile.loadDex(path, path + ".tmp", 0);
} else {
dexfile = new DexFile(path);
}
Enumeration<String> dexEntries = dexfile.entries();
while (dexEntries.hasMoreElements()) {
String className = dexEntries.nextElement();
if (className.startsWith(packageName)) {
classNames.add(className);
}
}
} catch (Throwable ignore) {
Log.e("ARouter", "Scan map file in dex files made error.", ignore);
} finally {
if (null != dexfile) {
try {
dexfile.close();
} catch (Throwable ignore) {
}
}
parserCtl.countDown();
}
}
});
}
parserCtl.await();
//Log.d(Consts.TAG, "Filter " + classNames.size() + " classes by packageName <" + packageName + ">");
return classNames;
}
/**
* get all the dex path
*
* @param context the application context
* @return all the dex path
* @throws PackageManager.NameNotFoundException
* @throws IOException
*/
public static List<String> getSourcePaths(Context context) throws PackageManager.NameNotFoundException, IOException {
ApplicationInfo applicationInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), 0);
File sourceApk = new File(applicationInfo.sourceDir);
List<String> sourcePaths = new ArrayList<>();
sourcePaths.add(applicationInfo.sourceDir); //add the default apk path
//the prefix of extracted file, ie: test.classes
String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
// 如果VM已经支持了MultiDex,就不要去Secondary Folder加载 Classesx.zip了,那里已经么有了
// 通过是否存在sp中的multidex.version是不准确的,因为从低版本升级上来的用户,是包含这个sp配置的
if (!isVMMultidexCapable()) {
//the total dex numbers
int totalDexNumber = getMultiDexPreferences(context).getInt(KEY_DEX_NUMBER, 1);
File dexDir = new File(applicationInfo.dataDir, SECONDARY_FOLDER_NAME);
for (int secondaryNumber = 2; secondaryNumber <= totalDexNumber; secondaryNumber++) {
//for each dex file, ie: test.classes2.zip, test.classes3.zip...
String fileName = extractedFilePrefix + secondaryNumber + EXTRACTED_SUFFIX;
File extractedFile = new File(dexDir, fileName);
if (extractedFile.isFile()) {
sourcePaths.add(extractedFile.getAbsolutePath());
//we ignore the verify zip part
} else {
throw new IOException("Missing extracted secondary dex file '" + extractedFile.getPath() + "'");
}
}
}
sourcePaths.addAll(tryLoadInstantRunDexFile(applicationInfo));
/* if (ARouter.debuggable()) { // Search instant run support only debuggable
sourcePaths.addAll(tryLoadInstantRunDexFile(applicationInfo));
}*/
return sourcePaths;
}
/**
* Get instant run dex path, used to catch the branch usingApkSplits=false.
*/
private static List<String> tryLoadInstantRunDexFile(ApplicationInfo applicationInfo) {
List<String> instantRunSourcePaths = new ArrayList<>();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && null != applicationInfo.splitSourceDirs) {
// add the split apk, normally for InstantRun, and newest version.
instantRunSourcePaths.addAll(Arrays.asList(applicationInfo.splitSourceDirs));
//Log.d(Consts.TAG, "Found InstantRun support");
} else {
try {
// This man is reflection from Google instant run sdk, he will tell me where the dex files go.
Class pathsByInstantRun = Class.forName("com.android.tools.fd.runtime.Paths");
Method getDexFileDirectory = pathsByInstantRun.getMethod("getDexFileDirectory", String.class);
String instantRunDexPath = (String) getDexFileDirectory.invoke(null, applicationInfo.packageName);
File instantRunFilePath = new File(instantRunDexPath);
if (instantRunFilePath.exists() && instantRunFilePath.isDirectory()) {
File[] dexFile = instantRunFilePath.listFiles();
for (File file : dexFile) {
if (null != file && file.exists() && file.isFile() && file.getName().endsWith(".dex")) {
instantRunSourcePaths.add(file.getAbsolutePath());
}
}
// Log.d(Consts.TAG, "Found InstantRun support");
}
} catch (Exception e) {
//Log.e(Consts.TAG, "InstantRun support error, " + e.getMessage());
}
}
return instantRunSourcePaths;
}
/**
* Identifies if the current VM has a native support for multidex, meaning there is no need for
* additional installation by this library.
*
* @return true if the VM handles multidex
*/
private static boolean isVMMultidexCapable() {
boolean isMultidexCapable = false;
String vmName = null;
try {
if (isYunOS()) { // YunOS需要特殊判断
vmName = "'YunOS'";
isMultidexCapable = Integer.valueOf(System.getProperty("ro.build.version.sdk")) >= 21;
} else { // 非YunOS原生Android
vmName = "'Android'";
String versionString = System.getProperty("java.vm.version");
if (versionString != null) {
Matcher matcher = Pattern.compile("(\\d+)\\.(\\d+)(\\.\\d+)?").matcher(versionString);
if (matcher.matches()) {
try {
int major = Integer.parseInt(matcher.group(1));
int minor = Integer.parseInt(matcher.group(2));
isMultidexCapable = (major > VM_WITH_MULTIDEX_VERSION_MAJOR)
|| ((major == VM_WITH_MULTIDEX_VERSION_MAJOR)
&& (minor >= VM_WITH_MULTIDEX_VERSION_MINOR));
} catch (NumberFormatException ignore) {
// let isMultidexCapable be false
}
}
}
}
} catch (Exception ignore) {
}
//Log.i(Consts.TAG, "VM with name " + vmName + (isMultidexCapable ? " has multidex support" : " does not have multidex support"));
return isMultidexCapable;
}
/**
* 判断系统是否为YunOS系统
*/
private static boolean isYunOS() {
try {
String version = System.getProperty("ro.yunos.version");
String vmName = System.getProperty("java.vm.name");
return (vmName != null && vmName.toLowerCase().contains("lemur"))
|| (version != null && version.trim().length() > 0);
} catch (Exception ignore) {
return false;
}
}
}
DefaultPoolExecutor
public class DefaultPoolExecutor extends ThreadPoolExecutor {
// Thread args
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
private static final int INIT_THREAD_COUNT = CPU_COUNT + 1;
private static final int MAX_THREAD_COUNT = INIT_THREAD_COUNT;
private static final long SURPLUS_THREAD_LIFE = 30L;
private static DefaultPoolExecutor instance;
public static DefaultPoolExecutor getInstance() {
if (null == instance) {
synchronized (DefaultPoolExecutor.class) {
if (null == instance) {
instance = new DefaultPoolExecutor(
INIT_THREAD_COUNT,
MAX_THREAD_COUNT,
SURPLUS_THREAD_LIFE,
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(64),
new DefaultThreadFactory());
}
}
}
return instance;
}
private DefaultPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
}
});
}
/*
* 线程执行结束,顺便看一下有么有什么乱七八糟的异常
*
* @param r the runnable that has completed
* @param t the exception that caused termination, or null if
*/
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
if (t == null && r instanceof Future<?>) {
try {
((Future<?>) r).get();
} catch (CancellationException ce) {
t = ce;
} catch (ExecutionException ee) {
t = ee.getCause();
} catch (InterruptedException ie) {
Thread.currentThread().interrupt(); // ignore/reset
}
}
if (t != null) {
}
}
}
线程池工厂类DefaultThreadFactory
public class DefaultThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final String namePrefix;
public DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "ARouter task pool No." + poolNumber.getAndIncrement() + ", thread No.";
}
public Thread newThread(@NonNull Runnable runnable) {
String threadName = namePrefix + threadNumber.getAndIncrement();
Thread thread = new Thread(group, runnable, threadName, 0);
if (thread.isDaemon()) { //设为非后台线程
thread.setDaemon(false);
}
if (thread.getPriority() != Thread.NORM_PRIORITY) { //优先级为normal
thread.setPriority(Thread.NORM_PRIORITY);
}
// 捕获多线程处理中的异常
thread.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread thread, Throwable ex) {
}
});
return thread;
}
}
整体架构思路还是比较明白的,若有表诉不清楚的,欢迎大家留言