技术标签: android热修复 andfix源码解析 andfix使用 热修复和插件化 andfix 热修复
目录
上一篇《Android热修复技术简介》中对Android的热修复技术的概念和常用的技术方案做了一个简单的介绍,那么今天就来实战一下热修复技术,我们使用的是AndFix,为什么是它?因为无论是从使用上还是原理上AndFix都是相对简单的,毕竟这是实战的第一篇,还是要有个由易到难的过程的,好了,话不多说,开始吧!
AndFix项目地址:https://github.com/alibaba/AndFix,大家访问这个地址去看它的详细介绍,我这里只是简单的列一下:
添加AndFix依赖,这一步没啥好说的,直接到它的GitHub主页上去复制就OK了:
//引入AndFix模块
implementation 'com.alipay.euler:andfix:0.5.0@aar'
根据GitHub文档上的How to use这一部分的说明,我们来对AndFix做初始化操作。这里创建一个类AndFixPatchManager来统一管理AndFix所有的API,这样做一是为了方便管理,二是为了可以降低AndFix对我们代码的侵入性:
/**
* 作者:created by Jarchie
* 时间:2020/5/22 14:33:16
* 邮箱:[email protected]
* 说明:管理AndFix所有的API
*/
public class AndFixPatchManager {
private static AndFixPatchManager mInstance = null;
private static PatchManager mPatchManager = null;
//单例模式双检查机制
public static AndFixPatchManager getInstance(){
if (mInstance == null){
synchronized (AndFixPatchManager.class){
if (mInstance == null){
mInstance = new AndFixPatchManager();
}
}
}
return mInstance;
}
//初始化AndFix方法
public void initPatch(Context context){
mPatchManager = new PatchManager(context);
mPatchManager.init(Utils.getVersionName(context));
mPatchManager.loadPatch();
}
//加载Patch文件
public void addPatch(String path){
try {
if (mPatchManager!=null){
mPatchManager.addPatch(path);
}
}catch (Exception e){
e.printStackTrace();
}
}
}
这里面用到了一个Utils.getVersionName()的工具方法,这个方法就是来获取应用版本信息的:
//获取版本名称
public static String getVersionName(Context context) {
String versionName = "1.0.0";
try {
PackageManager pm = context.getPackageManager();
PackageInfo pi = pm.getPackageInfo(context.getPackageName(), 0);
versionName = pi.versionName;
} catch (Exception e) {
e.printStackTrace();
}
return versionName;
}
然后在项目的Application类中调用上面写好的初始化方法即可完成AndFix的初始化操作:
/**
* 作者:created by Jarchie
* 时间:2020/5/22 14:40:30
* 邮箱:[email protected]
* 说明:自定义的Application类
*/
public class BaseApp extends Application {
@Override
public void onCreate() {
super.onCreate();
//完成AndFix的初始化
initAndFix();
}
private void initAndFix() {
AndFixPatchManager.getInstance().initPatch(this);
}
}
2.3.1、构建异常APK
①、创建布局
先来创建一个布局文件activity_main.xml,内容很简单,两个按钮,一个模拟异常场景,一个模拟修复场景:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/mCreateBug"
android:layout_width="match_parent"
android:layout_height="45dp"
android:layout_margin="20dp"
android:background="@color/colorPrimary"
android:gravity="center"
android:text="生成BUG"
android:textColor="#fff" />
<TextView
android:id="@+id/mFixBug"
android:layout_width="match_parent"
android:layout_height="45dp"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:background="@color/colorPrimary"
android:gravity="center"
android:text="修复BUG"
android:textColor="#fff" />
</LinearLayout>
界面如下:当我们点击生成BUG按钮时,我们的程序会发生崩溃Crash掉:
②、编写业务代码
首先是对差异包的后缀名、存放路径等的一个初始化操作:
private static final String TAG = MainActivity.class.getSimpleName();
//定义差异包文件的后缀名
private static final String FILE_SUFFIX = ".apatch";
//定义差异包文件的存放路径
private String mPatchDir;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//初始化差异包文件路径
mPatchDir = getExternalCacheDir().getAbsolutePath()+"/apatch/";
Log.e(TAG, "完整路径--->"+mPatchDir);
//创建文件夹
File file = new File(mPatchDir);
if (file == null || !file.exists()){
file.mkdir();
}
}
然后是构造apatch文件的完整路径,当点击修复BUG的时候,调用PatchManager的addPath方法加载文件:
@Override
public void onClick(View view) {
switch (view.getId()){
case R.id.mFixBug: //修复Bug
AndFixPatchManager.getInstance().addPatch(getPatchName());
break;
}
}
//构造patch文件名
private String getPatchName(){
return mPatchDir.concat("jaqandfix").concat(FILE_SUFFIX);
}
③、模拟BUG产生
在产生BUG按钮的点击事件的方法中我们模拟一次Crash的产生:
@Override
public void onClick(View view) {
switch (view.getId()){
case R.id.mCreateBug: //生成Bug
Utils.printLog();
break;
}
}
然后在Utils类中的printLog()方法中给它制造Crash,这里就简单的让它产生空指针异常:
//构造异常方法
public static void printLog() {
String info = null;
Log.e("jarchie-andfix", info);
}
④、Build异常APK
在构建APK时,我们需要构建带签名的版本,这是为了接下来生成apatch文件用的。关于如何构建release版本的APK我就不多说了,相信没有不知道的吧,构建完了之后,你可以把它弄到你的手机上,通过adb push或者文件传输工具都可以,只要安装到你手机上就行,注意这里需要将这个有bug的apk保存一份,因为后面要用到。
2.3.2、构建正常APK
①、修改空指针异常
public static void printLog() {
String info = "Jarchie"; //修复空指针
Log.e("jarchie-andfix", info);
}
②、构建修复后的APK
将修改后的代码重新打包,生成新的release包,这里也将新的apk包保存一份。
①、生成apatch文件
生成apatch文件主要是用到了apkpatch这个命令行工具,这个工具包在github上有,大家下载到自己电脑上就行了:
里面就3个文件,windows用户使用.bat的这个,Linux或者MAC OS的用户使用.sh的这个。
然后我将之前Build的两个apk和jks文件都复制到这个文件夹中,并且新建了一个文件夹outputs作为apatch文件的输出目录:
然后打开控制台,进入到apkpatch这个目录下,执行apkpatch命令来看一下这个命令的用法介绍:
上面的是用来生成apatch文件,下面的是用来合并多个patch文件为一个的时候用的,具体的参数下面也都给出了,并且也都有注释说明(虽然都是英文,但相信你都能看的懂)。
然后我们就来使用apkpatch命令来生成我们的.apatch差异包:
执行完这个命令就生成了我们的差异包,并且它还会告诉你哪个类的哪个方法做了修改,正好就是我们的printLog()方法修改了。
进到本地目录中可以看到确实生成了apatch文件,我将它重命名为 jaqandfix.apatch。
②、push apatch文件
在生成了apatch文件之后,就可以将它放到手机对应的目录中,这一步操作同样也没有限制具体的方法,你可以通过文件传输工具,也可以直接通过adb命令将文件push到对应的目录,我这里使用adb命令的方式进行:
可以看到,我们手机中对应的目录下面已经有了push进来的jaqandfix.apatch文件。
③、修复BUG
再次进入App,然后首先点击修复BUG,它会去load这个补丁文件,当你再次点击产生BUG时,你会发现BUG已经被修复了。
注意:官网上给出的是2.1-7.0的版本,如果你各种操作步骤都是正确的,但是没有效果,那就换一台手机试一下,因为毕竟这个东西并不是所有机型都适配的,这里主要是学习它的方法。还有一点是,实际应用中,补丁文件是肯定不可能通过adb push这种方式进入用户手机中的,基本上都是通过服务端下发,客户端是一个下载文件的过程,这一点也需要注意。
到这里就已经说完了AndFix的修复流程,整个流程总结下来就是下面这张简化的图:
首先找到之前封装的AndFixPatchManager类,然后找到initPatch()方法:
//初始化AndFix方法
public void initPatch(Context context){
mPatchManager = new PatchManager(context);
mPatchManager.init(Utils.getVersionName(context));
mPatchManager.loadPatch();
}
从代码中可以看到,所有的操作都是通过AndFix的PatchManager类来完成的,很明显是外观模式,将所有的API都包含在了PatchManger中,所以不需要关注AndFix其他模块的作用。这里需要说明一点,阅读源码我们不可能把每一个类的每一行代码都完全弄懂,我们读源码是为了了解这个框架的实现过程,所以最好的方式就是结合在应用层我们自己的业务代码中调用它的那些类和方法,按照顺序一一跟进阅读,把整个调用流程串起来就OK了。
好,现在来打开PatchManager类,首先看一下它里面几个比较重要的成员变量:
/**
* context
*/
private final Context mContext;
/**
* AndFix manager
*/
private final AndFixManager mAndFixManager;
/**
* patch directory
*/
private final File mPatchDir;
/**
* patchs
*/
private final SortedSet<Patch> mPatchs;
/**
* classloaders
*/
private final Map<String, ClassLoader> mLoaders;
接着来看一下它的构造方法,因为我们在应用层最先调用的就是它的构造方法:
/**
* @param context
* context
*/
public PatchManager(Context context) {
mContext = context;
mAndFixManager = new AndFixManager(mContext);
mPatchDir = new File(mContext.getFilesDir(), DIR);
mPatchs = new ConcurrentSkipListSet<Patch>();
mLoaders = new ConcurrentHashMap<String, ClassLoader>();
}
可以看到构造方法主要就是进行了一系列的初始化:上下文、AndFixManager、文件夹、数据结构等等的初始化操作。
接着来看我们应用层调用的第一个方法init()方法:
/**
* initialize
*
* @param appVersion
* App version
*/
public void init(String appVersion) {
if (!mPatchDir.exists() && !mPatchDir.mkdirs()) {// make directory fail
Log.e(TAG, "patch dir create error.");
return;
} else if (!mPatchDir.isDirectory()) {// not directory
mPatchDir.delete();
return;
}
SharedPreferences sp = mContext.getSharedPreferences(SP_NAME,
Context.MODE_PRIVATE);
String ver = sp.getString(SP_VERSION, null);
if (ver == null || !ver.equalsIgnoreCase(appVersion)) {
cleanPatch();
sp.edit().putString(SP_VERSION, appVersion).commit();
} else {
initPatchs();
}
}
入参是需要传入当前应用的版本号,然后内部一开始是进行了文件夹的判断,满足了条件之后,它会从AndFix的SharedPreferences中拿到之前保存的版本号,然后通过这个版本号和入参中传入的版本号去做一个判断,如果不同,表明我们的应用已经做了升级,然后就会调用cleanPatch()去删除所有的Patch文件,同时更新版本号,用于下一次的比较,如果版本号相同,表明没有升级,则会调用initPatchs()方法,接下来,跟进这个initPatchs()方法:
private void initPatchs() {
File[] files = mPatchDir.listFiles();
for (File file : files) {
addPatch(file);
}
}
这个方法很简单,就是遍历指定Patch文件夹下的所有文件,然后将它们通过addPatch()方法添加到mPatchs这个PatchList中,跟进addPatch()方法看一下:
private Patch addPatch(File file) {
Patch patch = null;
if (file.getName().endsWith(SUFFIX)) {
try {
patch = new Patch(file);
mPatchs.add(patch);
} catch (IOException e) {
Log.e(TAG, "addPatch", e);
}
}
return patch;
}
这个方法内部首先是判断传入的文件后缀名是否符合.apatch格式,如果符合,将其转化为Patch文件,然后将文件添加到PatchList中,所以这里的mPatchs内部就是保存了所有的Patch文件。然后点击Patch类进入到这个类中看一下它是如何将一个File转化为Patch类的?这个Patch类就相当于是一个实体类,这个类中定义了一些成员变量:
/**
* patch file
*/
private final File mFile;
/**
* name
*/
private String mName;
/**
* create time
*/
private Date mTime;
/**
* classes of patch
*/
private Map<String, List<String>> mClassesMap;
主要有传入的文件、文件名、mClassMap等,mClassMap是存储了本次Patch文件所有要修复的class的字符串,然后会调用类中的init()方法完成解析:
private void init() throws IOException {
JarFile jarFile = null;
InputStream inputStream = null;
try {
jarFile = new JarFile(mFile);
JarEntry entry = jarFile.getJarEntry(ENTRY_NAME);
inputStream = jarFile.getInputStream(entry);
Manifest manifest = new Manifest(inputStream);
Attributes main = manifest.getMainAttributes();
mName = main.getValue(PATCH_NAME);
mTime = new Date(main.getValue(CREATED_TIME));
mClassesMap = new HashMap<String, List<String>>();
Attributes.Name attrName;
String name;
List<String> strings;
for (Iterator<?> it = main.keySet().iterator(); it.hasNext();) {
attrName = (Attributes.Name) it.next();
name = attrName.toString();
if (name.endsWith(CLASSES)) {
strings = Arrays.asList(main.getValue(attrName).split(","));
if (name.equalsIgnoreCase(PATCH_CLASSES)) {
mClassesMap.put(mName, strings);
} else {
mClassesMap.put(
name.trim().substring(0, name.length() - 8),// remove
// "-Classes"
strings);
}
}
}
} finally {
if (jarFile != null) {
jarFile.close();
}
if (inputStream != null) {
inputStream.close();
}
}
}
这个方法是首先把文件转化成jar文件,然后解析jar文件中的所有字段比如:PATCH_NAME、CREATED_TIME等,这些字段是我们之前通过apatch命令行工具生成apatch文件的时候添加的,所以在这里可以直接解析了。然后来说mClassMap是如何初始化的,它会找到所有的Class,然后判断一下是不是自己要解析的PATCH_CLASS,如果是就添加到以当前Patch文件名为key的Map中,添加进来之后当你后续使用的时候,就可以直接通过getClasses传入当前的Patch文件名获取这个Patch文件中所有要修复的Class的绝对路径:
public List<String> getClasses(String patchName) {
return mClassesMap.get(patchName);
}
现在我们应该清楚了这个Patch文件的作用了,它就是将普通磁盘上的File转化成PatchFile方便使用。OK,到这里这个PatchManager的init()方法就说完了,总结一下它的作用就是对Patch文件的删除和添加。
应用层中在我们下载完Patch文件之后,我们调用了addPatch()方法还记得吗?mPatchManager.addPatch(path); 现在就来看一下这个addPatch()方法是如何实现的?
/**
* add patch at runtime
*
* @param path
* patch path
* @throws IOException
*/
public void addPatch(String path) throws IOException {
File src = new File(path);
File dest = new File(mPatchDir, src.getName());
if(!src.exists()){
throw new FileNotFoundException(path);
}
if (dest.exists()) {
Log.d(TAG, "patch [" + path + "] has be loaded.");
return;
}
FileUtil.copyFile(src, dest);// copy to patch's directory
Patch patch = addPatch(dest);
if (patch != null) {
loadPatch(patch);
}
}
前面就是一些判断和文件的创建,它会先把磁盘上的文件拷贝到mPatchDir下面,拷贝完成之后会将文件解析成Patch类,然后会添加到mPatchs这个PatchList中,添加完以后,最后调用了loadPatch()方法,正是因为调用了loadPatch()方法所以可以完成BUG的修复,在loadPatch()方法内部调用了AndFixManager去完成了方法的替换,所以接着来看一下loadPatch()方法的实现过程。
/**
* load patch,call when application start
*
*/
public void loadPatch() {
mLoaders.put("*", mContext.getClassLoader());// wildcard
Set<String> patchNames;
List<String> classes;
for (Patch patch : mPatchs) {
patchNames = patch.getPatchNames();
for (String patchName : patchNames) {
classes = patch.getClasses(patchName);
mAndFixManager.fix(patch.getFile(), mContext.getClassLoader(),
classes);
}
}
}
/**
* load specific patch
*
* @param patch
* patch
*/
private void loadPatch(Patch patch) {
Set<String> patchNames = patch.getPatchNames();
ClassLoader cl;
List<String> classes;
for (String patchName : patchNames) {
if (mLoaders.containsKey("*")) {
cl = mContext.getClassLoader();
} else {
cl = mLoaders.get(patchName);
}
if (cl != null) {
classes = patch.getClasses(patchName);
mAndFixManager.fix(patch.getFile(), cl, classes);
}
}
}
loadPatch()方法有两个重载的方法,上面的没有参数的方法会遍历mPatchs这个集合,对所有的Patch文件中的Class都调用一次AndFixManager的fix()方法,下面的有参数的方法就是单一的修复指定Patch文件中的Class字节码,无论是有参还是无参的方法都调用了mAndFixManager.fix()方法,接着来看一下这个方法内部又是如何实现的?
public synchronized void fix(File file, ClassLoader classLoader,
List<String> classes) {
if (!mSupport) {
return;
}
if (!mSecurityChecker.verifyApk(file)) {// security check fail
return;
}
try {
File optfile = new File(mOptDir, file.getName());
boolean saveFingerprint = true;
if (optfile.exists()) {
// need to verify fingerprint when the optimize file exist,
// prevent someone attack on jailbreak device with
// Vulnerability-Parasyte.
// btw:exaggerated android Vulnerability-Parasyte
// http://secauo.com/Exaggerated-Android-Vulnerability-Parasyte.html
if (mSecurityChecker.verifyOpt(optfile)) {
saveFingerprint = false;
} else if (!optfile.delete()) {
return;
}
}
final DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(),
optfile.getAbsolutePath(), Context.MODE_PRIVATE);
if (saveFingerprint) {
mSecurityChecker.saveOptSig(optfile);
}
ClassLoader patchClassLoader = new ClassLoader(classLoader) {
@Override
protected Class<?> findClass(String className)
throws ClassNotFoundException {
Class<?> clazz = dexFile.loadClass(className, this);
if (clazz == null
&& className.startsWith("com.alipay.euler.andfix")) {
return Class.forName(className);// annotation’s class
// not found
}
if (clazz == null) {
throw new ClassNotFoundException(className);
}
return clazz;
}
};
Enumeration<String> entrys = dexFile.entries();
Class<?> clazz = null;
while (entrys.hasMoreElements()) {
String entry = entrys.nextElement();
if (classes != null && !classes.contains(entry)) {
continue;// skip, not need fix
}
clazz = dexFile.loadClass(entry, patchClassLoader);
if (clazz != null) {
fixClass(clazz, classLoader);
}
}
} catch (IOException e) {
Log.e(TAG, "pacth", e);
}
}
首先是一些安全性的判断,它会验证签名是否符合,验证通过才会继续往下走,它会将Patch文件中的File转化成DexFile,然后遍历dexFile中的所有变量,在while循环中真正找到要修复的classes,因为我们传入的其实是Class文件的Name,所以这个while真正的遍历就是通过name调用dexFile的loadClass找到真正要修复的Class字节码,然后又调用了fixClass来完成方法的替换,所以还要继续往下来看fixClass又完成了哪些操作?
private void fixClass(Class<?> clazz, ClassLoader classLoader) {
Method[] methods = clazz.getDeclaredMethods();
MethodReplace methodReplace;
String clz;
String meth;
for (Method method : methods) {
methodReplace = method.getAnnotation(MethodReplace.class);
if (methodReplace == null)
continue;
clz = methodReplace.clazz();
meth = methodReplace.method();
if (!isEmpty(clz) && !isEmpty(meth)) {
replaceMethod(classLoader, clz, meth, method);
}
}
}
它的入参就是真正要修复的Class字节码和ClassLoader,方法内部首先是通过反射找到字节码中所有的方法,接着是定义了一个注解,这个注解就是之前一开始介绍到的AndFix是通过注解找到哪些方法是需要被替换的,接着会遍历所有的方法来看一下哪个方法上有methodReplace这个注解,如果有就把这个方法记录下来,接着调用replaceMethod()方法来完成方法的替换,继续跟进replaceMethod()方法看一下它内部又是如何实现的?
private void replaceMethod(ClassLoader classLoader, String clz,
String meth, Method method) {
try {
String key = clz + "@" + classLoader.toString();
Class<?> clazz = mFixedClass.get(key);
if (clazz == null) {// class not load
Class<?> clzz = classLoader.loadClass(clz);
// initialize target class
clazz = AndFix.initTargetClass(clzz);
}
if (clazz != null) {// initialize class OK
mFixedClass.put(key, clazz);
Method src = clazz.getDeclaredMethod(meth,
method.getParameterTypes());
AndFix.addReplaceMethod(src, method);
}
} catch (Exception e) {
Log.e(TAG, "replaceMethod", e);
}
}
这个方法中最关键的一句代码就是:AndFix.addReplaceMethod(src, method); 接着跟到这个方法中:
public static void addReplaceMethod(Method src, Method dest) {
try {
replaceMethod(src, dest);
initFields(dest.getDeclaringClass());
} catch (Throwable e) {
Log.e(TAG, "addReplaceMethod", e);
}
}
然后addReplaceMethod()中又调用了replaceMethod()方法,接着跟到replaceMethod()方法中:
private static native void replaceMethod(Method dest, Method src);
可以发现这个方法是native方法,所以到这里我们就跟不下去了,它应该是通过C层对dex文件的操作完成最终方法的替换。
到这里源码阅读就结束了,以上就是整个AndFix的执行流程。
今天就先到这里吧,下一篇准备来说说Tinker的使用,下期见!
在CentOS上静默安装Oracle,所以使用脚本进行安装。配置完成之后,执行:sh ./runInstaller -silent -noconfig -responseFile ./response/db_install.rsp失败提示:Starting Oracle Universal Installer...Checking Temp space: must be grea
目录1.前言1.1.参考博客1.前言1.1.参考博客【泡泡机器人原创专栏】DBoW3 视觉词袋模型、视觉字典和图像数据库分析浅谈回环检测中的词袋模型(bag of words)开源词袋模型DBow3原理&源码(一)整体结构...
沙箱环境是支付宝开放平台为开发者提供的与生产环境完全隔离的联调测试环境,开发者在沙箱环境中完成的接口调用不会对生产环境中的数据造成任何影响。第三方支付接口流程大同小异,考虑开发及教学的方便性,支付宝提供支付宝沙箱环境开发支付接口,在教学中接入支付宝手机网站支付接口。详细参见:https://docs.open.alipay.com/200/105311/本文档使用支付宝沙箱进行开发测试,这里主要介绍支付宝沙箱环境配置。使用沙箱环境的买家账号登录沙箱版本的支付宝。安装模拟器,安装在没有空格和中文的目录。_支付宝开发环境
semi-global matching(SGM)是一种用于计算双目视觉中视差(disparity)的半全局匹配算法,在OpenCV中的实现为semi-global block matching(SGBM);opencv中SGBM算法的参数含义及数值选取一、 预处理参数1:preFilterCap:水平sobel预处理后,映射滤波器大小。默认为15int ftzero =max(p..._stereosgbm
1:@Path上定义的参数,可以使用正则表达式如:@Path("users/{username: [a-zA-Z][a-zA-Z_0-9]*}")此处,如果用户输出的参数不匹配,就会报404(Not Found)错误。2:顺序作用域 /***顺序作用域* Http 方法: GET * API 路径: /rest/te_rest表达式
混合样本数据增强(Mixed Sample Data Augmentation,MSDA)目前非常火热,由于其实现简单且对性能提升确实有帮助,因此在图像识别、声音识别、GAN、半监督学习等领域均有广泛的应用。MSDA的代表性算法是Mixup,最早出现在ICLR2018的论文“Mixup: Beyond Empirical Risk Minimization”中。关于这篇论文,博主专门写了一篇文..._mixseq data augmentation
网上有分析说调用的高版本的gcc,生成的动态库没有替换老版本gcc的动态库导致。因此需要把高版本的so文件复制到低版本的so文件目录下。如下分析:但我报的错有点跟他们不太一样,我实在python的虚拟环境中执行的。我查看了虚拟环境中的so文件,有“GLIBCXX_3.4.29”的内容的。但为什么没有被引用,反而舍近求远去调用 “/usr/lib/” 这个目录下呢?_x86_64-linux-gnu
至少需要两个一个引用记录当前结点,另一个如果发现重复的结点,一直遍历,知道和当前数据不同为止,但是还需要一个当前结点的前驱,一旦发现相同,前驱的下一个结点就改为第一个和当前数据不同的结点。如果不同,三个引用往后移动。public Node deleteDuplication(Node head) { Node p=null; Node p1=head; Node p2=he..._一个长度为n的有序链表,存在重复的结点,要求删除所有重复元素从而让同样的元素只
Linux gcc编译、静态库、动态库linux下gcc编译原理静态库、动态库创建与使用常见错误及基本操作_gcc -shared -wl 静态库
第一题某学校的计算机系一共有n门专业课程,依次被标记为0、1、......、n-1。某些课程只能在前置课程修读完之后才能进行修读,例如课程0的前置课程为课程1,表示为[0,1]。给定专业课程数量以及专业课程之间的前置关系,输出课程的正确修读顺序从而满足课程之间的前置关系。例子:输入:4,[1,0],[2,0],[3,1],[3,2]输出:0,1,2,3或者0,2,1,3显然这是拓扑排序问题,这种有典型的思路:找到当前入度为0的结点输出 它的直接后序节点入度减一 ._复旦计算机夏令营机试
天玑1100采用的是台积电6nm的工艺,而骁龙750g处理器的工艺为8nm制程,相比来说,这款联发科天玑1100的工艺方面表现更好些。骁龙750G的CPU为八核心设计,包括两个A77的魔改版Kryo 570,主频2.2GHz,另外还有六个A55 1.8GHz。天玑820采用 4 个主频高达2.6GHz 的Cortex-A76核心和4 个主频 2.0GHz 的 Cortex-A55 高能效核心。性能方面的区别,这款天玑1100处理器其主要参数比起骁龙750g表现更好,对应在多核,单核,跑分方面都比骁龙75_i711390h和i511320h性能差别
昨天公司组织了一次讲座,现任的投资部经理给我上了一堂生动有趣的关于投资的课,受益匪浅啊。主要的几个观点是:1、要自己动脑思考2、要做长期投资,而不是短线投资。3、要把握好时机,做自己熟悉的4、多和大师们学习联想到自己现在所从事的工作,其实编程方面与投资也有相通的东西,可能是由于本来这些观点就是一些哲学观点,适用于很多方面吧。首先,动脑思考,作为一个程序员或者说