打包jar文件 外部调用资源 so等

时间:2022-05-06 15:00:49

一个非常好的从jar文件中加载so动态库方法,在android的gif支持开源中用到。这个项目的gif解码是用jni c实现的,避免了OOM等问题。

项目地址:https://github.com/koral--/android-gif-drawable

如果是把java文件生成jar。jni生成的so文件放到使用apk的libs/armeabi/lib_gif.so.....

gifExample.apk:

/libs/gif.jar

/libs/armeabi/lib_gi.so

这样做会报错,提示xml里面找不到GifImageView。

只能用项目之间依赖,so会自动进入生成的apk,不用拷贝。

调用方法:

//开始调用:
static {
LibraryLoader.loadLibrary(null, LibraryLoader.BASE_LIBRARY_NAME);
}

进入这里:

package pl.droidsonroids.gif;

import android.content.Context;
import android.support.annotation.NonNull; import java.lang.reflect.Method; /**
* Helper used to work around native libraries loading on some systems.
* See <a href="https://medium.com/keepsafe-engineering/the-perils-of-loading-native-libraries-on-android-befa49dce2db">ReLinker</a> for more details.
*/
public class LibraryLoader {
static final String SURFACE_LIBRARY_NAME = "pl_droidsonroids_gif_surface";
static final String BASE_LIBRARY_NAME = "pl_droidsonroids_gif";
private static Context sAppContext; /**
* Intitializes loader with given `Context`. Subsequent calls should have no effect since application Context is retrieved.
* Libraries will not be loaded immediately but only when needed.
* @param context any Context except null
*/
public static void initialize(@NonNull final Context context) {
sAppContext = context.getApplicationContext();
} static Context getContext() {
if (sAppContext == null) {
try {
final Class<?> activityThread = Class.forName("android.app.ActivityThread");
final Method currentApplicationMethod = activityThread.getDeclaredMethod("currentApplication");
sAppContext = (Context) currentApplicationMethod.invoke(null);
} catch (Exception e) {
throw new RuntimeException("LibraryLoader not initialized. Call LibraryLoader.initialize() before using library classes.", e);
}
}
return sAppContext;
} static void loadLibrary(Context context, final String library) {
try {
System.loadLibrary(library);
} catch (final UnsatisfiedLinkError e) {
if (SURFACE_LIBRARY_NAME.equals(library)) {
loadLibrary(context, BASE_LIBRARY_NAME);
}
if (context == null) {
context = getContext();
}
ReLinker.loadLibrary(context, library);
}
}
}

最终到这里:

 /**
* Copyright 2015 KeepSafe Software, Inc.
* <p/>
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p/>
* http://www.apache.org/licenses/LICENSE-2.0
* <p/>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package pl.droidsonroids.gif; import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.os.Build; import java.io.Closeable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile; /**
* Based on https://github.com/KeepSafe/ReLinker
* ReLinker is a small library to help alleviate {@link UnsatisfiedLinkError} exceptions thrown due
* to Android's inability to properly install / load native libraries for Android versions before
* API 21
*/
class ReLinker {
private static final String LIB_DIR = "lib";
private static final int MAX_TRIES = 5;
private static final int COPY_BUFFER_SIZE = 8192; private ReLinker() {
// No instances
} /**
* Utilizes the regular system call to attempt to load a native library. If a failure occurs,
* then the function extracts native .so library out of the app's APK and attempts to load it.
* <p/>
* <strong>Note: This is a synchronous operation</strong>
*/
static void loadLibrary(Context context, final String library) {
final String libName = System.mapLibraryName(library);
synchronized (ReLinker.class) {
final File workaroundFile = unpackLibrary(context, libName);
System.load(workaroundFile.getAbsolutePath());
}
} /**
* Attempts to unpack the given library to the workaround directory. Implements retry logic for
* IO operations to ensure they succeed.
*
* @param context {@link Context} to describe the location of the installed APK file
* @param libName The name of the library to load
*/
private static File unpackLibrary(final Context context, final String libName) {
File outputFile = new File(context.getDir(LIB_DIR, Context.MODE_PRIVATE), libName);// + BuildConfig.VERSION_NAME);
if (outputFile.isFile()) {
return outputFile;
} final File cachedLibraryFile = new File(context.getCacheDir(), libName );//+ BuildConfig.VERSION_NAME);
if (cachedLibraryFile.isFile()) {
return cachedLibraryFile;
} final FilenameFilter filter = new FilenameFilter() {
@Override
public boolean accept(File dir, String filename) {
return filename.startsWith(libName);
}
};
clearOldLibraryFiles(outputFile, filter);
clearOldLibraryFiles(cachedLibraryFile, filter); final ApplicationInfo appInfo = context.getApplicationInfo();
final File apkFile = new File(appInfo.sourceDir);
ZipFile zipFile = null;
try {
zipFile = openZipFile(apkFile); int tries = 0;
while (tries++ < MAX_TRIES) {
ZipEntry libraryEntry = getLibraryEntry(libName, zipFile); InputStream inputStream = null;
FileOutputStream fileOut = null;
try {
inputStream = zipFile.getInputStream(libraryEntry);
fileOut = new FileOutputStream(outputFile);
copy(inputStream, fileOut);
} catch (IOException e) {
if (tries > MAX_TRIES / 2) {
outputFile = cachedLibraryFile;
}
continue;
} finally {
closeSilently(inputStream);
closeSilently(fileOut);
}
setFilePermissions(outputFile);
break;
}
} finally {
closeSilently(zipFile);
}
return outputFile;
} @SuppressWarnings("deprecation") //required for old API levels
private static ZipEntry getLibraryEntry(final String libName, final ZipFile zipFile) {
String jniNameInApk; ZipEntry libraryEntry = null;
// if (Build.VERSION.SDK_INT >= 21 && Build.SUPPORTED_ABIS.length > 0) {
// for (final String ABI : Build.SUPPORTED_ABIS) {
// jniNameInApk = "lib/" + ABI + "/" + libName;
// libraryEntry = zipFile.getEntry(jniNameInApk);
//
// if (libraryEntry != null) {
// break;
// }
// }
// } else {
jniNameInApk = "lib/" + Build.CPU_ABI + "/" + libName;
libraryEntry = zipFile.getEntry(jniNameInApk);
} if (libraryEntry == null) {
throw new IllegalStateException("Library " + libName + " for supported ABIs not found in APK file");
}
return libraryEntry;
} private static ZipFile openZipFile(final File apkFile) {
int tries = 0;
ZipFile zipFile = null;
while (tries++ < MAX_TRIES) {
try {
zipFile = new ZipFile(apkFile, ZipFile.OPEN_READ);
break;
} catch (IOException ignored) {
}
} if (zipFile == null) {
throw new RuntimeException("Could not open APK file: " + apkFile.getAbsolutePath());
}
return zipFile;
} @SuppressWarnings("ResultOfMethodCallIgnored") //intended, nothing useful can be done
private static void clearOldLibraryFiles(final File outputFile, final FilenameFilter filter) {
final File[] fileList = outputFile.getParentFile().listFiles(filter);
if (fileList != null) {
for (File file : fileList) {
file.delete();
}
}
} @SuppressWarnings("ResultOfMethodCallIgnored") //intended, nothing useful can be done
@SuppressLint("SetWorldReadable") //intended, default permission
private static void setFilePermissions(File outputFile) {
// Try change permission to rwxr-xr-x
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
outputFile.setReadable(true, false);
outputFile.setExecutable(true, false);
outputFile.setWritable(true);
}
} /**
* Copies all data from an {@link InputStream} to an {@link OutputStream}.
*
* @param in The stream to read from.
* @param out The stream to write to.
* @throws IOException when a stream operation fails.
*/
private static void copy(InputStream in, OutputStream out) throws IOException {
final byte[] buf = new byte[COPY_BUFFER_SIZE];
while (true) {
final int bytesRead = in.read(buf);
if (bytesRead == -1) {
break;
}
out.write(buf, 0, bytesRead);
}
} /**
* Closes a {@link Closeable} silently (without throwing or handling any exceptions)
*
* @param closeable {@link Closeable} to close
*/
private static void closeSilently(final Closeable closeable) {
try {
if (closeable != null) {
closeable.close();
}
} catch (IOException ignored) {
}
}
}

获取当前apk路径

final ApplicationInfo appInfo = context.getApplicationInfo();
Log.d("zhibin","appInfo.sourceDir: "+ appInfo.sourceDir);
输出:/system/app/xxx.apk


对应sdk 5.0以下版本,修正一些不支持的变量:

    @SuppressWarnings("deprecation") //required for old API levels
private static ZipEntry getLibraryEntry(final String libName, final ZipFile zipFile) {
String jniNameInApk; ZipEntry libraryEntry = null;
// if (Build.VERSION.SDK_INT >= 21 && Build.SUPPORTED_ABIS.length > 0) {
// for (final String ABI : Build.SUPPORTED_ABIS) {
// jniNameInApk = "lib/" + ABI + "/" + libName;
// libraryEntry = zipFile.getEntry(jniNameInApk);
//
// if (libraryEntry != null) {
// break;
// }
// }
// } else {
jniNameInApk = "lib/" + Build.CPU_ABI + "/" + libName;
Log.d("zhibin","Search directory for jniNameInApk: "+ jniNameInApk);
libraryEntry = zipFile.getEntry(jniNameInApk); //直接指定
if(libraryEntry == null){
jniNameInApk = "lib/armeabi" + "/" + libName;
Log.d("zhibin","Correct it to jniNameInApk: "+ jniNameInApk);
libraryEntry = zipFile.getEntry(jniNameInApk);
}
} if (libraryEntry == null) {
throw new IllegalStateException("Library " + libName + " for supported ABIs not found in APK file");
}
return libraryEntry;
}

共外部调用资源:

背景:工作中需要开发一个广告插件,并提供给其它人使用。这里就需要把自己的插件程序,打成jar来提供给他人引用。
但是遇到一个问题:插件程序中无法使用资源文件。

试过以下几种方式解决:

1、从插件程序中导出jar包
论坛上有人说导出的jar包中无法包含Drawable等资源文件,一些图片等数据,需要放到Assert文件中使用。
其实,关于这个问题,我做了尝试:
首先,需要说明导出jar包含什么文件是由你导出时选择来决定的,比如下图:
打包jar文件 外部调用资源 so等
如果你选择了res文件夹,则打包出的jar文件是可以包含res文件到。

但是包含文件并不代表可以使用。如果你想当然在插件程序中使用R.drawable.XXXX等方式获取
资源会报错!
当然别人通过R.XX.XX也只能看到自己的资源文件,而无法获取jar中的资源文件。

2、获取jar包中的文件

虽然无法直接引用资源文件,但是如果外边程序想获取某个资源文件时,也是可行的。
其原理是以数据流读取jar中指定的文件。
比如读取Assert文件下的icon.jpg文件:
你可以在插件中封装一个对外的方法:
    publicstatic Drawable getAssertDrawable(Context context,StringfileName){
       try {
          InputStreaminStream=context.getAssets().open(fileName);
          return newBitmapDrawable(BitmapFactory.decodeStream(inStream));
       } catch(IOException e) {
         Log.e(LOG_TAG, "Assert中"+fileName+"不存在");
       }
       returnnull;
    }
直接使用该方法可以得到文件。
后来又尝试在外部程序,直接使用context.getAssets().open(fileName)方法获取jar中文件,
让人喜出望外的是竟然成功了。呵呵!
后来分析,外部程序编译时,其实连同jar包中内容一起混编。jar包中的Assert文件会同外部程序的Assert一起
由AssertManager管理。
所以当你jar包中Assert内部文件和外部Assert中的文件有命名冲突时,编译器会报错的。

另外,还有人提供另外一种方法来读取诸如Drawable等文件夹下的文件。
    publicstatic Drawable getDrawableForJar(String resName,Classclass){
       InputStreaminStream=class.getResourceAsStream(resName);
       return newBitmapDrawable(BitmapFactory.decodeStream(inStream));
    }
使用class.getResourceAsStream()方法读取,注意这里resName是文件的相对路径,比如jar根目录下res/drawable/icon.png,
则调用方法为:class.getResourceAsStream(/res/drawable/icon.png);

这里主要是采用ClassLoader的下面几个方法来实现:

  public URL getResource(String name);

  public InputStream getResourceAsStream(String name)

  public static InputStreamgetSystemResourceAsStream(String name)

  public static URL getSystemResource(String name)

  后两个方法可以看出是静态的方法,这几个方法都可以从Jar中读取图片资源,但是对与动画的gif文件,笔者在尝试过程中发现,存在一些差异。

  String gifName为Gif文件在Jar中的相对路径。

  (1)使用了两个静态方法

  BufferedImageimage = ImageIO.read(ClassLoader.getSystemResourceAsStream(gifName));

  或者

  Image image = Toolkit.getDefaultToolkit().getImage(ClassLoader.getSystemResource(gifName));

  这两种方式可以成功地读取gif文件,但是对于gif动画,显示出来地是静态的。

  (2)使用其他两个方法

  Image image = Toolkit.getDefaultToolkit().getImage( this .getClass.getClassLoader()
.getResource(gifName));

  再这种方式下动画可以正常显示了。

3、使用library方法加载资源文件

在论坛中看到帖子讲述如何把工程作为libarary,让其他工程添加library,编译后会自动生成jar,然后在哪来使用。 
当时看到此贴,喜出望外,所以赶紧尝试下!

方法:选择插件工程,右键选择属性,选择Android,勾选下面Is Liabrary选项。 
然后,选择我们现有的工程,右键属性,选择Android,在library下add相应的库。你会看到,刚才我们设置的插件项目,就在其中。最后,点击应用,完成。

详细步骤:

按如下方法设置:

1. 假设要引用的android工程叫LibProject,引入到的工程叫MainProject;

2.设置LibProject,右键->Properties->Android,将Islibrary项选中,然后Apply;

3.设置MainProject,右键->->Properties->Android,在Library中,点击Add按钮,将LibProject工程加入,Apply即可。

你会看到我们的工程中多出插件工程的引用,而且可以使用R.XXX.XXX获取资源文件。

以为可以解决了,但是发现并没有生成想要的jar文件。在插件工程中,倒是有编译的class文件,却没有jar包。 
而我们往往是不能像这样把原工程给别人直接引用的。 
经过多次试验,始终没有生成jar,非常奇怪别人怎么弄得。。。

另外,拿以前通过这种方式生成的jar文件看,里面也不包含资源文件夹。。 
可以把生成的类共享出去。

把.so文件打包到jar中

查了一些方法,其中一个我比较喜欢,再load动态库的时候,把so文件复制到tmp目录下,然后删掉

//modify the static block

static {
try {
Class c = HelloJNI.class;
URL location =
c.getProtectionDomain().getCodeSource().getLocation();
ZipFile zf = new ZipFile(location.getPath());
// libhellojni.so is put in the lib folder
InputStream in = zf.getinputStream(zf.getEntry("lib/libhellojni.so"));
File f = File.createTempFile("JNI-", "Temp");
FileOutputStream out = new FileOutputStream(f);
byte [] buf = new byte[1024];
int len;
while ((len = in.read(buf)) > 0)
out.write(buf, 0, len);
in.close();
out.close();
System.load(f.getAbsolutePath());
f.delete();
} catch (Exception e) { // I am still lazy ~~~
e.printStackTrace();
}
}