ARouter源码分析

2023-06-14,,

源码看过好几遍了,但是总是会忘记,特此记录下

先从注解处理器开始

BaseProcessor是其他三个注解处理器的抽象类,子类去实现process方法。
在其中的init方法中会获取我们的module模块中.gradle文件填写的AROUTER_MODULE_NAME对应的value。如果处理过的moduleName的值是空会抛出异常
if (StringUtils.isNotEmpty(moduleName)) {
moduleName = moduleName.replaceAll("[^0-9a-zA-Z_]+", ""); logger.info("The user has configuration the module name, it was [" + moduleName + "]");
} else {
logger.error(NO_MODULE_NAME_TIPS);
throw new RuntimeException("ARouter::Compiler >>> No module name, for more information, look at gradle log.");
}

然后就是初始化一些工具

 mFiler = processingEnv.getFiler();
types = processingEnv.getTypeUtils();
elementUtils = processingEnv.getElementUtils();
typeUtils = new TypeUtils(types, elementUtils);
logger = new Logger(processingEnv.getMessager());
RouteProcessor用来处理Route这个注解
//其实这个key是我们设置的group组名,缺省情况下是path的第一个单词,并不是每个module下的路由集合,
//例如:一个module下我们的2个Route值分别设置了group="group1"和 group="group2",那么这个Map中就会有2个值分别存储
//group1分组下的路由集合和group2下面的路由集合,也会为我们生成2个类设置。后面代码会解析
private Map<String, Set<RouteMeta>> groupMap = new HashMap<>(); // ModuleName and routeMeta. 这个官方的注释有问题
//这个其实是一个索引表,key是组名,value是上面注释中的生成的类名,也就是通过这个key就能找到上面的路由集合。这个是每一个module对应一个类。
private Map<String, String> rootMap = new TreeMap<>();

public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (CollectionUtils.isNotEmpty(annotations)) {
//扫描项目拿到所有带Route注解的元素
Set<? extends Element> routeElements = roundEnv.getElementsAnnotatedWith(Route.class);
try {
logger.info(">>> Found routes, start... <<<");
this.parseRoutes(routeElements); } catch (Exception e) {
logger.error(e);
}
return true;
} return false;
}

解析上面代码中的parseRoutes(Set<? extends Element> routeElements)方法

TypeMirror type_Activity = elementUtils.getTypeElement(ACTIVITY).asType();
TypeMirror type_Service = elementUtils.getTypeElement(SERVICE).asType();
TypeMirror fragmentTm = elementUtils.getTypeElement(FRAGMENT).asType();
TypeMirror fragmentTmV4 = elementUtils.getTypeElement(Consts.FRAGMENT_V4).asType();

// Interface of ARouter
TypeElement type_IRouteGroup = elementUtils.getTypeElement(IROUTE_GROUP);
TypeElement type_IProviderGroup = elementUtils.getTypeElement(IPROVIDER_GROUP);
ClassName routeMetaCn = ClassName.get(RouteMeta.class);
ClassName routeTypeCn = ClassName.get(RouteType.class);

获取类元素对应的Type类型 关于type和Element参考:https://www.jianshu.com/p/899063e8452e 

/*
Build input type, format as :

```Map<String, Class<? extends IRouteGroup>>```
*/
ParameterizedTypeName inputMapTypeOfRoot = ParameterizedTypeName.get(
ClassName.get(Map.class),
ClassName.get(String.class),
ParameterizedTypeName.get(
ClassName.get(Class.class),
WildcardTypeName.subtypeOf(ClassName.get(type_IRouteGroup))
)
);
/*

```Map<String, RouteMeta>```
*/
ParameterizedTypeName inputMapTypeOfGroup = ParameterizedTypeName.get(
ClassName.get(Map.class),
ClassName.get(String.class),
ClassName.get(RouteMeta.class)
);

利用javaport创建Map<String, Class<? extends IRouteGroup>> 和 Map<String, RouteMeta>方法这个参数类型

ParameterSpec rootParamSpec = ParameterSpec.builder(inputMapTypeOfRoot, "routes").build();
ParameterSpec groupParamSpec = ParameterSpec.builder(inputMapTypeOfGroup, "atlas").build();
ParameterSpec providerParamSpec = ParameterSpec.builder(inputMapTypeOfGroup, "providers").build();

到此就创建了
Map<String, Class<? extends IRouteGroup>> routes 和 Map<String, RouteMeta> atlas 以及 Map<String, RouteMeta> providers完整参数
/*
Build method : 'loadInto'
*/
MethodSpec.Builder loadIntoMethodOfRootBuilder = MethodSpec.methodBuilder(METHOD_LOAD_INTO)
.addAnnotation(Override.class)
.addModifiers(PUBLIC)
.addParameter(rootParamSpec);
创建方法:loadIntoMethodOfRootBuilder <--> loadInto(Map<String, Class<? extends IRouteGroup>> routes) 后面创建ARouter$$Root$$moduleName类使用

 for (Element element : routeElements) {
TypeMirror tm = element.asType();
Route route = element.getAnnotation(Route.class);
RouteMeta routeMeta; // Activity or Fragment
if (types.isSubtype(tm, type_Activity) || types.isSubtype(tm, fragmentTm) || types.isSubtype(tm, fragmentTmV4)) {
// Get all fields annotation by @Autowired
//带Autowirde注解属性的名称和类型(name,String类型)(sex,boolean属性)后面的Integer是枚举类型
Map<String, Integer> paramsType = new HashMap<>();

Map<String, Autowired> injectConfig = new HashMap<>();
//这个用来处理子级中带Autowirde注解
injectParamCollector(element, paramsType, injectConfig); if (types.isSubtype(tm, type_Activity)) {
// Activity
logger.info(">>> Found activity route: " + tm.toString() + " <<<");
routeMeta = new RouteMeta(route, element, RouteType.ACTIVITY, paramsType);
} else {
// Fragment
logger.info(">>> Found fragment route: " + tm.toString() + " <<<");
routeMeta = new RouteMeta(route, element, RouteType.parse(FRAGMENT), paramsType);
}
//Activity或者fragment中的带Autowird的属性也会放进路由中
routeMeta.setInjectConfig(injectConfig);
} else if (types.isSubtype(tm, iProvider)) { // IProvider
logger.info(">>> Found provider route: " + tm.toString() + " <<<");
routeMeta = new RouteMeta(route, element, RouteType.PROVIDER, null);
} else if (types.isSubtype(tm, type_Service)) { // Service
logger.info(">>> Found service route: " + tm.toString() + " <<<");
routeMeta = new RouteMeta(route, element, RouteType.parse(SERVICE), null);
} else {
throw new RuntimeException("The @Route is marked on unsupported class, look at [" + tm.toString() + "].");
}
//验证path的准确性和填充路由表groupMap
categories(routeMeta);
}
private void categories(RouteMeta routeMete) {
//验证通过后
if (routeVerify(routeMete)) {
logger.info(">>> Start categories, group = " + routeMete.getGroup() + ", path = " + routeMete.getPath() + " <<<");
Set<RouteMeta> routeMetas = groupMap.get(routeMete.getGroup());
if (CollectionUtils.isEmpty(routeMetas)) {
Set<RouteMeta> routeMetaSet = new TreeSet<>(new Comparator<RouteMeta>() {
@Override
public int compare(RouteMeta r1, RouteMeta r2) {
try {
return r1.getPath().compareTo(r2.getPath());
} catch (NullPointerException npe) {
logger.error(npe.getMessage());
return 0;
}
}
});
//填充路由表
routeMetaSet.add(routeMete);
//把路由表和相应的group对应
groupMap.put(routeMete.getGroup(), routeMetaSet);
} else {
//如果已经存在组了,就直接添加到路由表中
routeMetas.add(routeMete);
}
} else {
logger.warning(">>> Route meta verify error, group is " + routeMete.getGroup() + " <<<");
}
}
//验证path的准确性
private boolean routeVerify(RouteMeta meta) {
String path = meta.getPath();
//如果path是空或者path没有以"/"开始验证失败
if (StringUtils.isEmpty(path) || !path.startsWith("/")) { // The path must be start with '/' and not empty!
return false;
}
//group缺省情况用path中的第一个单词作为group值
if (StringUtils.isEmpty(meta.getGroup())) { // Use default group(the first word in path)
try {
String defaultGroup = path.substring(1, path.indexOf("/", 1));
if (StringUtils.isEmpty(defaultGroup)) {
return false;
}
//为路由信息设置组名
meta.setGroup(defaultGroup);
return true;
} catch (Exception e) {
logger.error("Failed to extract default group! " + e.getMessage());
return false;
}
}

return true;
}
经过上面的方法之后groupMap就被赋值成功了,下面就是类的生成了。

MethodSpec.Builder loadIntoMethodOfProviderBuilder = MethodSpec.methodBuilder(METHOD_LOAD_INTO)
.addAnnotation(Override.class)
.addModifiers(PUBLIC)
.addParameter(providerParamSpec);
创建 loadIntoMethodOfProviderBuilder <--->loadInto(Map<String, Class<? extends IRouteGroup>> providers )后面创建ARouter$$Providers$$moduleName类使用

  for (Map.Entry<String, Set<RouteMeta>> entry : groupMap.entrySet()) {
String groupName = entry.getKey(); MethodSpec.Builder loadIntoMethodOfGroupBuilder = MethodSpec.methodBuilder(METHOD_LOAD_INTO)
.addAnnotation(Override.class)
.addModifiers(PUBLIC)
.addParameter(groupParamSpec); List<RouteDoc> routeDocList = new ArrayList<>(); // Build group method body
Set<RouteMeta> groupData = entry.getValue();
for (RouteMeta routeMeta : groupData) {
RouteDoc routeDoc = extractDocInfo(routeMeta); ClassName className = ClassName.get((TypeElement) routeMeta.getRawType()); switch (routeMeta.getType()) {
case PROVIDER: // Need cache provider's super class
List<? extends TypeMirror> interfaces = ((TypeElement) routeMeta.getRawType()).getInterfaces();
for (TypeMirror tm : interfaces) {
routeDoc.addPrototype(tm.toString()); if (types.isSameType(tm, iProvider)) { // Its implements iProvider interface himself.
// This interface extend the IProvider, so it can be used for mark provider
loadIntoMethodOfProviderBuilder.addStatement(
"providers.put($S, $T.build($T." + routeMeta.getType() + ", $T.class, $S, $S, null, " + routeMeta.getPriority() + ", " + routeMeta.getExtra() + "))",
(routeMeta.getRawType()).toString(),
routeMetaCn,
routeTypeCn,
className,
routeMeta.getPath(),
routeMeta.getGroup());
} else if (types.isSubtype(tm, iProvider)) {
// This interface extend the IProvider, so it can be used for mark provider
loadIntoMethodOfProviderBuilder.addStatement(
"providers.put($S, $T.build($T." + routeMeta.getType() + ", $T.class, $S, $S, null, " + routeMeta.getPriority() + ", " + routeMeta.getExtra() + "))",
tm.toString(), // So stupid, will duplicate only save class name.
routeMetaCn,
routeTypeCn,
className,
routeMeta.getPath(),
routeMeta.getGroup());
}
}
break;
default:
break;
} // Make map body for paramsType
StringBuilder mapBodyBuilder = new StringBuilder();
Map<String, Integer> paramsType = routeMeta.getParamsType();
Map<String, Autowired> injectConfigs = routeMeta.getInjectConfig();
if (MapUtils.isNotEmpty(paramsType)) {
List<RouteDoc.Param> paramList = new ArrayList<>(); for (Map.Entry<String, Integer> types : paramsType.entrySet()) {
mapBodyBuilder.append("put(\"").append(types.getKey()).append("\", ").append(types.getValue()).append("); "); RouteDoc.Param param = new RouteDoc.Param();
Autowired injectConfig = injectConfigs.get(types.getKey());
param.setKey(types.getKey());
param.setType(TypeKind.values()[types.getValue()].name().toLowerCase());
param.setDescription(injectConfig.desc());
param.setRequired(injectConfig.required()); paramList.add(param);
} routeDoc.setParams(paramList);
}
String mapBody = mapBodyBuilder.toString(); loadIntoMethodOfGroupBuilder.addStatement(
"atlas.put($S, $T.build($T." + routeMeta.getType() + ", $T.class, $S, $S, " + (StringUtils.isEmpty(mapBody) ? null : ("new java.util.HashMap<String, Integer>(){{" + mapBodyBuilder.toString() + "}}")) + ", " + routeMeta.getPriority() + ", " + routeMeta.getExtra() + "))",
routeMeta.getPath(),
routeMetaCn,
routeTypeCn,
className,
routeMeta.getPath().toLowerCase(),
routeMeta.getGroup().toLowerCase()); routeDoc.setClassName(className.toString());
routeDocList.add(routeDoc);
} // 有几个组就生成了几个类ARouter$$Group$$groupName
String groupFileName = NAME_OF_GROUP + groupName;
JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
TypeSpec.classBuilder(groupFileName)
.addJavadoc(WARNING_TIPS)
.addSuperinterface(ClassName.get(type_IRouteGroup))
.addModifiers(PUBLIC)
.addMethod(loadIntoMethodOfGroupBuilder.build())
.build()
).build().writeTo(mFiler); logger.info(">>> Generated group: " + groupName + "<<<");
//填充rootMap key是组名 value是上面生成的类名
rootMap.put(groupName, groupFileName);
docSource.put(groupName, routeDocList);
}

经过这个遍历系统为我们生成了
public class ARouter$$Group$$组名 implement IRooteGroup{
public void loadInto(Map<String,RouteMata> atlas){
atlas.put("/order/OrderActivity", RouteMeta 对象)
atlas.put("/order/OrderDetailActivity", RouteMeta 对象)
     当外部调用这个方法的时候就会得到整个路由信息了
}
}

if (MapUtils.isNotEmpty(rootMap)) {
// Generate root meta by group name, it must be generated before root, then I can find out the class of group.
for (Map.Entry<String, String> entry : rootMap.entrySet()) {
loadIntoMethodOfRootBuilder.addStatement("routes.put($S, $T.class)", entry.getKey(), ClassName.get(PACKAGE_OF_GENERATE_FILE, entry.getValue()));
}
}

// Output route doc
if (generateDoc) {
docWriter.append(JSON.toJSONString(docSource, SerializerFeature.PrettyFormat));
docWriter.flush();
docWriter.close();
}

// 创建ARouter$$Providers$$模块名这个类 loadInto 方法中的Map key是整个类名 值是RouteMeta
String providerMapFileName = NAME_OF_PROVIDER + SEPARATOR + moduleName;
JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
TypeSpec.classBuilder(providerMapFileName)
.addJavadoc(WARNING_TIPS)
.addSuperinterface(ClassName.get(type_IProviderGroup))
.addModifiers(PUBLIC)
.addMethod(loadIntoMethodOfProviderBuilder.build())
.build()
).build().writeTo(mFiler);

logger.info(">>> Generated provider map, name is " + providerMapFileName + " <<<");

// 创建Arouter$$Route$$模块名 key是组名 值是 Arouter$$Group$$组名 对应的class
String rootFileName = NAME_OF_ROOT + SEPARATOR + moduleName;
JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
TypeSpec.classBuilder(rootFileName)
.addJavadoc(WARNING_TIPS)
.addSuperinterface(ClassName.get(elementUtils.getTypeElement(ITROUTE_ROOT)))
.addModifiers(PUBLIC)
.addMethod(loadIntoMethodOfRootBuilder.build())
.build()
).build().writeTo(mFiler);

经过上面的操作就生成了
public class ARouter$$Providers$$modulejava implements IProviderGroup {
@Override
public void loadInto(Map<String, RouteMeta> providers) {
providers.put("com.alibaba.android.arouter.demo.service.HelloService", RouteMeta.build(RouteType.PROVIDER, HelloServiceImpl.class, "/yourservicegroupname/hello", "yourservicegroupname", null, -1, -2147483648));
providers.put("com.alibaba.android.arouter.facade.service.SerializationService", RouteMeta.build(RouteType.PROVIDER, JsonServiceImpl.class, "/yourservicegroupname/json", "yourservicegroupname", null, -1, -2147483648));
providers.put("com.alibaba.android.arouter.demo.module1.testservice.SingleService", RouteMeta.build(RouteType.PROVIDER, SingleService.class, "/yourservicegroupname/single", "yourservicegroupname", null, -1, -2147483648));
}
}

public class ARouter$$Root$$modulejava implements IRouteRoot {
@Override
public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
routes.put("m2", ARouter$$Group$$m2.class);
routes.put("module", ARouter$$Group$$module.class);
routes.put("test", ARouter$$Group$$test.class);
routes.put("yourservicegroupname", ARouter$$Group$$yourservicegroupname.class);
}
}

至此我们也能发现ARouter$$Root$$模块名 和 ARouter$$Providers$$模块名 是有几个模块就会创建几个类,但是ARouter$$Group$$组名则是有几个组就创建几个类
并且ARouter$$Group$$组名这个类里面也会存储provider路由信息,ARouter$$Providers$$模块名也会存储provider信息。也就是说我们既可以通过path查找provider信息
也可以通过全类名查找provider信息。
 

至此 Route这个注解的功能就分析完毕,等待应用程序注册的时候调用我们上面创建类的方法,把数据存储到仓库中即可

Autowired这个注解比较简单就是利用反射调用我们的getIntent.get方法去赋值。

Interceptor这个注解用来帮助我们生成ARouter$$Interceptors$$modulejava这样的类
public class ARouter$$Interceptors$$modulejava implements IInterceptorGroup {
@Override
public void loadInto(Map<Integer, Class<? extends IInterceptor>> interceptors) {
//key 是优先级 值是我们添加的拦截器的class
interceptors.put(7, Test1Interceptor.class);
interceptors.put(90, TestInterceptor90.class);
}
}
当我们调用ARouter.init(application)的时候
1.启动线程池扫描包下面的class对象得到类的集合 有缓存不用每次都扫描
2.解析类集合如果是以ARouter$$Root开头的类利用class.forName获取IRouterRoot对象,调用loadInto方法把里面的Map赋值给数据仓库中的Map
填充:WareHouse中的Map<String,class<? extends IRoteGroup>> groupsIndex
3.以ARoute$$Interceptors开头就调用IInterceptors的loadInto方法填充WareHouse中的Map<Integer,class<? extends IRoteGroup>>interceptorsIndex
4.以ARoute$$Providers开头的就调用IProvidersGroup的loadInto方法填充WareHouse中的Map<String,RouteMeta>providersIndex
这个时候肯定会有疑问:我们生成的ARouter$$Group$$组名中的loadInto方法怎么没有调用?
for (String className : routerMap) {
if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {
// This one of root elements, load root.
((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);
} else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_INTERCEPTORS)) {
// Load interceptorMeta
((IInterceptorGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.interceptorsIndex);
} else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_PROVIDERS)) {
// Load providerIndex
((IProviderGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.providersIndex);
}
}

初始化完成后调用了下面的这个方法
static void afterInit() {
// Trigger interceptor init, use byName.
interceptorService = (InterceptorService) ARouter.getInstance().build("/arouter/service/interceptor").navigation();
}
protected Postcard build(String path) {
if (TextUtils.isEmpty(path)) {
throw new HandlerException(Consts.TAG + "Parameter is invalid!");
} else {
//给我们暴露了一个可以预处理事情的PathReplaceService这个IProvider接口,如果我们实现了这个接口就可以提前做一些事情主要是路径替换--一个hook点吧
        PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
if (null != pService) {
path = pService.forString(path);
}
return build(path, extractGroup(path), true);
}
}

@Route(path = "/arouter/service/interceptor")
public class InterceptorServiceImpl implements InterceptorService 这个系统的拦截器里面利用线程池处理我们自己的拦截器
 protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
//这个同样是一个预处理点如果我们设置了这个provider,就可以截获并取消这个跳转
PretreatmentService pretreatmentService = ARouter.getInstance().navigation(PretreatmentService.class);
if (null != pretreatmentService && !pretreatmentService.onPretreatment(context, postcard)) {
// Pretreatment failed, navigation canceled.
return null;
} // Set context to postcard.
postcard.setContext(null == context ? mContext : context); try {
//这个里面完成了对postcard信息的完善:路径class 是否是绿色通道等,数据仓库里面剩余map的填充
LogisticsCenter.completion(postcard);
} catch (NoRouteFoundException ex) {
logger.warning(Consts.TAG, ex.getMessage()); if (debuggable()) {
// Show friendly tips for user.
runInMainThread(new Runnable() {
@Override
public void run() {
Toast.makeText(mContext, "There's no route matched!\n" +
" Path = [" + postcard.getPath() + "]\n" +
" Group = [" + postcard.getGroup() + "]", Toast.LENGTH_LONG).show();
}
});
}
//异常发生后会回调我们的丢失方法
if (null != callback) {
callback.onLost(postcard);
} else {
// No callback for this invoke, then we use the global degrade service.
//如果没有设置回调还可以通过实现下面的接口来实现回调,这个是全局的
DegradeService degradeService = ARouter.getInstance().navigation(DegradeService.class);
if (null != degradeService) {
degradeService.onLost(context, postcard);
}
} return null;
} if (null != callback) {
callback.onFound(postcard);
}
//只有非绿色通道才可以进行拦截处理
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(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());
}
});
} else {
//看下真正的路由实现
return _navigation(postcard, requestCode, callback);
} return null;
}
    private Object _navigation(final Postcard postcard, final int requestCode, final NavigationCallback callback) {
final Context currentContext = postcard.getContext(); switch (postcard.getType()) {
case ACTIVITY:
// Build intent
final Intent intent = new Intent(currentContext, postcard.getDestination());
intent.putExtras(postcard.getExtras()); // Set flags.
int flags = postcard.getFlags();
if (0 != flags) {
intent.setFlags(flags);
} // Non activity, need FLAG_ACTIVITY_NEW_TASK
if (!(currentContext instanceof Activity)) {
intent.addFlags(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;
case PROVIDER:
return postcard.getProvider();
case BOARDCAST:
case CONTENT_PROVIDER:
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) {
logger.error(Consts.TAG, "Fetch fragment instance error, " + TextUtils.formatStackTrace(ex.getStackTrace()));
}
case METHOD:
case SERVICE:
default:
return null;
} return null;
}


  

ARouter源码分析的相关教程结束。

《ARouter源码分析.doc》

下载本文的Word格式文档,以方便收藏与打印。