当前位置:网站首页>Arouter source code analysis (I)
Arouter source code analysis (I)
2022-07-28 09:39:00 【Yu Qirong】
arouter-api version : 1.4.1
Preface
Previous pair ActivityRouter I did an analysis of the source code of , I believe you have a general understanding of the routing framework .
And today I'll give you an analysis ARouter . Everyone is in the process of Componentization of the project , Probably most developers will use ARouter As the routing framework of the project . After all ARouter It's made by Ali , Naturally, there is no need to say more about the advantages .
So in the process of ordinary use , Not just to be able to use , We need to know more about it ARouter Internal principle .
This time ARouter The analysis of is divided into three parts :
- Yes IRouteRoot Page Jump for source code analysis ;
- Yes IInterceptorGroup Interceptor for source code analysis ;
- Yes IProviderGroup Source code analysis of service components ;
This is ARouter The first in the series , Now it's right IRouteRoot Page Jump for detailed analysis .
ARouter Source code
Use ARouter When , All need to be initialized
if (isDebug()) {
ARouter.openLog();
ARouter.openDebug();
}
ARouter.init(mApplication);The entrance of source code analysis , It's just ARouter.init in
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.");
}
}You can see from the source code ,ARouter The inside of is actually _ARouter It's working ,ARouter Just put _ARouter Another layer of packaging . Then we will follow up _ARouter Of init Method .
protected static synchronized boolean init(Application application) {
mContext = application;
LogisticsCenter.init(mContext, executor);
logger.info(Consts.TAG, "ARouter init success!");
hasInit = true;
mHandler = new Handler(Looper.getMainLooper());
return true;
} The most important code is LogisticsCenter.init(mContext, executor) , among executor Thread pool .
So here comes the question , LogisticsCenter What is it for ?
* LogisticsCenter contains all of the map.
*
* 1. Creates instance when it is first used.
* 2. Handler Multi-Module relationship map(*)
* 3. Complex logic to solve duplicate group definitionAccording to official notes ,LogisticsCenter It contains all the mappings , Deal with cross module mapping and matching routes .
So according to the previous ActivityRouter Experience guessed ,LogisticsCenter Of init Method inside , I will definitely load the route , And build relationships .
public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
mContext = context;
executor = tpe;
try {
long startInit = System.currentTimeMillis();
//billy.qi modified at 2017-12-06
//load by plugin first
loadRouterMap();
if (registerByPlugin) {
logger.info(TAG, "Load router map by arouter-auto-register plugin.");
} else {
Set<String> routerMap;
// If it is debug Or the new version , Will reload the routing map
if (ARouter.debuggable() || PackageUtils.isNewVersion(context)) {
logger.info(TAG, "Run with debug mode or new install, rebuild router map.");
// Load routing map
routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
// Save all route mappings to SharedPreferences
if (!routerMap.isEmpty()) {
context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).edit().putStringSet(AROUTER_SP_KEY_MAP, routerMap).apply();
}
// Save the new version number to sharedpreference
PackageUtils.updateVersion(context); // Save new version name when router map update finishes.
} else {
// Otherwise, it will be from SharedPreferences Read all previously saved route maps in
logger.info(TAG, "Load router map from cache.");
routerMap = new HashSet<>(context.getSharedPreferences(AROUTER_SP_CACHE_KEY, Context.MODE_PRIVATE).getStringSet(AROUTER_SP_KEY_MAP, new HashSet<String>()));
}
logger.info(TAG, "Find router map finished, map size = " + routerMap.size() + ", cost " + (System.currentTimeMillis() - startInit) + " ms.");
startInit = System.currentTimeMillis();
// According to ClassName Divided into three , Register separately
// IRouteRoot Page Jump
// IInterceptorGroup Interceptor
// IProviderGroup Service components
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);
}
}
}
...
} catch (Exception e) {
throw new HandlerException(TAG + "ARouter init logistics center exception! [" + e.getMessage() + "]");
}
}LogisticsCenter Of init Basically, the code of the method can be understood , among ClassUtils.getFileNameByPackageName It's something we need to explore . The main thing this code does is start from dex Middle traversal class find arouter-compiler Generated class collection . We will talk about the specific analysis later , Here is a foreshadowing .
So let's look down , We know ,routerMap Medium className All are arouter-compiler Generated at compile time , Let's first see what the generated class looks like
/**
* DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
public class ARouter$$Root$$app implements IRouteRoot {
@Override
public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
routes.put("test", ARouter$$Group$$test.class);
routes.put("yourservicegroupname", ARouter$$Group$$yourservicegroupname.class);
}
}ARouter The route of will be loaded in groups , For example, there is /test/abc and /test/def Two routes , Then they all belong to /test This group . So in Warehouse.groupsIndex Stored in key Is the routing group name ,value Is the corresponding group routing class . The route is also found according to the group name key , Then find the group routing class value Find the matching route in .
/**
* DO NOT EDIT THIS FILE!!! IT WAS GENERATED BY AROUTER. */
public class ARouter$$Group$$test implements IRouteGroup {
@Override
public void loadInto(Map<String, RouteMeta> atlas) {
atlas.put("/test/activity1", RouteMeta.build(RouteType.ACTIVITY, Test1Activity.class, "/test/activity1", "test", new java.util.HashMap<String, Integer>(){{put("ser", 9); put("ch", 5); put("fl", 6); put("dou", 7); put("boy", 0); put("url", 8); put("pac", 10); put("obj", 11); put("name", 8); put("objList", 11); put("map", 11); put("age", 3); put("height", 3); }}, -1, -2147483648));
atlas.put("/test/activity2", RouteMeta.build(RouteType.ACTIVITY, Test2Activity.class, "/test/activity2", "test", new java.util.HashMap<String, Integer>(){{put("key1", 8); }}, -1, -2147483648));
atlas.put("/test/activity3", RouteMeta.build(RouteType.ACTIVITY, Test3Activity.class, "/test/activity3", "test", new java.util.HashMap<String, Integer>(){{put("name", 8); put("boy", 0); put("age", 3); }}, -1, -2147483648));
atlas.put("/test/activity4", RouteMeta.build(RouteType.ACTIVITY, Test4Activity.class, "/test/activity4", "test", null, -1, -2147483648));
atlas.put("/test/fragment", RouteMeta.build(RouteType.FRAGMENT, BlankFragment.class, "/test/fragment", "test", null, -1, -2147483648));
atlas.put("/test/webview", RouteMeta.build(RouteType.ACTIVITY, TestWebview.class, "/test/webview", "test", null, -1, -2147483648));
}
}You can see that the route related parameter configuration is constructed into a RouteMeta object .RouteMeta Class is as follows :
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.
...
}Come here , The part of loading routes is finished , The rest is to jump the route .
The usual operation of jump route :
ARouter.getInstance().build("/test/abc").navigation();Let's have a look ARouter Of build Method
public Postcard build(String path) {
return _ARouter.getInstance().build(path);
}It calls _ARouter Of build Method .
protected Postcard build(String path) {
if (TextUtils.isEmpty(path)) {
throw new HandlerException(Consts.TAG + "Parameter is invalid!");
} else {
// obtain PathReplaceService example , If it's not empty , Will deal with path
PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
if (null != pService) {
path = pService.forString(path);
}
return build(path, extractGroup(path));
}
}
/**
* Intercept the first segment in the jump path as the group name
*/
private String extractGroup(String path) {
if (TextUtils.isEmpty(path) || !path.startsWith("/")) {
throw new HandlerException(Consts.TAG + "Extract the default group failed, the path must be start with '/' and contain more than 2 '/'!");
}
try {
String defaultGroup = path.substring(1, path.indexOf("/", 1));
if (TextUtils.isEmpty(defaultGroup)) {
throw new HandlerException(Consts.TAG + "Extract the default group failed! There's nothing between 2 '/'!");
} else {
return defaultGroup;
}
} catch (Exception e) {
logger.warning(Consts.TAG, "Failed to extract default group! " + e.getMessage());
return null;
}
}PathReplaceService It's the opening reserved by the government for us , Used to correct path Preprocessing . If you have a need, come to path Do unified pretreatment , Then directly realize PathReplaceService that will do .
Let's follow up , look down _ARouter.build(String path, String group) Method
protected Postcard build(String path, String group) {
if (TextUtils.isEmpty(path) || TextUtils.isEmpty(group)) {
throw new HandlerException(Consts.TAG + "Parameter is invalid!");
} else {
PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
if (null != pService) {
path = pService.forString(path);
}
return new Postcard(path, group);
}
} Found in build(String path, String group) Created directly in Postcard Object and return .Postcard Class inherits RouteMeta , Some additional information has been added .
With Postcard after , Call directly navigation To jump .
public Object navigation(Context context, NavigationCallback callback) {
return ARouter.getInstance().navigation(context, this, -1, callback);
}
public void navigation(Activity mContext, int requestCode, NavigationCallback callback) {
ARouter.getInstance().navigation(mContext, this, requestCode, callback);
}Postcard All of the navigation Methods will eventually call ARouter Of navigation This method .
public Object navigation(Context mContext, Postcard postcard, int requestCode, NavigationCallback callback) {
return _ARouter.getInstance().navigation(mContext, postcard, requestCode, callback);
}And then it does _ARouter.navigation
protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
try {
// take postcard Match with the routing table , And fill in postcard The data of
LogisticsCenter.completion(postcard);
} catch (NoRouteFoundException ex) {
logger.warning(Consts.TAG, ex.getMessage());
// If debug , It shows a matching error
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();
}
});
}
// Callback route matching failed
if (null != callback) {
callback.onLost(postcard);
} else {
// No callback for this invoke, then we use the global degrade service.
// If there is no callback , Call the strategy of global degradation
DegradeService degradeService = ARouter.getInstance().navigation(DegradeService.class);
if (null != degradeService) {
degradeService.onLost(context, postcard);
}
}
return null;
}
// Callback route matching succeeded
if (null != callback) {
callback.onFound(postcard);
}
// If it's not the green channel , Just call the interceptor , Interceptors will be discussed separately later , I won't talk about it here
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());
}
});
} else {
// Otherwise, call _navigation To jump
return _navigation(context, postcard, requestCode, callback);
}
return null;
}
private Object _navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
final Context currentContext = null == context ? mContext : context;
switch (postcard.getType()) {
case ACTIVITY: // If it is activity Of , Perform jump
// Build intent
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)) { // Non activity, need less one flag.
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;
case PROVIDER: // If it is a service component , Then return to the component directly
return postcard.getProvider();
case BOARDCAST:
case CONTENT_PROVIDER:
case FRAGMENT: // If it is fragment Words , Return to the fragment Example
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;
}The above code is basically annotated , You should be able to read this .
Let's focus on LogisticsCenter.completion(postcard);
public synchronized static void completion(Postcard postcard) {
if (null == postcard) {
throw new NoRouteFoundException(TAG + "No postcard!");
}
// First, according to path To get RouteMeta
RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());
if (null == routeMeta) { // If routeMeta It's empty , It may not exist or it may not be loaded
Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(postcard.getGroup()); // Load the routing map under the packet
// If it doesn't exist , Just report a mistake
if (null == groupMeta) {
throw new NoRouteFoundException(TAG + "There is no route match the path [" + postcard.getPath() + "], in group [" + postcard.getGroup() + "]");
} else {
// Load route and cache it into memory, then delete from metas.
try {
if (ARouter.debuggable()) {
logger.debug(TAG, String.format(Locale.getDefault(), "The group [%s] starts loading, trigger by [%s]", postcard.getGroup(), postcard.getPath()));
}
// Implement on-demand loading
IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance();
iGroupInstance.loadInto(Warehouse.routes);
// remove groupsIndex , Otherwise, it will cause a dead cycle
Warehouse.groupsIndex.remove(postcard.getGroup());
if (ARouter.debuggable()) {
logger.debug(TAG, String.format(Locale.getDefault(), "The group [%s] has already been loaded, trigger by [%s]", postcard.getGroup(), postcard.getPath()));
}
} catch (Exception e) {
throw new HandlerException(TAG + "Fatal exception when loading group meta. [" + e.getMessage() + "]");
}
// Reload
completion(postcard); // Reload
}
} else {
// Find the corresponding routeMeta, fill postcard data
postcard.setDestination(routeMeta.getDestination());
postcard.setType(routeMeta.getType());
postcard.setPriority(routeMeta.getPriority());
postcard.setExtra(routeMeta.getExtra());
Uri rawUri = postcard.getUri();
// If rawUri Not empty , It is uri Jump . Just analyze rawUri Parameters in , Put in bundle in
if (null != rawUri) { // Try to set params into bundle.
Map<String, String> resultMap = TextUtils.splitQueryParameters(rawUri);
Map<String, Integer> paramsType = routeMeta.getParamsType();
if (MapUtils.isNotEmpty(paramsType)) {
// Set value by its type, just for params which annotation by @Param
for (Map.Entry<String, Integer> params : paramsType.entrySet()) {
setValue(postcard,
params.getValue(),
params.getKey(),
resultMap.get(params.getKey()));
}
// Save params name which need auto inject.
postcard.getExtras().putStringArray(ARouter.AUTO_INJECT, paramsType.keySet().toArray(new String[]{}));
}
// Save raw uri
postcard.withString(ARouter.RAW_URI, rawUri.toString());
}
// If it is PROVIDER and FRAGMENT Type of , Open green channel
switch (routeMeta.getType()) {
case PROVIDER: // if the route is provider, should find its instance
// Its provider, so it must implement IProvider
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(); // Fragment needn't interceptors
default:
break;
}
}
}thus , The whole process of route jump is over , The general process can be divided into
- Load routing map
- according to path Construct out Postcard object
- distinguish Postcard Of type To make a jump
Off the coast
As I said before ,ARouter Will be in dex Search for arouter-compiler The generated class . Let's finally see how it is achieved .
public static Set<String> getFileNameByPackageName(Context context, final String packageName) throws PackageManager.NameNotFoundException, IOException, InterruptedException {
final Set<String> classNames = new HashSet<>();
// obtain dex Path of file storage
List<String> paths = getSourcePaths(context);
final CountDownLatch parserCtl = new CountDownLatch(paths.size());
// Traverse all of dex Path to file
for (final String path : paths) {
DefaultPoolExecutor.getInstance().execute(new Runnable() {
@Override
public void run() {
DexFile dexfile = null;
try {
// Load according to the path dex file
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);
}
// Traverse dexfile All in className If it is arouter At the beginning of the registration , Join in classNames in
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;
}Let's see getSourcePaths Method , See how it finds dex File path
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;
// If VM Has supported MultiDex, Don't go Secondary Folder load Classesx.zip 了 , There is no such thing as
// Through the existence of sp Medium multidex.version It's not accurate , Because users upgrading from a lower version , It includes this sp Configured
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() + "'");
}
}
}
// If it is debug Of , Then load it extra instant run Medium dex File path
if (ARouter.debuggable()) { // Search instant run support only debuggable
sourcePaths.addAll(tryLoadInstantRunDexFile(applicationInfo));
}
return sourcePaths;
}For more details, interested students can go back and see it by themselves , Because of the space, I don't want to talk more about this .
So that's the end of today , About ARouter Series of more source code analysis , You can see the next two blogs .
bye
边栏推荐
- 【多线程】println方法底层原理
- [log] what does a log do? What is a log factory? Configuration and use of log4j? log4j. Properties file configuration, log4j jar package coordinates
- FPGA开发学习开源网站汇总
- RGB-T追踪——【多模态融合】Visible-Thermal UAV Tracking: A Large-Scale Benchmark and New Baseline
- Pytorch deep learning practice lesson 9 multi classification problems (handwritten numeral MNIST)
- Regular expressions for positive and negative values
- ArrayList内部原理解析
- 技术分享| 快对讲综合调度系统
- Alibaba cloud server setup and pagoda panel connection
- 2022 high voltage electrician examination simulated 100 questions and simulated examination
猜你喜欢

Opencv4.60 installation and configuration

JS array is de duplicated, the ID is the same, and a value is added and merged

【日志】日志干什么的?日志工厂是什么?log4j 的配置和使用? log4j.properties 文件配置、log4j jar包坐标

Title and answer of work permit for safety management personnel of hazardous chemical business units in 2022

MQTT.js 入门教程:学习笔记

使用Xposed对软件进行破解

Alibaba cloud server setup and pagoda panel connection

go语言切片Slice和数组Array对比panic runtime error index out of range问题解决

正负数值的正则表达式

What is cross domain? How to solve the cross domain problem?
随机推荐
《数据库系统内 幕》分布式系统
数据泄漏、删除事件频发,企业应如何构建安全防线?
IntelliJ idea associated database
[autosar-rte] - 3-runnable and its task mapping mapping
MATLAB的实时编辑器
[C language] detailed explanation sequence table (seqlist)
Window源码解析(一):与DecorView的那些事
JS array is de duplicated, the ID is the same, and a value is added and merged
QT基础练手小程序-简单计算器设计(附带源码,解析)
2022 safety officer-b certificate examination simulated 100 questions and answers
Technology sharing | quick intercom integrated dispatching system
Window源码解析(二):Window的添加机制
Detailed introduction of v-bind instruction
Source code analysis of view event distribution mechanism
c# 有符号和无符号字节变量
MQTT.js 入门教程:学习笔记
ARouter源码解析(二)
ECCV 2022 | can be promoted without fine adjustment! Registration based anomaly detection framework for small samples
【杂谈】程序员的发展最需要两点能力
[JVM] JVM refers to floating point number