Merge pull request #1181 from ag2s20150909/master

Cronet:利用网络加载Cronet so库,减少安装包大小。
pull/1184/head
kunfei 3 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"
applicationId "io.legado.cronet"
manifestPlaceholders = [APP_CHANNEL_VALUE: "cronet"]
ndk {
abiFilters 'arm64-v8a'
}
// ndk {
// abiFilters 'arm64-v8a','armeabi-v7a'
// }
}
}
compileOptions {
@ -123,7 +123,6 @@ kapt {
dependencies {
coreLibraryDesugaring('com.android.tools:desugar_jdk_libs:1.1.5')
implementation(fileTree(dir: 'libs', include: ['*.jar', '*.aar']))
testImplementation('junit:junit:4.13.2')
androidTestImplementation('androidx.test:runner:1.4.0')
androidTestImplementation('androidx.test.espresso:espresso-core:3.4.0')
@ -188,10 +187,14 @@ dependencies {
//
implementation('com.squareup.okhttp3:okhttp:4.9.1')
def cronet_version = '92.0.4509.1'
compileOnly("org.microg:cronet-api:$cronet_version")
cronetImplementation("org.microg:cronet-native:$cronet_version")
cronetImplementation("org.microg:cronet-api:$cronet_version")
cronetImplementation("org.microg:cronet-common:$cronet_version")
//compileOnly("org.microg:cronet-api:$cronet_version")
compileOnly(fileTree(dir: 'cronetlib', include: ['*.jar', '*.aar']))
cronetImplementation(fileTree(dir: 'cronetlib', include: ['*.jar', '*.aar']))
//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
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.LifecycleHelp
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.defaultSharedPreferences
@ -22,6 +23,11 @@ class App : MultiDexApplication() {
override fun onCreate() {
super.onCreate()
CrashHandler(this)
if (AppConfig.isCronet) {
//预下载Cronet so
CronetLoader.getInstance(this).preDownload()
}
LanguageUtils.setConfiguration(this)
createNotificationChannels()
applyDayNight(this)

@ -2,11 +2,13 @@ package io.legado.app.help.http
import io.legado.app.help.AppConfig
import io.legado.app.help.http.cronet.CronetInterceptor
import io.legado.app.help.http.cronet.CronetLoader
import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.ConnectionSpec
import okhttp3.Credentials
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import splitties.init.appCtx
import java.net.InetSocketAddress
import java.net.Proxy
import java.util.concurrent.ConcurrentHashMap
@ -43,7 +45,7 @@ val okHttpClient: OkHttpClient by lazy {
.build()
chain.proceed(request)
})
if (AppConfig.isCronet) {
if (AppConfig.isCronet&&CronetLoader.getInstance(appCtx).install()) {
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.UploadDataProviders
import org.chromium.net.UrlRequest
import org.chromium.net.urlconnection.CronetURLStreamHandlerFactory
import splitties.init.appCtx
import java.net.URL
import java.util.concurrent.Executor
import java.util.concurrent.Executors
@ -17,16 +19,58 @@ import java.util.concurrent.Executors
val executor: Executor by lazy { Executors.newSingleThreadExecutor() }
val cronetEngine: ExperimentalCronetEngine by lazy {
CronetLoader.getInstance(appCtx).preDownload()
val builder = ExperimentalCronetEngine.Builder(appCtx)
//设置自定义so库加载
.setLibraryLoader(CronetLoader.getInstance(appCtx))
//设置缓存路径
.setStoragePath(appCtx.externalCacheDir?.absolutePath)
//设置缓存模式
.enableHttpCache(HTTP_CACHE_DISK, (1024 * 1024 * 50))
//设置支持http/3
.enableQuic(true)
.enablePublicKeyPinningBypassForLocalTrustAnchors(true)
//设置支持http/2
.enableHttp2(true)
.enablePublicKeyPinningBypassForLocalTrustAnchors(true)
//.enableNetworkQualityEstimator(true)
//Brotli压缩
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.
buildscript {
ext.kotlin_version = '1.5.21'
ext.kotlin_version = '1.5.30-M1'
repositories {
google()
mavenCentral()

Loading…
Cancel
Save