Android自动化页面测速在美团的实践-程序员宅基地

技术标签: 程序员  android  php  自动化  

SDK初始化

SDK的初始化在 Application.onCreate() 中调用,初始化时会获取服务端的配置文件,解析为 Map<String,PageObject> ,对应配置中页面的id和其配置项。另外还维护了一个当前页面对象的 MAP<Integer, Object> ,key为一个int值而不是其类名,因为同一个类可能有多个实例同时在运行,如果存为一个key,可能会导致同一页面不同实例的测速对象只有一个,所以在这里我们使用Activity或Fragment的 hashcode() 值作为页面的唯一标识。

页面开始时间

页面的开始时间,我们以Activtiy或Fragment的 onCreate() 作为时间节点进行计算,记录页面的开始时间。

public void onPageCreate(Object page) {
int pageObjKey = Utils.getPageObjKey(page);
PageObject pageObject = activePages.get(pageObjKey);
ConfigModel configModel = getConfigModel(page);//获取该页面的配置
if (pageObject == null && configModel != null) {//有配置则需要测速
pageObject = new PageObject(pageObjKey, configModel, Utils.getDefaultReportKey(page), callback);
pageObject.onCreate();
activePages.put(pageObjKey, pageObject);
}
}
//PageObject.onCreate()
void onCreate() {
if (createTime > 0) {
return;
}
createTime = Utils.getRealTime();
}

这里的 getConfigModel() 方法中,会使用页面的类名或者全路径类名,去初始化时解析的配置Map中进行id的匹配,如果匹配到说明页面需要测速,就会创建测速对象 PageObject 进行测速。

网络请求时间

一个页面的初始请求由配置文件指定,我们只需在第一个请求发起前记录请求开始时间,在最后一个请求回来后记录结束时间即可。

boolean onApiLoadStart(String url) {
String relUrl = Utils.getRelativeUrl(url);
if (!hasApiConfig() || !hasUrl(relUrl) || apiStatusMap.get(relUrl.hashCode()) != NONE) {
return false;
}
//改变Url的状态为执行中
apiStatusMap.put(relUrl.hashCode(), LOADING);
//第一个请求开始时记录起始点
if (apiLoadStartTime <= 0) {
apiLoadStartTime = Utils.getRealTime();
}
return true;
}
boolean onApiLoadEnd(String url) {
String relUrl = Utils.getRelativeUrl(url);
if (!hasApiConfig() || !hasUrl(relUrl) || apiStatusMap.get(relUrl.hashCode()) != LOADING) {
return false;
}
//改变Url的状态为执行结束
apiStatusMap.put(relUrl.hashCode(), LOADED);
//全部请求结束后记录时间
if (apiLoadEndTime <= 0 && allApiLoaded()) {
apiLoadEndTime = Utils.getRealTime();
}
return true;
}
private boolean allApiLoaded() {
if (!hasApiConfig()) return true;
int size = apiStatusMap.size();
for (int i = 0; i < size; ++i) {
if (apiStatusMap.valueAt(i) != LOADED) {
return false;
}
}
return true;
}

每个页面的测速对象,维护了一个请求url和其状态的映射关系 SparseIntArray ,key就为请求url的hashcode,状态初始为 NONE 。每次请求发起时,将对应url的状态置为 LOADING ,结束时置为 LOADED 。当第一个请求发起时记录起始时间,当所有url状态为 LOADED 时说明所有请求完成,记录结束时间。

渲染时间

按照我们对测速的定义,现在冷启动开始时间有了,还差结束时间,即指定的首页初次渲染结束时的时间;页面的开始时间有了,还差页面初次渲染的结束时间;网络请求的结束时间有了,还差页面的二次渲染的结束时间。这一切都是和页面的View渲染时间有关,那么怎么获取页面的渲染结束时间点呢?

由View的绘制流程可知,父View的 dispatchDraw() 方法会执行其所有子View的绘制过程,那么把页面的根View当做子View,是不是可以在其外部增加一层父View,以其 dispatchDraw() 作为页面绘制完毕的时间点呢?答案是可以的。

class AutoSpeedFrameLayout extends FrameLayout {
public static View wrap(int pageObjectKey, @NonNull View child) {

//将页面根View作为子View,其他参数保持不变
ViewGroup vg = new AutoSpeedFrameLayout(child.getContext(), pageObjectKey);
if (child.getLayoutParams() != null) {
vg.setLayoutParams(child.getLayoutParams());
}
vg.addView(child, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
return vg;
}
private final int pageObjectKey;//关联的页面key
private AutoSpeedFrameLayout(@NonNull Context context, int pageObjectKey) {
super(context);
this.pageObjectKey = pageObjectKey;
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
AutoSpeed.getInstance().onPageDrawEnd(pageObjectKey);
}
}

我们自定义了一层 FrameLayout 作为所有页面根View的父View,其 dispatchDraw() 方法执行super后,记录相关页面绘制结束的时间点。

测速完成

现在所有时间点都有了,那么什么时候算作测速过程结束呢?我们来看看每次渲染结束后的处理就知道了。

//PageObject.onPageDrawEnd()
void onPageDrawEnd() {
if (initialDrawEndTime <= 0) {//初次渲染还没有完成
initialDrawEndTime = Utils.getRealTime();
if (!hasApiConfig() || allApiLoaded()) {//如果没有请求配置或者请求已完成,则没有二次渲染时间,即初次渲染时间即为页面整体时间,且可以上报结束页面了
finalDrawEndTime = -1;
reportIfNeed();
}
//页面初次展示,回调,用于统计冷启动结束
callback.onPageShow(this);
return;
}
//如果二次渲染没有完成,且所有请求已经完成,则记录二次渲染时间并结束测速,上报数据
if (finalDrawEndTime <= 0 && (!hasApiConfig() || allApiLoaded())) {
finalDrawEndTime = Utils.getRealTime();
reportIfNeed();
}
}

该方法用于处理渲染完毕的各种情况,包括初次渲染时间、二次渲染时间、冷启动时间以及相应的上报。这里的冷启动在 callback.onPageShow(this) 是如何处理的呢?

//初次渲染完成时的回调
void onMiddlePageShow(boolean isMainPage) {
if (!isFinish && isMainPage && startTime > 0 && endTime <= 0) {
endTime = Utils.getRealTime();
callback.onColdStartReport(this);
finish();
}
}

还记得配置文件中 tag 么,他的作用就是指明该页面是否为首页,也就是代码段里的 isMainPage 参数。如果是首页的话,说明首页的初次渲染结束,就可以计算冷启动结束的时间并进行上报了。

上报数据

当测速完成后,页面测速对象 PageObject 里已经记录了页面(包括冷启动)各个时间点,剩下的只需要进行测速阶段的计算并进行网络上报即可。

//计算网络请求时间
long getApiLoadTime() {
if (!hasApiConfig() || apiLoadEndTime <= 0 || apiLoadStartTime <= 0) {
return -1;
}
return apiLoadEndTime - apiLoadStartTime;
}

自动化实现

有了SDK,就要在我们的项目中接入,并在相应的位置调用SDK的API来实现测速功能,那么如何自动化实现API的调用呢?答案就是采用AOP的方式,在App编译时动态注入代码,我们 实现一个Gradle插件,利用其Transform功能以及Javassist实现代码的动态注入 。动态注入代码分为以下几步:

  • 初始化埋点:SDK的初始化。
  • 冷启动埋点:Application的冷启动开始时间点。
  • 页面埋点:Activity和Fragment页面的时间点。
  • 请求埋点:网络请求的时间点。

初始化埋点

在 Transform 中遍历所有生成的class文件,找到Application对应的子类,在其 onCreate() 方法中调用SDK初始化API即可。

CtMethod method = it.getDeclaredMethod(“onCreate”)
method.insertBefore(“${Constants.AUTO_SPEED_CLASSNAME}.getInstance().init(this);”)

最终生成的Application代码如下:

public void onCreate() {

AutoSpeed.getInstance().init(this);
}

冷启动埋点

同上一步,找到Application对应的子类,在其构造方法中记录冷启动开始时间,在SDK初始化时候传入SDK,原因在上文已经解释过。

//Application
private long coldStartTime;
public MobileCRMApplication() {
coldStartTime = SystemClock.elapsedRealtime();
}
public void onCreate(){

AutoSpeed.getInstance().init(this,coldStartTime);
}

页面埋点

结合测速时间点的定义以及Activity和Fragment的生命周期,我们能够确定在何处调用相应的API。

Activity

对于Activity页面,现在开发者已经很少直接使用 android.app.Activity 了,取而代之的是 android.support.v4.app.FragmentActivity 和 android.support.v7.app.AppCompatActivity ,所以我们只需在这两个基类中进行埋点即可,我们先来看FragmentActivity。

protected void onCreate(@Nullable Bundle savedInstanceState) {
AutoSpeed.getInstance().onPageCreate(this);

}
public void setContentView(View var1) {
super.setContentView(AutoSpeed.getInstance().createPageView(this, var1));
}

注入代码后,在FragmentActivity的 onCreate 一开始调用了 onPageCreate() 方法进行了页面开始时间点的计算;在 setContentView() 内部,直接调用super,并将页面根View包装在我们自定义的 AutoSpeedFrameLayout 中传入,用于渲染时间点的计算。

然而在AppCompatActivity中,重写了setContentView()方法,且没有调用super,调用的是 AppCompatDelegate 的相应方法。

public void setContentView(View view) {
getDelegate().setContentView(view);
}

这个delegate类用于适配不同版本的Activity的一些行为,对于setContentView,无非就是将根View传入delegate相应的方法,所以我们可以直接包装View,调用delegate相应方法并传入即可。

public void setContentView(View view) {
AppCompatDelegate var2 = this.getDelegate();
var2.setContentView(AutoSpeed.getInstance().createPageView(this, view));
}

对于Activity的setContentView埋点需要注意的是,该方法是重载方法,我们需要对每个重载的方法做处理。

Fragment

Fragment的 onCreate() 埋点和Activity一样,不必多说。这里主要说下 onCreateView() ,这个方法是返回值代表根View,而不是直接传入View,而Javassist无法单独修改方法的返回值,所以无法像Activity的setContentView那样注入代码,并且这个方法不是 @CallSuper 的,意味着不能在基类里实现。那么怎么办呢?我们决定在每个Fragment的该方法上做一些事情。

//Fragment标志位
protected static boolean AUTO_SPEED_FRAGMENT_CREATE_VIEW_FLAG = true;
//利用递归包装根View
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
if(AUTO_SPEED_FRAGMENT_CREATE_VIEW_FLAG) {
AUTO_SPEED_FRAGMENT_CREATE_VIEW_FLAG = false;
View var4 = AutoSpeed.getInstance().createPageView(this, this.onCreateView(inflater, container, savedInstanceState));
AUTO_SPEED_FRAGMENT_CREATE_VIEW_FLAG = true;
return var4;
} else {

return rootView;
}
}

我们利用一个boolean类型的标志位,进行递归调用 onCreateView() 方法:

AutoSpeedFrameLayout





并且由于标志位为false,所以在递归调用时,即使调用了 super.onCreateView() 方法,在父类的该方法中也不会走if分支,而是直接返回其根View。

**请求埋点**

关于请求埋点我们针对不同的网络框架进行不同的处理,插件中只需要配置使用了哪些网络框架即可实现埋点,我们拿现在用的最多的 Retrofit 框架来说。

开始时间点

在创建Retrofit对象时,需要 OkHttpClient 对象,可以为其添加 Interceptor 进行请求发起前 Request 的拦截,我们可以构建一个用于记录请求开始时间点的Interceptor,在 OkHttpClient.Builder() 调用时,插入该对象。




public Builder() {
this.addInterceptor(new AutoSpeedRetrofitInterceptor());
...
}





而该Interceptor对象就是用于在请求发起前,进行请求开始时间点的记录。




public class AutoSpeedRetrofitInterceptor implements Interceptor {
public Response intercept(Chain var1) throws IOException {
AutoSpeed.getInstance().onApiLoadStart(var1.request().url());
return var1.proceed(var1.request());
}
}





结束时间点

使用Retrofit发起请求时,我们会调用其 enqueue() 方法进行异步请求,同时传入一个 Callback 进行回调,我们可以自定义一个Callback,用于记录请求回来后的时间点,然后在enqueue方法中将参数换为自定义的Callback,而原Callback作为其代理对象即可。



public void enqueue(Callback callback) {
final Callback callback = new AutoSpeedRetrofitCallback(callback);
...
}





该Callback对象用于在请求成功或失败回调时,记录请求结束时间点,并调用代理对象的相应方法处理原有逻辑。




public class AutoSpeedRetrofitCallback implements Callback {
private final Callback delegate;
public AutoSpeedRetrofitMtCallback(Callback var1) {
this.delegate = var1;
}
public void onResponse(Call var1, Response var2) {
AutoSpeed.getInstance().onApiLoadEnd(var1.request().url());
this.delegate.onResponse(var1, var2);
}
public void onFailure(Call var1, Throwable var2) {
AutoSpeed.getInstance().onApiLoadEnd(var1.request().url());
this.delegate.onFailure(var1, var2);
}
}





使用Retrofit+RXJava时,发起请求时内部是调用的 execute() 方法进行同步请求,我们只需要在其执行前后插入计算时间的代码即可,此处不再赘述。

**疑难杂症**

至此,我们基本的测速框架已经完成,不过经过我们的实践发现,有一种情况下测速数据会非常不准,那就是开头提过的ViewPager+Fragment并且实现延迟加载的情况。这也是一种很常见的情况,通常是为了节省开销,在切换ViewPager的Tab时,才首次调用Fragment的初始加载方法进行数据请求。经过调试分析,我们找到了问题的原因。

**等待切换时间**

![](https://img-blog.csdnimg.cn/img_convert/484ee8c347bde2b1e45d38b5d32d0b53.webp?x-oss-process=image/format,png)

该图红色时间段反映出,直到ViewPager切换到Fragment前,Fragment不会发起请求,这段等待的时间就会延长整个页面的加载时间,但其实这块时间不应该算在内,因为这段时间是用户无感知的,不能作为页面耗时过长的依据。

那么如何解决呢?我们都知道ViewPager的Tab切换是可以通过一个 OnPageChangeListener 对象进行监听的,所以我们可以为ViewPager添加一个自定义的Listener对象,在切换时记录一个时间,这样可以通过用这个时间减去页面创建后的时间得出这个多余的等待时间,上报时在总时间中减去即可。




public ViewPager(Context context) {
...
this.addOnPageChangeListener(new AutoSpeedLazyLoadListener(this.mItems));
}





mItems 是ViewPager中当前页面对象的数组,在Listener中可以通过他找到对应的页面,进行切换时的埋点。




//AutoSpeedLazyLoadListener
public void onPageSelected(int var1) {
if(this.items != null) {
int var2 = this.items.size();
for(int var3 = 0; var3 < var2; ++var3) {
Object var4 = this.items.get(var3);
if(var4 instanceof ItemInfo) {
ItemInfo var5 = (ItemInfo)var4;
if(var5.position == var1 && var5.object instanceof Fragment) {
AutoSpeed.getInstance().onPageSelect(var5.object);
break;
}
}
}
}
}





AutoSpeed的 onPageSelected() 方法记录页面的切换时间。这样一来,在计算页面加载速度总时间时,就要减去这一段时间。




long getTotalTime() {
if (createTime <= 0) {
return -1;
}
if (finalDrawEndTime > 0) {//有二次渲染时间
long totalTime = finalDrawEndTime - createTime;
//如果有等待时间,则减掉这段多余的时间
if (selectedTime > 0 && selectedTime > viewCreatedTime && selectedTime < finalDrawEndTime) {
totalTime -= (selectedTime - viewCreatedTime);
}
return totalTime;
} else {//以初次渲染时间为整体时间
return getInitialDrawTime();
**自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。**

**深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!**

**因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。**

![img](https://img-blog.csdnimg.cn/img_convert/4fc8d8023948d24e029644e40620797c.png)

![img](https://img-blog.csdnimg.cn/img_convert/661e62a3e278b9ef099a37c0ba6ce9e6.png)

![img](https://img-blog.csdnimg.cn/img_convert/089740d0def63314ca2e0096e0a38716.png)

![img](https://img-blog.csdnimg.cn/img_convert/a36a741eb72a1ca890ac1ce8fbc4e4b0.png)

![](https://img-blog.csdnimg.cn/img_convert/514ba1ffd5c5b854dc16efc836d1bddf.png)

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!**

**由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!**

**如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)**






### 最后

**分享一份NDK基础开发资料**

![详解:Linux网络虚拟化技术](https://img-blog.csdnimg.cn/img_convert/e2e0d98e6144ce862d9b44d09b1509d6.webp?x-oss-process=image/format,png)

分享内容包括不限于高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter等全方面的Android进阶实践技术;希望能帮助到大家,也节省大家在网上搜索资料的时间来学习,也可以分享动态给身边好友一起学习!


**《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》[点击传送门即可获取!](https://bbs.csdn.net/forums/f76c2498e3b04ae99081eaf6e6cf692c)**

识点,真正体系化!**

**由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!**

**如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)**






### 最后

**分享一份NDK基础开发资料**

[外链图片转存中...(img-K72IBdFM-1713276780712)]

分享内容包括不限于高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter等全方面的Android进阶实践技术;希望能帮助到大家,也节省大家在网上搜索资料的时间来学习,也可以分享动态给身边好友一起学习!


**《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》[点击传送门即可获取!](https://bbs.csdn.net/forums/f76c2498e3b04ae99081eaf6e6cf692c)**

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/2401_84123584/article/details/137842733

智能推荐

分布式光纤传感器的全球与中国市场2022-2028年:技术、参与者、趋势、市场规模及占有率研究报告_预计2026年中国分布式传感器市场规模有多大-程序员宅基地

文章浏览阅读3.2k次。本文研究全球与中国市场分布式光纤传感器的发展现状及未来发展趋势,分别从生产和消费的角度分析分布式光纤传感器的主要生产地区、主要消费地区以及主要的生产商。重点分析全球与中国市场的主要厂商产品特点、产品规格、不同规格产品的价格、产量、产值及全球和中国市场主要生产商的市场份额。主要生产商包括:FISO TechnologiesBrugg KabelSensor HighwayOmnisensAFL GlobalQinetiQ GroupLockheed MartinOSENSA Innovati_预计2026年中国分布式传感器市场规模有多大

07_08 常用组合逻辑电路结构——为IC设计的延时估计铺垫_基4布斯算法代码-程序员宅基地

文章浏览阅读1.1k次,点赞2次,收藏12次。常用组合逻辑电路结构——为IC设计的延时估计铺垫学习目的:估计模块间的delay,确保写的代码的timing 综合能给到多少HZ,以满足需求!_基4布斯算法代码

OpenAI Manager助手(基于SpringBoot和Vue)_chatgpt网页版-程序员宅基地

文章浏览阅读3.3k次,点赞3次,收藏5次。OpenAI Manager助手(基于SpringBoot和Vue)_chatgpt网页版

关于美国计算机奥赛USACO,你想知道的都在这_usaco可以多次提交吗-程序员宅基地

文章浏览阅读2.2k次。USACO自1992年举办,到目前为止已经举办了27届,目的是为了帮助美国信息学国家队选拔IOI的队员,目前逐渐发展为全球热门的线上赛事,成为美国大学申请条件下,含金量相当高的官方竞赛。USACO的比赛成绩可以助力计算机专业留学,越来越多的学生进入了康奈尔,麻省理工,普林斯顿,哈佛和耶鲁等大学,这些同学的共同点是他们都参加了美国计算机科学竞赛(USACO),并且取得过非常好的成绩。适合参赛人群USACO适合国内在读学生有意向申请美国大学的或者想锻炼自己编程能力的同学,高三学生也可以参加12月的第_usaco可以多次提交吗

MySQL存储过程和自定义函数_mysql自定义函数和存储过程-程序员宅基地

文章浏览阅读394次。1.1 存储程序1.2 创建存储过程1.3 创建自定义函数1.3.1 示例1.4 自定义函数和存储过程的区别1.5 变量的使用1.6 定义条件和处理程序1.6.1 定义条件1.6.1.1 示例1.6.2 定义处理程序1.6.2.1 示例1.7 光标的使用1.7.1 声明光标1.7.2 打开光标1.7.3 使用光标1.7.4 关闭光标1.8 流程控制的使用1.8.1 IF语句1.8.2 CASE语句1.8.3 LOOP语句1.8.4 LEAVE语句1.8.5 ITERATE语句1.8.6 REPEAT语句。_mysql自定义函数和存储过程

半导体基础知识与PN结_本征半导体电流为0-程序员宅基地

文章浏览阅读188次。半导体二极管——集成电路最小组成单元。_本征半导体电流为0

随便推点

【Unity3d Shader】水面和岩浆效果_unity 岩浆shader-程序员宅基地

文章浏览阅读2.8k次,点赞3次,收藏18次。游戏水面特效实现方式太多。咱们这边介绍的是一最简单的UV动画(无顶点位移),整个mesh由4个顶点构成。实现了水面效果(左图),不动代码稍微修改下参数和贴图可以实现岩浆效果(右图)。有要思路是1,uv按时间去做正弦波移动2,在1的基础上加个凹凸图混合uv3,在1、2的基础上加个水流方向4,加上对雾效的支持,如没必要请自行删除雾效代码(把包含fog的几行代码删除)S..._unity 岩浆shader

广义线性模型——Logistic回归模型(1)_广义线性回归模型-程序员宅基地

文章浏览阅读5k次。广义线性模型是线性模型的扩展,它通过连接函数建立响应变量的数学期望值与线性组合的预测变量之间的关系。广义线性模型拟合的形式为:其中g(μY)是条件均值的函数(称为连接函数)。另外,你可放松Y为正态分布的假设,改为Y 服从指数分布族中的一种分布即可。设定好连接函数和概率分布后,便可以通过最大似然估计的多次迭代推导出各参数值。在大部分情况下,线性模型就可以通过一系列连续型或类别型预测变量来预测正态分布的响应变量的工作。但是,有时候我们要进行非正态因变量的分析,例如:(1)类别型.._广义线性回归模型

HTML+CSS大作业 环境网页设计与实现(垃圾分类) web前端开发技术 web课程设计 网页规划与设计_垃圾分类网页设计目标怎么写-程序员宅基地

文章浏览阅读69次。环境保护、 保护地球、 校园环保、垃圾分类、绿色家园、等网站的设计与制作。 总结了一些学生网页制作的经验:一般的网页需要融入以下知识点:div+css布局、浮动、定位、高级css、表格、表单及验证、js轮播图、音频 视频 Flash的应用、ul li、下拉导航栏、鼠标划过效果等知识点,网页的风格主题也很全面:如爱好、风景、校园、美食、动漫、游戏、咖啡、音乐、家乡、电影、名人、商城以及个人主页等主题,学生、新手可参考下方页面的布局和设计和HTML源码(有用点赞△) 一套A+的网_垃圾分类网页设计目标怎么写

C# .Net 发布后,把dll全部放在一个文件夹中,让软件目录更整洁_.net dll 全局目录-程序员宅基地

文章浏览阅读614次,点赞7次,收藏11次。之前找到一个修改 exe 中 DLL地址 的方法, 不太好使,虽然能正确启动, 但无法改变 exe 的工作目录,这就影响了.Net 中很多获取 exe 执行目录来拼接的地址 ( 相对路径 ),比如 wwwroot 和 代码中相对目录还有一些复制到目录的普通文件 等等,它们的地址都会指向原来 exe 的目录, 而不是自定义的 “lib” 目录,根本原因就是没有修改 exe 的工作目录这次来搞一个启动程序,把 .net 的所有东西都放在一个文件夹,在文件夹同级的目录制作一个 exe._.net dll 全局目录

BRIEF特征点描述算法_breif description calculation 特征点-程序员宅基地

文章浏览阅读1.5k次。本文为转载,原博客地址:http://blog.csdn.net/hujingshuang/article/details/46910259简介 BRIEF是2010年的一篇名为《BRIEF:Binary Robust Independent Elementary Features》的文章中提出,BRIEF是对已检测到的特征点进行描述,它是一种二进制编码的描述子,摈弃了利用区域灰度..._breif description calculation 特征点

房屋租赁管理系统的设计和实现,SpringBoot计算机毕业设计论文_基于spring boot的房屋租赁系统论文-程序员宅基地

文章浏览阅读4.1k次,点赞21次,收藏79次。本文是《基于SpringBoot的房屋租赁管理系统》的配套原创说明文档,可以给应届毕业生提供格式撰写参考,也可以给开发类似系统的朋友们提供功能业务设计思路。_基于spring boot的房屋租赁系统论文