|
|
|
|
@ -1,9 +1,6 @@ |
|
|
|
|
package com.github.catvod.crawler; |
|
|
|
|
|
|
|
|
|
import android.content.Context; |
|
|
|
|
import android.os.Handler; |
|
|
|
|
import android.os.Looper; |
|
|
|
|
import android.os.NetworkOnMainThreadException; |
|
|
|
|
import android.util.Log; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -17,21 +14,11 @@ import java.io.File; |
|
|
|
|
import java.io.FileOutputStream; |
|
|
|
|
import java.io.InputStream; |
|
|
|
|
import java.io.OutputStream; |
|
|
|
|
import java.io.PrintWriter; |
|
|
|
|
import java.io.StringWriter; |
|
|
|
|
import java.lang.reflect.Field; |
|
|
|
|
import java.lang.reflect.InvocationTargetException; |
|
|
|
|
import java.lang.reflect.Method; |
|
|
|
|
import java.util.HashMap; |
|
|
|
|
import java.util.LinkedHashMap; |
|
|
|
|
import java.util.Map; |
|
|
|
|
import java.util.Objects; |
|
|
|
|
import java.util.concurrent.ConcurrentHashMap; |
|
|
|
|
import java.util.concurrent.CountDownLatch; |
|
|
|
|
import java.util.concurrent.Executors; |
|
|
|
|
import java.util.concurrent.TimeUnit; |
|
|
|
|
import java.util.concurrent.TimeoutException; |
|
|
|
|
import java.util.concurrent.atomic.AtomicBoolean; |
|
|
|
|
|
|
|
|
|
import dalvik.system.DexClassLoader; |
|
|
|
|
import okhttp3.Response; |
|
|
|
|
@ -71,177 +58,66 @@ public class JarLoader { |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private boolean loadClassLoader(String jar, String key) { |
|
|
|
|
final String TAG = "JarLoader"; |
|
|
|
|
if (classLoaders.containsKey(key)){ |
|
|
|
|
Log.i(TAG, "loadClassLoader jar缓存: " + key); |
|
|
|
|
Log.i("JarLoader", "echo-loadClassLoader jar缓存: " + key); |
|
|
|
|
return true; |
|
|
|
|
} |
|
|
|
|
final File jarFile = new File(jar); |
|
|
|
|
final AtomicBoolean success = new AtomicBoolean(false); |
|
|
|
|
DexClassLoader classLoader; |
|
|
|
|
// 1. 前置校验
|
|
|
|
|
if (!validateJarFile(jarFile, TAG)) return false; |
|
|
|
|
// 2. 准备缓存目录
|
|
|
|
|
File cacheDir = prepareCacheDir(TAG); |
|
|
|
|
if (cacheDir == null) return false; |
|
|
|
|
classLoader = createDexClassLoader(jarFile, cacheDir, TAG); |
|
|
|
|
if (classLoader == null) return false; |
|
|
|
|
int retryCount = 0; |
|
|
|
|
final int maxRetries = 2; // 减少重试次数,增加超时检测
|
|
|
|
|
final long retryInterval = 200; // 增加重试间隔
|
|
|
|
|
while (retryCount < maxRetries && !success.get()) { |
|
|
|
|
try { |
|
|
|
|
Class<?> initClass = classLoader.loadClass("com.github.catvod.spider.Init"); |
|
|
|
|
Method initMethod = initClass.getMethod("init", Context.class); |
|
|
|
|
// 4.2 异步执行初始化(解决主线程网络问题)
|
|
|
|
|
executeInitInBackground(initMethod, success, TAG); |
|
|
|
|
// 4.3 处理初始化结果
|
|
|
|
|
if (success.get()) { |
|
|
|
|
handlePostInit(classLoader, key, TAG); |
|
|
|
|
classLoaders.put(key, classLoader); |
|
|
|
|
Log.i(TAG, "JAR加载成功: " + jar); |
|
|
|
|
return true; |
|
|
|
|
} |
|
|
|
|
} catch (ClassNotFoundException e) { |
|
|
|
|
Log.w(TAG, "Init类未找到,重试: " + (++retryCount) + "/" + maxRetries); |
|
|
|
|
sleep(retryInterval); |
|
|
|
|
} catch (Exception e) { |
|
|
|
|
Log.w(TAG, "Init类 加载失败"); |
|
|
|
|
break; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 5. 清理资源
|
|
|
|
|
cleanupResources(classLoader, TAG); |
|
|
|
|
return false; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ------------------- 辅助方法 -------------------
|
|
|
|
|
private boolean validateJarFile(File jarFile, String tag) { |
|
|
|
|
if (!jarFile.exists() || !jarFile.isFile() || jarFile.length() == 0) { |
|
|
|
|
Log.e(tag, "JAR文件无效: " + jarFile); |
|
|
|
|
return false; |
|
|
|
|
} |
|
|
|
|
return true; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private File prepareCacheDir(String tag) { |
|
|
|
|
File cacheDir = new File(App.getInstance().getCacheDir(), "catvod_csp"); |
|
|
|
|
try { |
|
|
|
|
if (!cacheDir.exists() && !cacheDir.mkdirs()) { |
|
|
|
|
Log.e(tag, "目录创建失败: " + cacheDir); |
|
|
|
|
return null; |
|
|
|
|
} |
|
|
|
|
return cacheDir; |
|
|
|
|
} catch (SecurityException e) { |
|
|
|
|
Log.e(tag, "目录访问拒绝: " + e.getMessage()); |
|
|
|
|
return null; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private DexClassLoader createDexClassLoader(File jarFile, File cacheDir, String tag) { |
|
|
|
|
try { |
|
|
|
|
return new DexClassLoader( |
|
|
|
|
jarFile.getAbsolutePath(), |
|
|
|
|
cacheDir.getAbsolutePath(), |
|
|
|
|
null, |
|
|
|
|
App.getInstance().getClassLoader() |
|
|
|
|
); |
|
|
|
|
} catch (Exception e) { |
|
|
|
|
Log.e(tag, "类加载器创建失败", e); |
|
|
|
|
return null; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private void executeInitInBackground(Method initMethod, AtomicBoolean successFlag, String tag) { |
|
|
|
|
final CountDownLatch latch = new CountDownLatch(1); |
|
|
|
|
final Throwable[] exceptionHolder = {null}; |
|
|
|
|
|
|
|
|
|
Executors.newSingleThreadExecutor().execute(() -> { |
|
|
|
|
try { |
|
|
|
|
initMethod.invoke(null, App.getInstance()); |
|
|
|
|
successFlag.set(true); |
|
|
|
|
} catch (InvocationTargetException e) { |
|
|
|
|
exceptionHolder[0] = e.getTargetException(); |
|
|
|
|
} catch (Exception e) { |
|
|
|
|
exceptionHolder[0] = e; |
|
|
|
|
} finally { |
|
|
|
|
latch.countDown(); |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
boolean success = false; |
|
|
|
|
try { |
|
|
|
|
if (!latch.await(6, TimeUnit.SECONDS)) { |
|
|
|
|
Log.e(tag, "初始化超时"); |
|
|
|
|
throw new TimeoutException("初始化未在6秒内完成"); |
|
|
|
|
} |
|
|
|
|
} catch (InterruptedException e) { |
|
|
|
|
Log.e(tag, "线程中断", e); |
|
|
|
|
Thread.currentThread().interrupt(); |
|
|
|
|
} catch (TimeoutException e) { |
|
|
|
|
throw new RuntimeException(e); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (exceptionHolder[0] != null) { |
|
|
|
|
handleInitException(exceptionHolder[0], tag); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private void handlePostInit(ClassLoader loader, String key, String tag) { |
|
|
|
|
// 主线程处理后续操作
|
|
|
|
|
new Handler(Looper.getMainLooper()).post(() -> { |
|
|
|
|
try { |
|
|
|
|
Class<?> proxyClass = loader.loadClass("com.github.catvod.spider.Proxy"); |
|
|
|
|
Method method = proxyClass.getMethod("proxy", Map.class); |
|
|
|
|
proxyMethods.put(key, method); |
|
|
|
|
Log.d(tag, "代理方法加载成功"); |
|
|
|
|
} catch (Exception e) { |
|
|
|
|
Log.w(tag, "代理功能未启用: " + e.getMessage()); |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private void handleInitException(Throwable t, String tag) { |
|
|
|
|
StringWriter sw = new StringWriter(); |
|
|
|
|
t.printStackTrace(new PrintWriter(sw)); |
|
|
|
|
|
|
|
|
|
Log.e(tag, "初始化失败: \n" + |
|
|
|
|
"类型: " + t.getClass().getName() + "\n" + |
|
|
|
|
"信息: " + (t.getMessage() != null ? t.getMessage() : "无详细消息") + "\n" + |
|
|
|
|
"堆栈: \n" + sw.toString()); |
|
|
|
|
|
|
|
|
|
if (t instanceof NetworkOnMainThreadException) { |
|
|
|
|
Log.w(tag, "建议: 第三方JAR包含主线程网络操作,请更新实现或联系开发者"); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
File cacheDir = new File(App.getInstance().getCacheDir().getAbsolutePath() + "/catvod_csp"); |
|
|
|
|
if (!cacheDir.exists()) |
|
|
|
|
cacheDir.mkdirs(); |
|
|
|
|
final DexClassLoader classLoader = new DexClassLoader(jar, cacheDir.getAbsolutePath(), null, App.getInstance().getClassLoader()); |
|
|
|
|
int count = 0; |
|
|
|
|
do { |
|
|
|
|
try { |
|
|
|
|
final Class<?> classInit = classLoader.loadClass("com.github.catvod.spider.Init"); |
|
|
|
|
if (classInit != null) { |
|
|
|
|
final Method initMethod = classInit.getMethod("init", Context.class); |
|
|
|
|
// 在子线程中调用 init 方法,避免网络请求在主线程中执行
|
|
|
|
|
Thread initThread = new Thread(new Runnable() { |
|
|
|
|
@Override |
|
|
|
|
public void run() { |
|
|
|
|
try { |
|
|
|
|
initMethod.invoke(null, App.getInstance()); |
|
|
|
|
} catch (Exception e) { |
|
|
|
|
e.printStackTrace(); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
initThread.start(); |
|
|
|
|
initThread.join(); |
|
|
|
|
Log.i("JarLoader", "echo-自定义爬虫代码加载成功!"); |
|
|
|
|
success = true; |
|
|
|
|
try { |
|
|
|
|
Class<?> proxy = classLoader.loadClass("com.github.catvod.spider.Proxy"); |
|
|
|
|
Method proxyMethod = proxy.getMethod("proxy", Map.class); |
|
|
|
|
proxyMethods.put(key, proxyMethod); |
|
|
|
|
} catch (Throwable th) { |
|
|
|
|
// 可以记录错误日志
|
|
|
|
|
th.printStackTrace(); |
|
|
|
|
} |
|
|
|
|
break; |
|
|
|
|
} |
|
|
|
|
Thread.sleep(200); |
|
|
|
|
} catch (Throwable th) { |
|
|
|
|
th.printStackTrace(); |
|
|
|
|
} |
|
|
|
|
count++; |
|
|
|
|
} while (count < 2); |
|
|
|
|
|
|
|
|
|
private void cleanupResources(DexClassLoader loader, String tag) { |
|
|
|
|
if (loader != null) { |
|
|
|
|
try { |
|
|
|
|
Field pathList = Objects.requireNonNull(loader.getClass().getSuperclass()).getDeclaredField("pathList"); |
|
|
|
|
pathList.setAccessible(true); |
|
|
|
|
Object dexPathList = pathList.get(loader); |
|
|
|
|
assert dexPathList != null; |
|
|
|
|
Field dexElements = dexPathList.getClass().getDeclaredField("dexElements"); |
|
|
|
|
dexElements.setAccessible(true); |
|
|
|
|
dexElements.set(dexPathList, new Object[0]); |
|
|
|
|
} catch (Exception e) { |
|
|
|
|
Log.w(tag, "资源清理失败", e); |
|
|
|
|
if (success) { |
|
|
|
|
classLoaders.put(key, classLoader); |
|
|
|
|
} |
|
|
|
|
} catch (Throwable th) { |
|
|
|
|
th.printStackTrace(); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private void sleep(long millis) { |
|
|
|
|
try { |
|
|
|
|
Thread.sleep(millis); |
|
|
|
|
} catch (InterruptedException ignored) { |
|
|
|
|
} |
|
|
|
|
return success; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
private DexClassLoader loadJarInternal(String jar, String md5, String key) { |
|
|
|
|
if (classLoaders.containsKey(key)){ |
|
|
|
|
Log.i("JarLoader", "loadJarInternal jar缓存: " + key); |
|
|
|
|
Log.i("JarLoader", "echo-loadJarInternal jar缓存: " + key); |
|
|
|
|
return classLoaders.get(key); |
|
|
|
|
} |
|
|
|
|
File cache = new File(App.getInstance().getFilesDir().getAbsolutePath() + "/" + key + ".jar"); |
|
|
|
|
@ -280,7 +156,7 @@ public class JarLoader { |
|
|
|
|
|
|
|
|
|
public Spider getSpider(String key, String cls, String ext, String jar) { |
|
|
|
|
if (spiders.containsKey(key)) { |
|
|
|
|
Log.i("JarLoader", "getSpider spider缓存: " + key); |
|
|
|
|
Log.i("JarLoader", "echo-getSpider spider缓存: " + key); |
|
|
|
|
return spiders.get(key); |
|
|
|
|
} |
|
|
|
|
String clsKey = cls.replace("csp_", ""); |
|
|
|
|
|