Android热修复及插件化原理示例详解

目录
  • 1.前言
  • 2.类加载机制
  • 3.Android类加载
  • 4.Tinker原理
    • 代码实现
  • 5.插件化
    • 5.1 Activity启动流程简单介绍
    • 5.2 插件化原理
      • 5.2.1 绕开验证
      • 5.2.2还原插件Activity
    • 5.3 加载插件资源
      • 5.3.1 Resources&AssetManager
      • 5.3.2 id冲突

1.前言

热修复一直是这几年来很热门的话题,主流方案大致有两种,一种是微信Tinker的dex文件替换,另一种是阿里的Native层的方法替换。这里重点介绍Tinker的大致原理。

2.类加载机制

介绍Tinker原理之前,我们先来回顾一下类加载机制。

我们编译好的class文件,需要先加载到虚拟机然后才会执行,这个过程是通过ClassLoader来完成的。

双亲委派模型:

  • 1.加载某个类的时候,这个类加载器不会自己立刻去加载,它会委托给父类去加载
  • 2.如果这个父类还存在父类加载器,则进一步委托,直到最顶层的类加载器
  • 3.如果父类加载器可以完成加载任务,就成功返回,否则就再委派给子类加载器
  • 4.如果都未加载成功就抛出ClassNotFoundException

作用:

1.避免类的重复加载。

比如有两个类加载器,他们都要加载同一个类,这时候如果不是委托而是自己加载自己的,则会将类重复加载到方法区。

2.避免核心类被修改。

比如我们在自定义一个 java.lang.String 类,执行的时候会报错,因为 String 是 java.lang 包下的类,应该由启动类加载器加载。

JVM并不会一开始就加载所有的类,它是当你使用到的时候才会去通知类加载器去加载。

3.Android类加载

当我们new一个类时,首先是Android的虚拟机(Dalvik/ART虚拟机)通过ClassLoader去加载dex文件到内存。

Android中的ClassLoader主要是PathClassLoader和DexClassLoader,这两者都继承自BaseDexClassLoader。它们都可以理解成应用类加载器。

PathClassLoader和DexClassLoader的区别:

  • PathClassLoader只能指定加载apk包路径,不能指定dex文件解压路径。该路径是写死的在/data/dalvik-cache/路径下。所以只能用于加载已安装的apk。
  • DexClassLoader可以指定apk包路径和dex文件解压路径(加载jar、apk、dex文件)

当ClassLoader加载类时,会调用它的findclass方法去查找该类。

下方是BaseDexClassLoader的findClass方法实现:

public class BaseDexClassLoader extends ClassLoader {
...
    @UnsupportedAppUsage
    private final DexPathList pathList;
    ...
     @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 首先检查该类是否存在shared libraries中.
        if (sharedLibraryLoaders != null) {
            for (ClassLoader loader : sharedLibraryLoaders) {
                try {
                    return loader.loadClass(name);
                } catch (ClassNotFoundException ignored) {
                }
            }
        }
        //再调用pathList.findClass去查找该类,结果为null则抛出错误。
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException(
                    "Didn't find class \"" + name + "\" on path: " + pathList);
            for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }
}

接下来我们再来看看DexPathList的findClass实现:

 public DexPathList(ClassLoader definingContext, String librarySearchPath) {
...
    /**
     * List of dex/resource (class path) elements.
     * 存放dex文件的一个数组
     */
    @UnsupportedAppUsage
    private Element[] dexElements;
...
    public Class&lt;?&gt; findClass(String name, List&lt;Throwable&gt; suppressed) {
     	 //遍历Element数组,去查寻对应的类,找到后就立刻返回了
        for (Element element : dexElements) {
            Class&lt;?&gt; clazz = element.findClass(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }
        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }
...
}

4.Tinker原理

  • 1.使用DexClassLoader加载补丁包的dex文件
  • 2.通过反射获取DexClassLoader类的pathList,再次通过反射获得dexElements数组。
  • 3.获取加载应用类的PathClassLoader,同样通过反射获取它的dexElements数组。
  • 4.合并两个dexElements数组,且将补丁包的dex文件放在前面。
    根据类加载机制,一个类只会被加载一次,DexPathList.findClass方法中是顺序遍历数组,所以将补丁的dex文件放在前面,这样bug修复类会被优先加载,而原来的bug类不会被加载,达到了替换bug类的功能(补丁包中的修复类名、包名要和bug类相同)
  • 5.再次通过反射将合并后的dexElements数组赋值给PathClassLoader.dexElements属性。
    加载类时,Dalvik/ART虚拟机会通过PathClassLoader去查找已安装的apk文件中的类。

Ok,这样就替换成功了,重启App,再调用原来的bug类,将会优先使用补丁包中的修复类。

为什么要重启:因为双亲委派模型,一个类只会被ClassLoader加载一次,且加载过后的类不能卸载。

代码实现

接下来我们动手撸一个乞丐版的Tinker。
首先我们写一个bug类。

package com.baima.plugin;
class BugClass {
    public String getTitle(){
        return "这是个Bug";
    }
}

接着我们新建一个module来生成补丁包apk。

创建bug修复类,注意包名类名要一样。

package com.baima.plugin;
class BugClass {
    public String getTitle(){
        return "修复成功";
    }
}

生成补丁apk,让用户下载这个补丁包。接下来就是加载这个apk文件并替换了。

    public void loadDexAndInject(Context appContext, String dexPath, String dexOptPath) {
        try {
            // 加载应用程序dex的Loader
            PathClassLoader pathLoader = (PathClassLoader) appContext.getClassLoader();
            //dexPath 补丁dex文件所在的路径
            //dexOptPath 补丁dex文件被写入后存放的路径
            DexClassLoader dexClassLoader = new DexClassLoader(dexPath, dexOptPath, null, pathLoader);
            //利用反射获取DexClassLoader和PathClassLoader的pathList属性
            Object dexPathList = getPathList(dexClassLoader);
            Object pathPathList = getPathList(pathLoader);
            //同样用反射获取DexClassLoader和PathClassLoader的dexElements属性
            Object leftDexElements = getDexElements(dexPathList);
            Object rightDexElements = getDexElements(pathPathList);
            //合并两个数组,且补丁包的dex文件在数组的前面
            Object dexElements = combineArray(leftDexElements, rightDexElements);
            //反射将合并后的数组赋值给PathClassLoader的pathList.dexElements
            Object pathList = getPathList(pathLoader);
            Class<?> pathClazz = pathList.getClass();
            Field declaredField = pathClazz.getDeclaredField("dexElements");
            declaredField.set看,ccessible(true);
            declaredField.set(pathList, dexElements);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    private static Object getPathList(Object classLoader) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        Class<?> cl = Class.forName("dalvik.system.BaseDexClassLoader");
        Field field = cl.getDeclaredField("pathList");
        field.setAccessible(true);
        return field.get(classLoader);
    }
    private static Object getDexElements(Object pathList) throws NoSuchFieldException, IllegalAccessException {
        Class<?> cl = pathList.getClass();
        Field field = cl.getDeclaredField("dexElements");
        field.setAccessible(true);
        return field.get(pathList);
    }
    private static Object combineArray(Object arrayLeft, Object arrayRight) {
        Class<?> clazz = arrayLeft.getClass().getComponentType();
        int i = Array.getLength(arrayLeft);
        int j = Array.getLength(arrayRight);
        int k = i + j;
        Object result = Array.newInstance(clazz, k);// 创建一个类型为clazz,长度为k的新数组
        System.arraycopy(arrayLeft, 0, result, 0, i);
        System.arraycopy(arrayRight, 0, result, i, j);
        return result;
    }

ok,乞丐版Tinker完成了,使用时先在Splash界面检查是否有插件补丁,有的话执行替换,这时你再使用bug类会发现它已经被替换成补丁中的修复类了。

5.插件化

插件化开发模式,打包时是一个宿主apk+多个插件apk。

组件化开发模式,打包时是一个apk,里面分多个module。

优点:

  • 安装的主apk包会小好多
  • 给开发者提供了业务功能扩展,并且不需要用户进行更新
  • 在非主apk包中的功能出现BUG时,可以及时修复
  • 用户不需要的功能,完全就不会出现在系统里面,减轻设备的负担

需要掌握的知识:

  • 1.类加载机制
  • 2.四大组件启动流程
  • 3.AIDL、Binder机制
  • 4.Hook、反射、代理

5.1 Activity启动流程简单介绍

上图是普通的Activity启动流程,和根Activity启动流程的区别是不用创建应用程序进程(Application Thread)。

启动过程:

  • 应用程序进程中的Activity向AMS请求创建普通Activity
  • AMS会对这个Activty的生命周期管和栈进行管理,校验Activity等等
  • 如果Activity满足AMS的校验,AMS就会请求应用程序进程中的ActivityThread去创建并启动普通Activity

他们之间的跨进程通信是通过Binder实现的。

5.2 插件化原理

通过上面介绍的热修复,我们有办法去加载插件apk里面的类,但是还没有办法去启动插件中的Activity,因为如果要启动一个Activity,那么这个Activity必须在AndroidManifest.xml中注册。

这里介绍插件化的一种主流实现方式--Hook技术。

  • 1.宿主App预留占坑Activity
  • 2.使用classLoader加载dex文件到内存
  • 3.先使用占坑Activity绕过AMS验证,接着用插件Activity替换占坑的Activity。

步骤1、2这里就不在赘述了,2就是上面讲到的热修复技术。

5.2.1 绕开验证

AMS是在SystemServer进程中,我们无法直接进行修改,只能在应用程序进程中做文章。

介绍一个类--IActivityManager,IActivityManager它通过AIDL(内部使用的是Binder机制)和SystemServer进程的AMS通讯。所以IActivityManager很适合作为一个hook点。

Activity启动时会调用IActivityManager.startActivity方法向AMS发出启动请求,该方法参数包含一个Intent对象,它是原本要启动的Activity的Intent。

我们可以动态代理IActivityManager的startActivity方法,将该Intent换为占坑Activity的Intent,并将原来的Intent作为参数传递过去,以此达到欺骗AMS绕开验证。

public class IActivityManagerProxy implements InvocationHandler {
    private Object mActivityManager;
    private static final String TAG = "IActivityManagerProxy";
    public IActivityManagerProxy(Object activityManager) {
        this.mActivityManager = activityManager;
    }
    @Override
    public Object invoke(Object o, Method method, Object[] args) throws Throwable {
        if ("startActivity".equals(method.getName())) {
            Intent intent = null;
            int index = 0;
            for (int i = 0; i < args.length; i++) {
                if (args[i] instanceof Intent) {
                    index = i;
                    break;
                }
            }
            intent = (Intent) args[index];
            Intent subIntent = new Intent();
            String packageName = "com.example.pluginactivity";
            subIntent.setClassName(packageName,packageName+".StubActivity");
            subIntent.putExtra(HookHelper.TARGET_INTENT, intent);
            args[index] = subIntent;
        }
        return method.invoke(mActivityManager, args);
    }
}

接下来就通过反射的方式,将ActivityManager中的IActivityManager替换成我们的代理对象。

  public void hookAMS() {
        try {
            Object defaultSingleton = null;
            if (Build.VERSION.SDK_INT >= 26) {
                Class<?> activityManagerClazz = Class.forName("android.app.ActivityManager");
                defaultSingleton = FieldUtil.getObjectField(activityManagerClazz, null, "IActivityManagerSingleton");
            } else {
                Class<?> activityManagerNativeClazz = Class.forName("android.app.ActivityManagerNative");
                defaultSingleton = FieldUtil.getObjectField(activityManagerNativeClazz, null, "gDefault");
            }
            Class<?> singletonClazz = Class.forName("android.util.Singleton");
            Field mInstanceField = FieldUtil.getField(singletonClazz, "mInstance");
            Object iActivityManager = mInstanceField.get(defaultSingleton);
            Class<?> iActivityManagerClazz = Class.forName("android.app.IActivityManager");
            Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class<?>[]{iActivityManagerClazz}, new IActivityManagerProxy(iActivityManager));
            mInstanceField.set(defaultSingleton, proxy);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

Note: 这里获取IActivityManager实例会因为Android版本不同而不同,具体获取方法就需要去看源码了解了。这里的代码Android 8.0是可以运行的。

5.2.2还原插件Activity

ActivityThread启动Activity的过程如下所示:

ActivityThread会通过H在主线程中去启动Activity,H类是ActivityThread的内部类并继承自Handler。

private class H extends Handler {
public static final int LAUNCH_ACTIVITY         = 100;
public static final int PAUSE_ACTIVITY          = 101;
...
   public void handleMessage(Message msg) {
            if (DEBUG_MESSAGES) Slog.v(TAG, "&gt;&gt;&gt; handling: " + codeToString(msg.what));
            switch (msg.what) {
                case LAUNCH_ACTIVITY: {
                    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
                    final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
                    r.packageInfo = getPackageInfoNoCheck(
                            r.activityInfo.applicationInfo, r.compatInfo);
                    handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");
                    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                } break;
                ...
              }
...
}

H中重写的handleMessage方法会对LAUNCH_ACTIVITY类型的消息进行处理,最终会调用Activity的onCreate方法。那么在哪进行替换呢?接着来看Handler的dispatchMessage方法:

public void dispatchMessage(Message msg) {
       if (msg.callback != null) {
           handleCallback(msg);
       } else {
           if (mCallback != null) {
               if (mCallback.handleMessage(msg)) {
                   return;
               }
           }
           handleMessage(msg);
       }
   }

Handler的dispatchMessage用于处理消息,可以看到如果Handler的Callback类型的mCallback不为null,就会执行mCallback的handleMessage方法。因此,mCallback可以作为Hook点,我们可以用自定义的Callback来替换mCallback,自定义的Callback如下所示。

public class HCallback implements Handler.Callback{
    public static final int LAUNCH_ACTIVITY = 100;
    Handler mHandler;
    public HCallback(Handler handler) {
        mHandler = handler;
    }
    @Override
    public boolean handleMessage(Message msg) {
        if (msg.what == LAUNCH_ACTIVITY) {
            Object r = msg.obj;
            try {
                //得到消息中的Intent(启动占坑Activity的Intent)
                Intent intent = (Intent) FieldUtil.getField(r.getClass(), r, "intent");
                //得到此前保存起来的Intent(启动插件Activity的Intent)
                Intent target = intent.getParcelableExtra(HookHelper.TARGET_INTENT);
                //将占坑Activity的Intent替换为插件Activity的Intent
                intent.setComponent(target.getComponent());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        mHandler.handleMessage(msg);
        return true;
    }
}

最后一步就是用反射将我们自定义的callBack设置给ActivityThread.sCurrentActivityThread.mH.mCallback

    public void hookHandler() {
        try {
            Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
            Object currentActivityThread = FieldUtil.getObjectField(activityThreadClass, null, "sCurrentActivityThread");
            Field mHField = FieldUtil.getField(activityThreadClass, "mH");
            Handler mH = (Handler) mHField.get(currentActivityThread);
            FieldUtil.setObjectField(Handler.class, mH, "mCallback", new HCallback(mH));
        } catch (Exception e) {
             e.printStackTrace();
        }
    }

其实要想启动一个Activity到这步还没有完,一个完整的Activity应该还需要布局文件,而我们的宿主APP并不会包含插件的资源。

5.3 加载插件资源

5.3.1 Resources&AssetManager

android中的资源大致分为两类:一类是res目录下存在的可编译的资源文件,比如anim,string之类的,第二类是assets目录下存放的原始资源文件。因为Apk编译的时候不会编译这些文件,所以不能通过id来访问,当然也不能通过绝对路径来访问。于是Android系统让我们通过Resources的getAssets方法来获取AssetManager,利用AssetManager来访问这些文件。

其实Resource的getString, getText等各种方法都是通过调用AssetManager的私有方法来完成的。 过程就是Resource通过resource.arsc(AAPT工具打包过程中生成的文件)把ID转换成资源文件的名称,然后交由AssetManager来加载文件。

AssetManager里有个很重要的方法addAssetPath(String path)方法,App启动的时候会把当前apk的路径传进去,然后AssetManager就能访问这个路径下的所有资源也就是宿主apk的资源了。我们可以通过hook这个方法将插件的path传进去,得到的AssetManager就能同时访问宿主和插件的所有资源了。

 public void hookAssets(Activity activity,String dexPath){
        try {
            AssetManager assetManager = activity.getResources().getAssets();
            Method addAssetPath = assetManager.getClass().getMethod("addAssetPath",String.class);
            addAssetPath.invoke(assetManager,dexPath);
            Resources mResources = new Resources(assetManager, activity.getResources().getDisplayMetrics(), activity.getResources().getConfiguration());
            //接下来我们要将宿主原有Resources替换成我们上面生成的Resources。
            FieldUtil.setObjectField(ContextWrapper.class,activity.getResources(),"mResources",mResources);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

5.3.2 id冲突

新的问题又出现了,宿主apk和插件apk是两个不同的apk,他们在编译时都会产生自己的resources.arsc。即他们是两个独立的编译过程。那么它们的resources.arsc中的资源id必定是有相同的情况。这样我们上面生成的新Resources中就出现了资源id重复的情况,这样在运行的时候使用资源id来获取资源就会报错。

怎么解决资源Id冲突的问题?这里介绍一下VirtualApk采用的方案。

修改aapt的产物。即编译后期重新整理插件Apk的资源,编排ID,更新R文件

VirtualApkhook了ProcessAndroidResourcestask。这个task是用来编译Android资源的。VirtualApk拿到这个task的输出结果,做了以下处理:

  • 1.根据编译产生的R.txt文件收集插件中所有的资源
  • 2.根据编译产生的R.txt文件收集宿主apk中的所有资源
  • 3.过滤插件资源:过滤掉在宿主中已经存在的资源
  • 4.重新设置插件资源的资源ID
  • 5.删除掉插件资源目录下前面已经被过滤掉的资源
  • 6.重新编排插件resources.arsc文件中插件资源ID为新设置的资源ID
  • 7.重新产生R.java文件

大致原理是这样的,但如何保证新的Id不会重复了,这里在介绍一下资源Id的组成。

packageId: 前两位是packageId,相当于一个命名空间,主要用来区分不同的包空间(不是不同的module)。目前来看,在编译app的时候,至少会遇到两个包空间:android系统资源包和咱们自己的App资源包。大家可以观察R.java文件,可以看到部分是以0x01开头的,部分是以0x7f开头的。以0x01开头的就是系统已经内置的资源id,以0x7f开头的是咱们自己添加的app资源id。

typeId:typeId是指资源的类型id,我们知道android资源有animator、anim、color、drawable、layout,string等等,typeId就是拿来区分不同的资源类型。

entryId:entryId是指每一个资源在其所属的资源类型中所出现的次序。注意,不同类型的资源的Entry ID有可能是相同的,但是由于它们的类型不同,我们仍然可以通过其资源ID来区别开来。

所以为了避免冲突,插件的资源id通常会采用0x02 - 0x7e之间的数值。

以上就是Android热修复及插件化原理详解的详细内容,更多关于Android热修复插件化的资料请关注我们其它相关文章!

时间: 2022-08-07

深入理解Android热修复技术原理之代码热修复技术

一.底层热替换原理 1.1.Andfix 回顾 我们先来看一下,为何唯独 Andfix 能够做到即时生效呢? 原因是这样的,在 app运行到一半的时候,所有需要发生变更的分类已经被加载过了,在Android 上是无法对一个分类进行卸载的.而腾讯系的方案,都是让 Classloader去加载新的类.如果不重启,原来的类还在虚拟机中,就无法加载新类.因此,只有在下次重启的时候,在还没走到业务逻辑之前抢先加载补丁中的新类,这样后续访问这个类时,就会Resolve 为新的类.从而达到热修复的目的. An

深入理解Android热修复技术原理之so库热修复技术

目录 一.SO库加载原理 二.SO库热部署实时生效可行性分析 2.1.动态注册 native 方法实时生效 2.2.静态注册 native 方法实时生效 2.3.SO实时生效方案总结 三.SO库冷部署重启生效实现方案 3.1.接口调用替换方案 3.2.反射注入方案 四.如何正确复制补丁 SO库 五.本章小结 一.SO库加载原理 Java Api 提供以下两个接口加载一个 so 库 System. loadLibrary (String libName):传进去的参数:so库名称, 表示的so 库

Android热修复Tinker接入及源码解读

一.概述 热修复这项技术,基本上已经成为项目比较重要的模块了.主要因为项目在上线之后,都难免会有各种问题,而依靠发版去修复问题,成本太高了. 现在热修复的技术基本上有阿里的AndFix.QZone的方案.美团提出的思想方案以及腾讯的Tinker等. 其中AndFix可能接入是最简单的一个(和Tinker命令行接入方式差不多),不过兼容性还是是有一定的问题的:QZone方案对性能会有一定的影响,且在Art模式下出现内存错乱的问题(其实这个问题我之前并不清楚,主要是tinker在MDCC上指出的);

深入理解Android热修复技术原理之资源热修复技术

一.普遍的实现方式 目前市面上的很多资源热修复方案基本上都是参考了 Instant Run的实现. 简要说来,Instant Run中的资源热修复分为两步: 1.构造一个新的 AssetManager,并通过反射调用 addAssetPath,把这个完 整的新资源包加入到AssetManager中.这样就得到了一个含有所有新资源的 AssetManager. 2.找到所有之前引用到原有 AssetManager的地方,通过反射,把引用处替换 为 AssetManager. 一个 Android

Android接入阿里云热修复介绍

1.AndroidManinifest.xml中加入权限 <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permissio

Android实现阿里云oss上传流程解析

购买了阿里云的oss空间,于是用它来存储图片,不过中间的使用算是出了些问题,导致很长的才成功. 不得不说,阿里云文档真的是无力吐槽...乱七八糟的.我完全是东拼西凑,才完成的图片上传功能. 走了很多的弯路,今天来记录下. 服务器上传: 阿里云上传分服务器上传和客户端上传,首先要分清,因为两边有差别的,服务器的上传简单很多,官方给的下载下来,输入配置的参数accessKeyId 和accessKeySecret 还有bucketName 就能够上传成功,很简单,这里也就不细说了. 客户端上传: 这

阿里云服务器手动实现mysql双机热备的两种方式

一.概念 1.热备份和备份的区别 热备份指的是:High Available(HA)即高可用,而备份指的是Backup,数据备份的一种.这是两种不同的概念,应对的产品也是两种功能上完全不同的产品.热备份主要保障业务的连续性,实现的方法是故障点的转移.而备份,主要目的是为了防止数据丢失,而做的一份拷贝,所以备份强调的是数据恢复而不是应用的故障转移. 2.什么是双机热备? 双机热备从广义上讲,就是对于重要的服务,使用两台服务器,互相备份,共同执行同一服务.当一台服务器出现故障时,可以由另一台服务器承

详解阿里云视频直播PHP-SDK接入教程

阿里云 视频直播 配置 及 PHP-SDK 接入教程 个人感觉,阿里的文档比微信的要坑很多- 微信最多是有些比较重要的东西放到比较不起眼.比较难发现的地方. 阿里的文档就是要么就是sdk已经更新了,但是文档还没更新,导致文档与SDK不匹配.在关键点卡我一下- 要么是同样的功能有好几个版本的SDK以及文档,却不告诉你哪个是最新的- 吐槽结束~ 准备工作 1.最重要的,先仔细看下视频直播文档! 2. 购买阿里云的视频直播服务 ,用量不大的时候建议使用按量收费的方式结算,接通之后消耗了流量才扣钱: 3

Android 阿里云OSS文件上传的实现示例

估计有小伙伴看到阿里云oss的api文档十脸懵逼了,啊哈哈哈,接下来博主就来拯救你了,看完觉得好的就关注博主吧,么么么么么哒!!! 想要自己研究阿里云oss的,博主开启传送门给你:阿里云oss 一.项目导入阿里云oss的SDK 方式1:在build文件添加依赖(博主和官方一致推荐) dependencies { //阿里云oss implementation 'com.aliyun.dpa:oss-android-sdk:+' } 方式2:源码编译jar包 源码传送门:官方阿里云oss源码 二.

详细介绍Java阿里云的短信验证码实现

阿里云手机短信验证码 第一步 登录阿里云开放平台 1.进入阿里云开放平台---->点击控制台 2.点击AccessKey管理 3.点击之后会弹出提示,选择开始使用子用户 4.新建一个用户组,然后按要求填写即可 5.创建一个用户,按要求填写内容,勾选编程访问 6.会得到AccessKey(id,密码) 要将这个账号保存下来! //用户登录名称 ==================== //AccessKey ID ==================== //AccessKey Secret ==

php与阿里云短信接口接入操作案例分析

本文实例讲述了php与阿里云短信接口接入操作.分享给大家供大家参考,具体如下: 使用阿里云短信API,需要在控制台获取以下必要参数,其中需要自己手机验证+官方审核多次,尤其审核需要保持耐心. 1. accessKeyId  相当于你的个人账户密钥: 2. accessKeySecret 与上是成对的: 3. SignName  个人签名,在发出去的短信中,这个签名会显示在开头,类似 [签名]亲爱的用户...... 这种格式,SignName需要通过提交审核: 4.TemplateCode  模板

Android so库的热更新问题

本来想写资源的热修复的,虽然方案差不多已经完成了,但是考虑到一些敏感问题,资源修复就不写了.那就来写写so的热修复,其原理和class的修复是一样的,但是so的热修复的需求并不高,就当做学习吧. 首先来总结一下Android的ClassLoader方式的热更新,这种方式类的查找过程是通过BaseDexClassLoader来完成的,最终会通过成员变量DexPathList对象中的findClass方法来查找类,代码如下: public Class findClass(String name, L

使用navicate连接阿里云服务器上的mysql

1.先进入服务器的mysql修改权限: GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '123456' WITH GRANT OPTION; 这里设置的登录登录用户为root,密码为: 2.在执行下面的语句刷新权限: FLUSH PRIVILEGES; 这里设置的是root用户,访问密码为123456 3.之后打开navicate,在常规设置密码和用户名,密码是上面设置的mysql密码123456 4.并且在ssh中添加阿里云的公

远程连接阿里云SqlServer 2012 数据库服务器的图文教程

前言: 在使用 阿里云 上的一些产品时,遇到不少坑. 安装IIS 时,遇到因买的配置过低,虚拟内存不足,而导致 IIS 总是安装失败: 现在 在上面安装了个 Sql Sever 2012,远程老是 不能连接,百度找半天,终于能够连接上了. 实现步骤如下: 1. 找到 安全组配置,打开 安全组配置,点击配置规则,增加 地址段访问的 授权规则,Sql Server的默认端口时 1433 . 2. 服务器上 win + R  键入  compmgmt.msc ,打开 计算机管理,按照如图所示 设置.注