Merge pull request #1181 from ag2s20150909/master

Cronet:利用网络加载Cronet so库,减少安装包大小。
pull/1184/head
kunfei 4 years ago committed by GitHub
commit 1df2aaf187
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 19
      app/build.gradle
  2. BIN
      app/cronetlib/cronet_api.jar
  3. BIN
      app/cronetlib/cronet_impl_common_java.jar
  4. BIN
      app/cronetlib/cronet_impl_native_java.jar
  5. BIN
      app/cronetlib/cronet_impl_platform_java.jar
  6. BIN
      app/cronetlib/src/cronet_api-src.jar
  7. BIN
      app/cronetlib/src/cronet_impl_common_java-src.jar
  8. BIN
      app/cronetlib/src/cronet_impl_native_java-src.jar
  9. BIN
      app/cronetlib/src/cronet_impl_platform_java-src.jar
  10. 6
      app/src/main/java/io/legado/app/App.kt
  11. 4
      app/src/main/java/io/legado/app/help/http/HttpHelper.kt
  12. 48
      app/src/main/java/io/legado/app/help/http/cronet/CronetHelper.kt
  13. 355
      app/src/main/java/io/legado/app/help/http/cronet/CronetLoader.java
  14. 2
      build.gradle

@ -89,9 +89,9 @@ android {
dimension "mode" dimension "mode"
applicationId "io.legado.cronet" applicationId "io.legado.cronet"
manifestPlaceholders = [APP_CHANNEL_VALUE: "cronet"] manifestPlaceholders = [APP_CHANNEL_VALUE: "cronet"]
ndk { // ndk {
abiFilters 'arm64-v8a' // abiFilters 'arm64-v8a','armeabi-v7a'
} // }
} }
} }
compileOptions { compileOptions {
@ -123,7 +123,6 @@ kapt {
dependencies { dependencies {
coreLibraryDesugaring('com.android.tools:desugar_jdk_libs:1.1.5') coreLibraryDesugaring('com.android.tools:desugar_jdk_libs:1.1.5')
implementation(fileTree(dir: 'libs', include: ['*.jar', '*.aar']))
testImplementation('junit:junit:4.13.2') testImplementation('junit:junit:4.13.2')
androidTestImplementation('androidx.test:runner:1.4.0') androidTestImplementation('androidx.test:runner:1.4.0')
androidTestImplementation('androidx.test.espresso:espresso-core:3.4.0') androidTestImplementation('androidx.test.espresso:espresso-core:3.4.0')
@ -188,10 +187,14 @@ dependencies {
// //
implementation('com.squareup.okhttp3:okhttp:4.9.1') implementation('com.squareup.okhttp3:okhttp:4.9.1')
def cronet_version = '92.0.4509.1' def cronet_version = '92.0.4509.1'
compileOnly("org.microg:cronet-api:$cronet_version") //compileOnly("org.microg:cronet-api:$cronet_version")
cronetImplementation("org.microg:cronet-native:$cronet_version") compileOnly(fileTree(dir: 'cronetlib', include: ['*.jar', '*.aar']))
cronetImplementation("org.microg:cronet-api:$cronet_version") cronetImplementation(fileTree(dir: 'cronetlib', include: ['*.jar', '*.aar']))
cronetImplementation("org.microg:cronet-common:$cronet_version") //cronetImplementation("org.microg:cronet-native:$cronet_version")
//cronetImplementation("org.microg:cronet-api:$cronet_version")
//cronetImplementation("org.microg:cronet-common:$cronet_version")
//cronetImplementation('com.huawei.hms:hquic-provider:5.0.1.300') {exclude( group :"org.chromium.net")}
//cronetImplementation('com.google.android.gms:play-services-cronet:17.0.1')
//Glide //Glide
implementation('com.github.bumptech.glide:glide:4.12.0') implementation('com.github.bumptech.glide:glide:4.12.0')

Binary file not shown.

@ -14,6 +14,7 @@ import io.legado.app.help.AppConfig
import io.legado.app.help.CrashHandler import io.legado.app.help.CrashHandler
import io.legado.app.help.LifecycleHelp import io.legado.app.help.LifecycleHelp
import io.legado.app.help.ThemeConfig.applyDayNight import io.legado.app.help.ThemeConfig.applyDayNight
import io.legado.app.help.http.cronet.CronetLoader
import io.legado.app.utils.LanguageUtils import io.legado.app.utils.LanguageUtils
import io.legado.app.utils.defaultSharedPreferences import io.legado.app.utils.defaultSharedPreferences
@ -22,6 +23,11 @@ class App : MultiDexApplication() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
CrashHandler(this) CrashHandler(this)
if (AppConfig.isCronet) {
//预下载Cronet so
CronetLoader.getInstance(this).preDownload()
}
LanguageUtils.setConfiguration(this) LanguageUtils.setConfiguration(this)
createNotificationChannels() createNotificationChannels()
applyDayNight(this) applyDayNight(this)

@ -2,11 +2,13 @@ package io.legado.app.help.http
import io.legado.app.help.AppConfig import io.legado.app.help.AppConfig
import io.legado.app.help.http.cronet.CronetInterceptor import io.legado.app.help.http.cronet.CronetInterceptor
import io.legado.app.help.http.cronet.CronetLoader
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.ConnectionSpec import okhttp3.ConnectionSpec
import okhttp3.Credentials import okhttp3.Credentials
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import splitties.init.appCtx
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.net.Proxy import java.net.Proxy
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
@ -43,7 +45,7 @@ val okHttpClient: OkHttpClient by lazy {
.build() .build()
chain.proceed(request) chain.proceed(request)
}) })
if (AppConfig.isCronet) { if (AppConfig.isCronet&&CronetLoader.getInstance(appCtx).install()) {
builder.addInterceptor(CronetInterceptor()) builder.addInterceptor(CronetInterceptor())
} }

@ -9,7 +9,9 @@ import org.chromium.net.CronetEngine.Builder.HTTP_CACHE_DISK
import org.chromium.net.ExperimentalCronetEngine import org.chromium.net.ExperimentalCronetEngine
import org.chromium.net.UploadDataProviders import org.chromium.net.UploadDataProviders
import org.chromium.net.UrlRequest import org.chromium.net.UrlRequest
import org.chromium.net.urlconnection.CronetURLStreamHandlerFactory
import splitties.init.appCtx import splitties.init.appCtx
import java.net.URL
import java.util.concurrent.Executor import java.util.concurrent.Executor
import java.util.concurrent.Executors import java.util.concurrent.Executors
@ -17,16 +19,58 @@ import java.util.concurrent.Executors
val executor: Executor by lazy { Executors.newSingleThreadExecutor() } val executor: Executor by lazy { Executors.newSingleThreadExecutor() }
val cronetEngine: ExperimentalCronetEngine by lazy { val cronetEngine: ExperimentalCronetEngine by lazy {
CronetLoader.getInstance(appCtx).preDownload()
val builder = ExperimentalCronetEngine.Builder(appCtx) val builder = ExperimentalCronetEngine.Builder(appCtx)
//设置自定义so库加载
.setLibraryLoader(CronetLoader.getInstance(appCtx))
//设置缓存路径
.setStoragePath(appCtx.externalCacheDir?.absolutePath) .setStoragePath(appCtx.externalCacheDir?.absolutePath)
//设置缓存模式
.enableHttpCache(HTTP_CACHE_DISK, (1024 * 1024 * 50)) .enableHttpCache(HTTP_CACHE_DISK, (1024 * 1024 * 50))
//设置支持http/3
.enableQuic(true) .enableQuic(true)
.enablePublicKeyPinningBypassForLocalTrustAnchors(true) //设置支持http/2
.enableHttp2(true) .enableHttp2(true)
.enablePublicKeyPinningBypassForLocalTrustAnchors(true)
//.enableNetworkQualityEstimator(true)
//Brotli压缩 //Brotli压缩
builder.enableBrotli(true) builder.enableBrotli(true)
return@lazy builder.build() //builder.setExperimentalOptions("{\"quic_version\": \"h3-29\"}")
val engine = builder.build()
URL.setURLStreamHandlerFactory(CronetURLStreamHandlerFactory(engine))
// engine.addRequestFinishedListener(object : RequestFinishedInfo.Listener(executor) {
// override fun onRequestFinished(requestFinishedInfo: RequestFinishedInfo?) {
// val sb = StringBuilder(requestFinishedInfo!!.url).append("\r\n")
//
// try {
// if (requestFinishedInfo.responseInfo != null) {
// val responseInfo = requestFinishedInfo.responseInfo
// if (responseInfo != null) {
// sb.append("[Cached:").append(responseInfo.wasCached())
// .append("][StatusCode:")
// .append(
// responseInfo.httpStatusCode
// ).append("][StatusText:").append(responseInfo.httpStatusText)
// .append("][Protocol:").append(responseInfo.negotiatedProtocol)
// .append("][ByteCount:").append(
// responseInfo.receivedByteCount
// ).append("]\r\n")
// }
// val httpHeaders = requestFinishedInfo.responseInfo!!
// .allHeadersAsList
// for ((key, value) in httpHeaders) {
// sb.append("[").append(key).append("]").append(value).append("\r\n")
// }
// Log.e("Cronet", sb.toString())
// }
// } catch (e: URISyntaxException) {
// e.printStackTrace()
// }
// }
// })
return@lazy engine
} }

@ -0,0 +1,355 @@
package io.legado.app.help.http.cronet;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Build;
import android.util.Log;
import org.chromium.net.CronetEngine;
import org.chromium.net.impl.ImplVersion;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigInteger;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.MessageDigest;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class CronetLoader extends CronetEngine.Builder.LibraryLoader {
//https://storage.googleapis.com/chromium-cronet/android/92.0.4515.127/Release/cronet/libs/arm64-v8a/libcronet.92.0.4515.127.so
//https://cdn.jsdelivr.net/gh/ag2s20150909/cronet-repo@92.0.4515.127/cronet/92.0.4515.127/arm64-v8a/libcronet.92.0.4515.127.so.js
private final String soName = "libcronet." + ImplVersion.getCronetVersion() + ".so";
private final String soUrl = "https://storage.googleapis.com/chromium-cronet/android/" + ImplVersion.getCronetVersion() + "/Release/cronet/libs/" + getCpuAbi() + "/" + soName;
private final String md5Url = "https://cdn.jsdelivr.net/gh/ag2s20150909/cronet-repo@" + ImplVersion.getCronetVersion() + "/cronet/" + ImplVersion.getCronetVersion() + "/" + getCpuAbi() + "/" + soName + ".js";
private final File soFile;
private final File downloadFile;
private static final String TAG = "CronetLoader";
private static CronetLoader instance;
public static CronetLoader getInstance(Context context) {
if (mContext == null) {
mContext = context;
}
if (instance == null) {
synchronized (CronetLoader.class) {
if (instance == null) {
instance = new CronetLoader(mContext);
}
}
}
return instance;
}
private static Context mContext;
CronetLoader(Context context) {
mContext = context.getApplicationContext();
File dir = mContext.getDir("lib", Context.MODE_PRIVATE);
soFile = new File(dir + "/" + getCpuAbi(), soName);
downloadFile = new File(mContext.getCacheDir() + "/so_download", soName);
}
public boolean install() {
return soFile.exists();
}
public void preDownload() {
new Thread(() -> {
String md5 = getUrlMd5(md5Url);
if (soFile.exists() && Objects.equals(md5, getFileMD5(soFile))) {
Log.e(TAG, "So 库已存在");
} else {
download(soUrl, md5, downloadFile, soFile);
}
Log.e(TAG, soName);
}).start();
}
@SuppressLint("UnsafeDynamicallyLoadedCode")
@Override
public void loadLibrary(String libName) {
Log.e(TAG, "libName:" + libName);
long start = System.currentTimeMillis();
try {
//非cronet的so调用系统方法加载
if (!libName.contains("cronet")) {
System.loadLibrary(libName);
return;
}
//以下逻辑为cronet加载,优先加载本地,否则从远程加载
//首先调用系统行为进行加载
System.loadLibrary(libName);
Log.e(TAG, "load from system");
} catch (Throwable e) {
//如果找不到,则从远程下载
//删除历史文件
deleteHistoryFile(Objects.requireNonNull(soFile.getParentFile()), soFile);
Log.e(TAG, "soUrl:" + soUrl);
String md5 = getUrlMd5(md5Url);
Log.e(TAG, "soMD5:" + md5);
Log.e(TAG, "soName+:" + soName);
Log.e(TAG, "destSuccessFile:" + soFile);
Log.e(TAG, "tempFile:" + downloadFile);
if (md5 == null || md5.length() != 32 || soUrl.length() == 0) {
//如果md5或下载的url为空,则调用系统行为进行加载
System.loadLibrary(libName);
return;
}
if (!soFile.exists() || !soFile.isFile()) {
//noinspection ResultOfMethodCallIgnored
soFile.delete();
download(soUrl, md5, downloadFile, soFile);
//如果文件不存在或不是文件,则调用系统行为进行加载
System.loadLibrary(libName);
return;
}
if (soFile.exists()) {
//如果文件存在,则校验md5值
String fileMD5 = getFileMD5(soFile);
if (fileMD5 != null && fileMD5.equalsIgnoreCase(md5)) {
//md5值一样,则加载
System.load(soFile.getAbsolutePath());
Log.e(TAG, "load from:" + soFile);
return;
}
//md5不一样则删除
//noinspection ResultOfMethodCallIgnored
soFile.delete();
}
//不存在则下载
download(soUrl, md5, downloadFile, soFile);
//使用系统加载方法
System.loadLibrary(libName);
} finally {
Log.e(TAG, "time:" + (System.currentTimeMillis() - start));
}
}
public static String getCpuAbi() {
//貌似只有这个过时了的API能获取当前APP使用的ABI
return Build.CPU_ABI;
}
private static String getUrlMd5(String url) {
InputStream inputStream;
OutputStream outputStream;
try {
outputStream = new ByteArrayOutputStream();
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
inputStream = connection.getInputStream();
byte[] buffer = new byte[32768];
int read;
while ((read = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, read);
outputStream.flush();
}
return outputStream.toString();
} catch (IOException e) {
return null;
}
}
/**
* 删除历史文件
*/
private static void deleteHistoryFile(File dir, File currentFile) {
File[] files = dir.listFiles();
if (files != null && files.length > 0) {
for (File f : files) {
if (f.exists() && (currentFile == null || !f.getAbsolutePath().equals(currentFile.getAbsolutePath()))) {
boolean delete = f.delete();
Log.e(TAG, "delete file: " + f + " result: " + delete);
if (!delete) {
f.deleteOnExit();
}
}
}
}
}
/**
* 下载文件
*/
private static boolean downloadFileIfNotExist(String url, File destFile) {
InputStream inputStream = null;
OutputStream outputStream = null;
try {
HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection();
inputStream = connection.getInputStream();
if (destFile.exists()) {
return true;
}
destFile.getParentFile().mkdirs();
destFile.createNewFile();
outputStream = new FileOutputStream(destFile);
byte[] buffer = new byte[32768];
int read;
while ((read = inputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, read);
outputStream.flush();
}
return true;
} catch (Throwable e) {
e.printStackTrace();
if (destFile.exists() && !destFile.delete()) {
destFile.deleteOnExit();
}
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return false;
}
static boolean download = false;
static Executor executor = Executors.newSingleThreadExecutor();
/**
* 下载并拷贝文件
*/
private static synchronized void download(final String url, final String md5, final File downloadTempFile, final File destSuccessFile) {
if (download) {
return;
}
download = true;
executor.execute(() -> {
boolean result = downloadFileIfNotExist(url, downloadTempFile);
Log.e(TAG, "download result:" + result);
//文件md5再次校验
String fileMD5 = getFileMD5(downloadTempFile);
if (md5 != null && !md5.equalsIgnoreCase(fileMD5)) {
boolean delete = downloadTempFile.delete();
if (!delete) {
downloadTempFile.deleteOnExit();
}
download = false;
return;
}
Log.e(TAG, "download success, copy to " + destSuccessFile);
//下载成功拷贝文件
copyFile(downloadTempFile, destSuccessFile);
File parentFile = downloadTempFile.getParentFile();
deleteHistoryFile(parentFile, null);
});
}
/**
* 拷贝文件
*/
private static boolean copyFile(File source, File dest) {
if (source == null || !source.exists() || !source.isFile() || dest == null) {
return false;
}
if (source.getAbsolutePath().equals(dest.getAbsolutePath())) {
return true;
}
FileInputStream is = null;
FileOutputStream os = null;
File parent = dest.getParentFile();
if (parent != null && (!parent.exists())) {
boolean mkdirs = parent.mkdirs();
if (!mkdirs) {
mkdirs = parent.mkdirs();
}
}
try {
is = new FileInputStream(source);
os = new FileOutputStream(dest, false);
byte[] buffer = new byte[1024 * 512];
int length;
while ((length = is.read(buffer)) > 0) {
os.write(buffer, 0, length);
}
return true;
} catch (Exception e) {
e.printStackTrace();
} finally {
if (is != null) {
try {
is.close();
} catch (Exception e) {
e.printStackTrace();
}
}
if (os != null) {
try {
os.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
return false;
}
/**
* 获得文件md5
*/
private static String getFileMD5(File file) {
FileInputStream fileInputStream = null;
try {
fileInputStream = new FileInputStream(file);
MessageDigest md5 = MessageDigest.getInstance("MD5");
byte[] buffer = new byte[1024];
int numRead = 0;
while ((numRead = fileInputStream.read(buffer)) > 0) {
md5.update(buffer, 0, numRead);
}
return String.format("%032x", new BigInteger(1, md5.digest())).toLowerCase();
} catch (Exception e) {
e.printStackTrace();
} catch (OutOfMemoryError e) {
e.printStackTrace();
} finally {
if (fileInputStream != null) {
try {
fileInputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
return null;
}
}

@ -1,7 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules. // Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript { buildscript {
ext.kotlin_version = '1.5.21' ext.kotlin_version = '1.5.30-M1'
repositories { repositories {
google() google()
mavenCentral() mavenCentral()

Loading…
Cancel
Save