APK的生成步骤:

大家从图中可以看出,大体分为以下 7 个大步: 1、打包资源文件,生成 R.java 文件

2、处理 aidl 文件,生成相应 java 文件

3、编译工程源代码,生成相应 class 文件

4、转换所有 class 文件,生成 classes.dex 文件

5、打包生成 apk

6、对 apk 文件进行签名

7、对签名的 apk 进行 zipalign 对其操作

说明:这里只是大致表明大致的打包编译过程,其实如果细分每一部分还有很多细节。

结合原理和渠道的 1、2 两种设置方法我们分别来说.

1、如果渠道信息是通过 Java 的硬编码方式来做的,我们可以在打包之前预处理 Java 源文件,找到渠道设置关键字,从渠道列表中找到一个渠道设置进去即可。由于脚本这块儿,不同的语言的实现方式不同,这里不做过多的说明。如果有需要的我可 以把自己之前 shell 写的一段代码分享了。

2、如果使用写在 AndroidManifext.xml 中,这个就可以通过读取 XML 文件的方式定位到 meta-data 并且 android:name 的值为 TDCHANNEL ID 的元节点。把这个元节点的值设置成某一个渠道即可。这里推荐大家看一下友盟开源的一个多渠道打包工具中有相关的实现细 [1]。这个过程发生在上图中的 aapt 阶段,这个阶段做的事情还比较多没有分来来说。

大致原理就是这样,其实也很简单~,就是在正常的 Android 打包编译过程中进行干涉,加入一些逻辑来替换相关的渠道信息,保证打包之后的 APK 中的渠道信息各不相同。 其实这个过程是这样的:

02.友盟的多渠道打包

说明:

1.什么是多渠道包?

渠道包就是要在安装包中添加渠道信息,也就是channel,对应不同的渠道,例如:小米市场、360市场、应用宝市场等

2.为什么要提供多渠道包?

我们要在安装包中添加不同的标识,应用在请求网络的时候携带渠道信息,方便后台做运营统计(这就是添加渠道信息的用处)。

3.实现多渠道打包的原理:

核心原理就是通过脚本修改androidManifest.xml中的mate-date内容,执行N次打包签名操作实现多渠道打包的需求。productFlavors

一般来讲,这个渠道的标识会放在AndroidManifest.xml的Application的一个Metadata中。然后就可以在java中通过API获取对应的数据了。

  • 原理:清单文件添加渠道标签读取对应值。
  • 打包后修改渠道值的两种方法
  • 第一种方法:
    通过ApkTool进行解包,然后修改AndroidManifest中修改渠道标示,最后再通过ApkTool进行打包、签名。
  • 第二种方法:
    使用AXML解析器axmleditor.jar,拥有很弱的编辑功能,工程中用来编辑二进制格式的 AndroidManifest.xml 文件.

最近不断有朋友向我咨询AndroidStudio多渠道的打包方法,今天整理一下之前积累的打包套路,写一篇文章,手把手的教给大家。
       说到多渠道,这里不得不提一下友盟统计,友盟统计是大家日常开发中常用的渠道统计工具,而我们的打包方法就是基于友盟统计实施的。按照友盟官方文档说明,渠道信息通常需要在AndroidManifest.xml中配置如下值:

<meta-data android:value="Channel ID" android:name="UMENG_CHANNEL"/>

上面的value值Channel_ID就是渠道标识。我们的期望的就是在编译时候这个值能够自动变化以满足区分多渠道的需求。

友盟多渠道打包

(一)在AndroidManifest.xml里设置动态渠道变量

<meta-data
    android:name="UMENG_CHANNEL"
    android:value="${UMENG_CHANNEL_VALUE}" />

(二)在build.gradle设置productFlavors

这里假定我们需要打包的渠道为酷安市场、360、小米、百度、豌豆荚
android {  
    productFlavors {
        kuan {
            manifestPlaceholders = [UMENG_CHANNEL_VALUE: "kuan"]
        }
        xiaomi {
            manifestPlaceholders = [UMENG_CHANNEL_VALUE: "xiaomi"]
        }
        qh360 {
            manifestPlaceholders = [UMENG_CHANNEL_VALUE: "qh360"]
        }
        baidu {
            manifestPlaceholders = [UMENG_CHANNEL_VALUE: "baidu"]
        }
        wandoujia {
            manifestPlaceholders = [UMENG_CHANNEL_VALUE: "wandoujia"]
        }
    }  
}

或者批量修改

android {  
    productFlavors {
        kuan {}
        xiaomi {}
        qh360 {}
        baidu {}
        wandoujia {}
    }  
    productFlavors.all { 
        flavor -> flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name] 
    }
}

所谓ProductFlavors其实就是可定义的产品特性,配合 manifest merger 使用的时候就可以达成在一次编译过程中产生多个具有自己特性配置的版本。上面这个配置的作用就是,为每个渠道包产生不同的 UMENG_CHANNEL_VALUE 的值。

如何实现多个 Apk 安装在同一设备

在之前的印象中,同一个应用在同一设备上只能安装一个,除非手动修改 AndroidManifest.xml 文件中的包名( package ),但这么做的后果就是新的应用真的是新的应用,旧版应用再也收不到更新。而现在你通过 Gradle,你可以轻松构建多个不同版本的应用,并且在同一设备上安装使用。

这里要用到 productFlavors ,productFlavors 可以用来自定义应用构建版本,我们可以用其 applicationId 属性来实现多个 Apk 安装在同一设备上。

build.gradle 中部分配置代码如下:

android {compileSdkVersion 24buildToolsVersion "24.0.1"//默认配置,所有 productFlavors 都会继承 defaultConfig 中配置的属性defaultConfig {//默认的 applicationId,一般与 AndroidManifest.xml 文件 package属性相同applicationId "com.littlejie.multichannel"minSdkVersion 15targetSdkVersion 24versionCode 1versionName "1.0"}// productFlavors 定义了一个应用的自定义构建版本//一个单一的项目可以同时定义多个不同的 flavor 来改变应用的输出。// productFlavors 这个概念是为了解决不同的版本之间的差异非常小的情况,通常用于区分同一个应用的不同渠道/客户等,可包含少量业务功能差别。// productFlavors 中的 flavor 不能跟 buildType 中的一样,否则会报: "ProductFlavor names cannot collide with BuildType names"productFlavors {//默认版本,不设置 applicationId ,继承 defaultConfig 中的配置flavors_default {}//开发版本, applicationId 替换为 com.littlejie.multichannel.devflavors_dev {applicationId "com.littlejie.multichannel.dev"}//发布版本, applicationId 替换为 com.littlejie.multichannel.releaseflavors_release {applicationId "com.littlejie.multichannel.release"}}
}

来源: https://www.cnblogs.com/travellife/p/Gradle-shi-xian-Android-duo-qu-dao-ding-zhi-hua-da.html

6、缺点: 
这样的打包方式效率比较低下,如果是几十个包还可以应付,打一个包快的话需要十几秒,慢的话需要几分钟不等,跟机器性能很有关系。

这种方式缺点非常明显,大部分情况下打渠道包只是为了修改一个渠道号,这种方式完全是杀鸡用牛刀,非常耗费时间。而且很多时候是要混淆代码的,如果多次打包就会生成多个mapping文件,在进行错误信息的查看时,不同渠道使用不同的mapping文件,想想就酸爽。

、美团多渠道打包

整个APK(ZIP文件格式)会被分为以下四个区块:

  • Contents of ZIP entries(from offset 0 until the start of APK Signing Block)
  • APK Signing Block
  • ZIP Central Directory
  • ZIP End of Central Directory

    apk-sections.png

这个是V2签名包的APK包格式,新的应用签名方案有着良好的向后兼容性,能完全兼容低于Android 7.0(Nougat)的版本。对比旧签名方案,它有更快的验证速度和更安全的保护。
区块1、3、4都是受保护区块,不允许修改保护区块。美团打包的方式,是在2区块内写入ID-value的扩展信息(渠道信息),并保存到APK中。这样,每打一个渠道包只需复制一个APK,然后在APK中添加一个ID-value即可,这种打包方式速度非常快,对一个30M大小的APK包只需要100多毫秒(包含文件复制时间)就能生成一个渠道包,而在运行时获取渠道信息只需要大约几毫秒的时间。

原理:

原理很简单,就是将渠道信息存放在APK文件的注释字段中。美团的打包方式非常快速,打渠道包几乎就只是进行一次copy apk文件。

把一个Android应用包当作zip文件包进行解压,然后发现在签名生成的目录下(META-INF)添加一个空文件不需要重新签名。利用这个机制,该文件的文件名就是渠道名。这种方式不需要重新签名等步骤,非常高效,但是貌似在Android7.0之后,Google为了增强签名的安全性,采用了新的签名规则,不是针对每个文件来进行数字编码,而是对zip包文件结构编码签名后产生一个唯一的数据叫做apk signing block。如果修改了zip文件的任何模块的内容,APK Signing Block都会发生改变,从而无法再绕过签名机制。

  • 1、直接将apk文件解压缩,然后在META-INF中间中添加以渠道名命名的空文件。
  • 2、代码中读取该文件名作为渠道名。
  • 该种方式不需要重新对apk进行签名,操作简单,也可将添加渠道文件的步骤做成Python脚本。实现自动化添加
  • 优缺点:需要将apk解压缩,如果APK包较大则解压缩所需时间较长。
  • 使用遇到的问题:使用爱加密加密过后,再按此种方式进行渠道包的添加,会导致在7.0及以上手机上无法安装
    • 解决办法:先分渠道打多个包,然后提交爱加密进行加密。

在打包之前我们需要了解一下Android studio 在打包签名过程中V1签名和V2签名的一个区别

这里可以看到:v1签名是对jar进行签名,V2签名是对整个apk签名:官方介绍就是:v2签名是在整个APK文件的二进制内容上计算和验证的,v1是在归档文件中解压缩文件内容。

二者签名所产生的结果:

v1:在v1中只对未压缩的文件内容进行了验证,所以在APK签名之后可以进行很多修改——文件可以移动,甚至可以重新压缩。即可以对签名后的文件在进行处理

v2:v2签名验证了归档中的所有字节,而不是单独的ZIP条目,如果您在构建过程中有任何定制任务,包括篡改或处理APK文件,请确保禁用它们,否则您可能会使v2签名失效,从而使您的APKs与Android 7.0和以上版本不兼容。

google官方最后也说了:一个APK可以同时由v1和v2签名同时签署,所以它仍然可以向后兼容以前的Android版本

这里开发经验表示 :

一定可行的方案: 只使用 v1 方案

不一定可行的方案:同时使用 v1 和 v2 方案

对 7.0 以下一定不行的方案:只使用 v2 方案

1, 如果要支持 Android 7.0 以下版本,那么尽量同时选择两种签

名方式,但是一旦遇到签名问题,可以只使用 v1 签名方案

2,如果需要对签名后的信息做处理修改,那就使用v1签名方案

3,如果最后遇到各种不同的问题,可以不勾选v1和v2,直接打包签名。

方法:

首先你需要去下载相关的工具:

详细步骤:

方法一:Python开发环境的安装,美团打包工具(地址1或地址2任选其一既可)

1、将要打包的apk放到PythonTool中 
2、在PythonTool/info/channel.txt中写入需要的渠道,一个渠道占一行 
3、双击执行PythonTool/MultiChannelBuildTool.py文件(需要Python环境),就会生成渠道包 
4、获取渠道信息:将JavaUtil文件中的ChannelUtil.java拷贝到工程,调用ChannelUtil.getChannel即可获取渠道.

步骤:

1.第一步 :配置build.gradle

在位于项目的根目录 build.gradle 文件中添加Walle Gradle插件的依赖, 如下:

第二步:创建channel,如下 txt文件

第三部:接下来就是打渠道包了。在这里我们用Android studio 的Terminal来进行打包:(Terminal 下 运行一句命令:gradlew clean assembleReleaseChannels)

方法二:

第一步:直接将PackerNg作为Utils拷贝到项目中。

package com.yshr.util;import java.io.BufferedReader;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;public final class PackerNg {private static final String TAG = PackerNg.class.getSimpleName();private static final String EMPTY_STRING = "";private static String sCachedMarket;public static String getMarket(final Object context) {return getMarket(context, EMPTY_STRING);}public static synchronized String getMarket(final Object context, final String defaultValue) {if (sCachedMarket == null) {sCachedMarket = getMarketInternal(context, defaultValue).market;}return sCachedMarket;}public static MarketInfo getMarketInfo(final Object context) {return getMarketInfo(context, EMPTY_STRING);}public static synchronized MarketInfo getMarketInfo(final Object context, final String defaultValue) {return getMarketInternal(context, defaultValue);}private static MarketInfo getMarketInternal(final Object context, final String defaultValue) {String market;Exception error;try {final String sourceDir = Helper.getSourceDir(context);market = Helper.readMarket(new File(sourceDir));error = null;} catch (Exception e) {market = null;error = e;}return new MarketInfo(market == null ? defaultValue : market, error);}public static class MarketInfo {public final String market;public final Exception error;public MarketInfo(final String market, final Exception error) {this.market = market;this.error = error;}@Overridepublic String toString() {return "MarketInfo{" +"market='" + market + '\'' +", error=" + error +'}';}}public static class Helper {static final String UTF_8 = "UTF-8";static final int ZIP_COMMENT_MAX_LENGTH = 65535;static final int SHORT_LENGTH = 2;static final byte[] MAGIC = new byte[]{0x21, 0x5a, 0x58, 0x4b, 0x21}; //!ZXK!// for android codeprivate static String getSourceDir(final Object context)throws ClassNotFoundException,InvocationTargetException,IllegalAccessException,NoSuchFieldException,NoSuchMethodException {final Class<?> contextClass = Class.forName("android.content.Context");final Class<?> applicationInfoClass = Class.forName("android.content.pm.ApplicationInfo");final Method getApplicationInfoMethod = contextClass.getMethod("getApplicationInfo");final Object appInfo = getApplicationInfoMethod.invoke(context);// try ApplicationInfo.publicSourceDirfinal Field publicSourceDirField = applicationInfoClass.getField("publicSourceDir");String sourceDir = (String) publicSourceDirField.get(appInfo);if (sourceDir == null) {// try ApplicationInfo.sourceDirfinal Field sourceDirField = applicationInfoClass.getField("sourceDir");sourceDir = (String) sourceDirField.get(appInfo);}if (sourceDir == null) {// try Context.getPackageCodePath()final Method getPackageCodePathMethod = contextClass.getMethod("getPackageCodePath");sourceDir = (String) getPackageCodePathMethod.invoke(context);}return sourceDir;}private static boolean isMagicMatched(byte[] buffer) {if (buffer.length != MAGIC.length) {return false;}for (int i = 0; i < MAGIC.length; ++i) {if (buffer[i] != MAGIC[i]) {return false;}}return true;}private static void writeBytes(byte[] data, DataOutput out) throws IOException {out.write(data);}private static void writeShort(int i, DataOutput out) throws IOException {ByteBuffer bb = ByteBuffer.allocate(SHORT_LENGTH).order(ByteOrder.LITTLE_ENDIAN);bb.putShort((short) i);out.write(bb.array());}private static short readShort(DataInput input) throws IOException {byte[] buf = new byte[SHORT_LENGTH];input.readFully(buf);ByteBuffer bb = ByteBuffer.wrap(buf).order(ByteOrder.LITTLE_ENDIAN);return bb.getShort(0);}public static void writeZipComment(File file, String comment) throws IOException {if (hasZipCommentMagic(file)) {throw new IllegalStateException("zip comment already exists, ignore.");}// {@see java.util.zip.ZipOutputStream.writeEND}byte[] data = comment.getBytes(UTF_8);final RandomAccessFile raf = new RandomAccessFile(file, "rw");raf.seek(file.length() - SHORT_LENGTH);// write zip comment length// (content field length + length field length + magic field length)writeShort(data.length + SHORT_LENGTH + MAGIC.length, raf);// write contentwriteBytes(data, raf);// write content lengthwriteShort(data.length, raf);// write magic byteswriteBytes(MAGIC, raf);raf.close();}public static boolean hasZipCommentMagic(File file) throws IOException {RandomAccessFile raf = null;try {raf = new RandomAccessFile(file, "r");long index = raf.length();byte[] buffer = new byte[MAGIC.length];index -= MAGIC.length;// read magic bytesraf.seek(index);raf.readFully(buffer);// check magic bytes matchedreturn isMagicMatched(buffer);} finally {if (raf != null) {raf.close();}}}public static String readZipComment(File file) throws IOException {RandomAccessFile raf = null;try {raf = new RandomAccessFile(file, "r");long index = raf.length();byte[] buffer = new byte[MAGIC.length];index -= MAGIC.length;// read magic bytesraf.seek(index);raf.readFully(buffer);// if magic bytes matchedif (isMagicMatched(buffer)) {index -= SHORT_LENGTH;raf.seek(index);// read content length fieldint length = readShort(raf);if (length > 0) {index -= length;raf.seek(index);// read content bytesbyte[] bytesComment = new byte[length];raf.readFully(bytesComment);return new String(bytesComment, UTF_8);} else {throw new IOException("zip comment content not found");}} else {throw new IOException("zip comment magic bytes not found");}} finally {if (raf != null) {raf.close();}}}private static String readZipCommentMmp(File file) throws IOException {final int mappedSize = 10240;final long fz = file.length();RandomAccessFile raf = null;MappedByteBuffer map = null;try {raf = new RandomAccessFile(file, "r");map = raf.getChannel().map(MapMode.READ_ONLY, fz - mappedSize, mappedSize);map.order(ByteOrder.LITTLE_ENDIAN);int index = mappedSize;byte[] buffer = new byte[MAGIC.length];index -= MAGIC.length;// read magic bytesmap.position(index);map.get(buffer);// if magic bytes matchedif (isMagicMatched(buffer)) {index -= SHORT_LENGTH;map.position(index);// read content length fieldint length = map.getShort();if (length > 0) {index -= length;map.position(index);// read content bytesbyte[] bytesComment = new byte[length];map.get(bytesComment);return new String(bytesComment, UTF_8);}}} finally {if (map != null) {map.clear();}if (raf != null) {raf.close();}}return null;}public static void writeMarket(final File file, final String market) throws IOException {writeZipComment(file, market);}public static String readMarket(final File file) throws IOException {return readZipComment(file);}public static boolean verifyMarket(final File file, final String market) throws IOException {return market.equals(readMarket(file));}public static void println(String msg) {System.out.println(TAG + ": " + msg);}public static List<String> parseMarkets(final File file) throws IOException {final List<String> markets = new ArrayList<String>();FileReader fr = new FileReader(file);BufferedReader br = new BufferedReader(fr);String line = null;int lineNo = 1;while ((line = br.readLine()) != null) {String parts[] = line.split("#");if (parts.length > 0) {final String market = parts[0].trim();if (market.length() > 0) {markets.add(market);} else {println("skip invalid market line " + lineNo + ":'" + line + "'");}} else {println("skip invalid market line" + lineNo + ":'" + line + "'");}++lineNo;}br.close();fr.close();return markets;}
}

第二步:创建一个保存渠道包名的txt文件,可以放在项目主目录下:比如命名market.txt
渠道名可以按照需求随便添加
anzhi
baidu
huawei
legend
letv
meizu
oppo
qq
PC
sougou
UC
update
update1
vivo
wandoujia
woshangdian
xiaomi

第三步:ChannelUtil这个工具类是用于取出文件里的渠道名

package com.yshr.util;import android.content.Context;
import android.text.TextUtils;import com.ztx.shudu.supermarket.app.App;
import com.ztx.shudu.supermarket.app.Constants;
import com.ztx.shudu.supermarket.model.prefs.ImplPreferencesHelper;public class ChannelUtil {private static String mChannel;/*** 返回市场。  如果获取失败返回""** @param context* @return*/public static String getChannel(Context context) {return getChannel(context, "default");
//        return getChannel(context, "sjzs360");}/*** 返回市场。  如果获取失败返回defaultChannel** @param context* @param defaultChannel* @return*/public static String getChannel(Context context, String defaultChannel) {//内存中获取if (!TextUtils.isEmpty(mChannel)) {return mChannel;}//sp中获取mChannel = getChannelBySharedPreferences(context);if (!TextUtils.isEmpty(mChannel)) {return mChannel;}mChannel = PackerNg.getMarket(context);if (!TextUtils.isEmpty(mChannel)) {//保存sp中备用saveChannelBySharedPreferences(context, mChannel);return mChannel;}//全部获取失败return defaultChannel;}/*** 本地保存channel & 对应版本号** @param context* @param channel*/private static void saveChannelBySharedPreferences(Context context, String channel) {
//        SharedPreferencesUtil.getInstance(context).applyString(Constants.Companion.getSUPERMARKET_CHANNEL(), channel);App.instance.getSharedPreferences(ImplPreferencesHelper.Companion.getSHAREDPREFERENCES_NAME(), Context.MODE_PRIVATE).edit().putString(Constants.Companion.getSUPERMARKET_CHANNEL(), "").apply();}}

优缺点:

优点: 
这种打包方式速度非常快,900多个渠道不到一分钟就能打完

缺点: 
1、google现在已经修改了新的签名规则,若使用新的签名规则则无法使用(老的无所谓)。

2.加固的apk会出现什么问题。
3、一些不法的渠道商很容易通过工具修改渠道,如果一个渠道商,通过网络劫持和篡改渠道的组合方式来获取暴利,对于程序开发者来说可能会存在着巨大的经济损失

360多渠道打包

  • 利用的是Zip文件“可以添加comment(摘要)”的数据结构特点,在文件的末尾写入任意数据,而不用重新解压zip文件
  • apk文件就是zip文件格式;
  • 注释的读取即渠道标识的读取
  • 不需要对apk文件解压缩和重新签名即可完成多渠道自动打包,高效速度快,无兼容性问题

Android 手把手完美教你美团wally(瓦力)多渠道打包 3种方法(针对签名V1和V2都会有很详细的介绍)相关推荐

  1. Android实现截屏和截长图功能的几种方法

    一般情况下各种型号的手机都会有自带的截屏功能,也会有诸如"开关机键+音量键"的截屏快捷键,只要手机是亮屏状态,都会将手机屏幕的可视区域(包含状态栏)全部截取下来. 如果开发中想要调 ...

  2. linux下logcat命令,Android shell命令行中过滤adb logcat输出的几种方法

    我们在Android开发中总能看到程序的log日志内容充满了屏幕,而真正对开发者有意义的信息被淹没在洪流之中,让开发者无所适从,严重影响开发效率.本文就具体介绍几种在shell命令行中过滤adb lo ...

  3. 【Android 教程系列第 28 篇】Android 分别使用 jarsigner 和 apksigner 对 APK 签名(v1、v2 签名)的详细教程

    这是[Android 教程系列第 28 篇],如果觉得有用的话,欢迎关注专栏. 为了增加 Apk 的安全性,防止反编译,我们会对开发的 Apk 做签名处理,如果要上架应用商店,部分还会要求 Apk 必 ...

  4. Android 实现截屏和截长图功能的几种方法

    欢迎大家关注我的公众号:**牛角尖尖上起舞** 一般情况下各种型号的手机都会有自带的截屏功能,也会有诸如"开关机键+音量键"的截屏快捷键,只要手机是亮屏状态,都会将手机屏幕的可视区 ...

  5. Android之UI线程与子线程交互设计的5种方法

    转载地址:http://www.cr173.com/html/19165_1.html 在android的设计思想中,为了确保用户顺滑的操作体验.一些耗时的任务不能够在UI线程中运行,像访问网络就属于 ...

  6. 21天学习之二(Android 10.0 SystemUI默认去掉底部导航栏的三种方法)

    活动地址:CSDN21天学习挑战赛 1.概述 在定制化开发中,在SystemUI的一些定制功能中,针对默认去掉底部导航栏的方法有好几种,StatusBar和DisplayPolicy.java中api ...

  7. android 真机dev tools,移动端手机调试的几种方法

    很多时候,我们在进行移动端开发时,都是先在PC端使用手机模拟器进行调试,没有问题后,我们才会在手机端的浏览器进行测试,这个时候,如果没有出现问题,皆大欢喜.但是一旦出现问题,我们就很难解决,因为缺乏可 ...

  8. android手机禁止休眠_Android应用禁止屏幕休眠的3种方法

    做Android应用开发时,有时需要在应用前台运行时,禁止休眠,以下几种方法供参考. 方法一:持有WakeLock 添加休眠锁,休眠锁必须成对出现. private WakeLock mWakeLoc ...

  9. 手把手教你写web全栈入门项目—React+Koa+MongoDB(3w字教程,真的很详细,有代码)

    手把手教你写web全栈入门项目-React+Koa+MongoDB

最新文章

  1. 【转载】MongoDB安装并随windows开机自启
  2. c语言 char operator,C语言取模运算符(modulus operator)“%”的作用是什么
  3. Android studio/sdk/appium/jdk遇到的坑坑洼洼
  4. linux检测病毒工具,Linux下查杀病毒工具
  5. ntrip获取源列表_Ntrip协议简介(转)
  6. mac 无法识别android,mac 无法识别android真机
  7. 数学分析 反函数存在性定理,连续性定理与求导定理
  8. 虚拟机安装Ubuntu解决自带Firefox浏览器不能看视频,提示无Flash插件问题!安装flash!
  9. 绘制附带 AHT20 温湿度传感器的 stm32 电路原理图
  10. js移除某个样式_JS removeAttribute()方法:删除元素的某个属性
  11. uniapp获取屏幕宽度的方式_uni-app如何实现rem自适应手机屏幕尺寸?
  12. 带你了解“不拘一格去创新,别出心裁入场景”的锐捷
  13. mysql created_tmp_tables_Created_tmp_tables和Created_tmp_disk_tables参数一问
  14. 小狐狸被抛弃?火狐浏览器官网域名仍为firefox.com.cn
  15. 博客从CSDN迁移至简书啦
  16. 论文笔记(十七):Brax — A Differentiable Physics Engine for Large Scale Rigid Body Simulation
  17. Android 地图导航调用百度地图、高德地图、腾讯地图,腾讯T3团队整理
  18. 一台计算机只能注册一台sql,局域网中的一台电脑为啥连接不到另一台电脑中的SQL远程数据库...
  19. “探讨下一代公链和 DApps 生态前景“ Conflux首次线下 meetup 在京举办
  20. 绝缘栅型n沟道场管_技术小科普—MOS管场效应管(MOSFET)详解

热门文章

  1. 基于VMware采用kubeadm方式部署单节点k8s集群(练习使用)
  2. PAT 1004 解析
  3. Can not find java process. Try to run `jps` command lists the instrumented Java HotSpot VMs
  4. MySql超详细总结(珍藏版)
  5. 哪些软件可以用于统计数据
  6. 简单的信息管理应用(宾馆订房系统项目)
  7. Java第三方登录(QQ,微博,微信)
  8. 7 Ways To Free Up Hard Disk Space On Windows
  9. gb g t计算机单位,t和g的换算(硬盘容量G跟T的换算)
  10. 中考化学计算机题教案,初三化学教案:化学反应中的有关计算