• 注册
  • 投稿
    • 中文
    • English
  • 注册
  • 【新手上路】 【新手上路】 关注:6 内容:9

    通过代码注入在apk中添加图片轮播功能

  • 查看作者
  • 打赏作者
    • 【新手上路】
    • Lv.14
      永久会员
      大富豪

      通过代码注入在apk中添加图片轮播功能

      卓修改大师可以在没有源代码的基础上,通过代码注入插桩的方式,添加任何界面和任何逻辑功能。本教程主要通过在一款名为“多媒体评价器”的app上,将原来的显示静态图片的图片框变为多图片轮播的功能。通过讲解,给大家一个明确的插桩方式添加业务逻辑代码的思路,抛砖引玉而已。

      为了方便大家按照本教程操作,附带了所需要的文件,请点击这里下载

      1、 需求描述:根据用户的需要,需要在下述截屏应用的右侧添加图片轮播功能(目前是单独的图片,不能多张滚动),要求图片内置在apk中,放到Assets目录下面的指定文件夹中,图片数量不限,自动从该文件夹读取图片并随机自动轮播显示。

      2、 在没有源代码的情况下,如果要在apk中添加额外的逻辑,实现自定义功能,需要通过代码注入的方式来实现。一般的做法是,先用Android Studio开发一个完整实现所需功能的Demo项目,然后编译为apk,并通过安卓修改大师将apk进行反编译,获得smali代码和资源文件,最终将获得的代码和资源文件整合到目标项目,重新打包即可。

      3、 按照上述思路分步骤进行讲解说明,向大家完整展示如何通过插桩注入的方式,在任意的apk添加额外逻辑。

      第一步:创建Android Studio项目,并实现一个从Asset目录读取图片,并在ViewPaper实现轮播功能的工具类。代码如下:

         public class MarqueeImageControl {

          static ViewPager viewPager;

          static ArrayList<ImageView> imageviews;

          static Activity context;

          // 图片资源

          static Hashtable<Integer, AdData> hsAd = new Hashtable<Integer, AdData>();

          static int preposition = 0;// 设置高亮的位置

          static Handler handler = new Handler() {

              public void handleMessage(android.os.Message msg) {

                  int item = viewPager.getCurrentItem() + 1;

                  viewPager.setCurrentItem(item);

                  // 延迟发消息

                  handler.sendEmptyMessageDelayed(0, 3000);

              }

              ;

          };

          static boolean isdragging = false;

          public static class AdData {

              public String Title;

              public String Url;

              public Bitmap image;

              public AdData(String Title, String Url, Bitmap image) {

                  this.Title = Title;

                  this.Url = Url;

                  this.image = image;

              }

          }

          public static void show(final Activity context, int resid) {

              try {

                  AssetManager assets = context.getAssets();

                  //获取/assets/目录下所有文件

                  String[] images = assets.list(“pics”);

                  if (images == null) return;

                  for (int i = 0; i < images.length; i++) {

                      hsAd.put(i, new AdData(“”, “pics/” + images[i], null));

                  }

                  if (hsAd.size() <= 0)

                      return;

                  viewPager = new ViewPager(context);

                  viewPager.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));

                  ViewGroup view = context.findViewById(resid);

                  view.removeAllViews();

                  view.addView(viewPager);

                  context.runOnUiThread(new Runnable() {

                      public void run() {

                          imageviews = new ArrayList<ImageView>();

                          for (int i = 0; i < hsAd.size(); i++) {

                              AdData adData = (AdData) hsAd.get(i);

                              ImageView imageview = new ImageView(context);

                              AssetManager assets = context.getAssets();

                              InputStream in = null;

                              try {

                                  in = assets.open(adData.Url);

                                  imageview.setImageBitmap(BitmapFactory.decodeStream(in));

                              } catch (IOException e) {

                                  e.printStackTrace();

                              }

                              imageview.setScaleType(ImageView.ScaleType.FIT_START);

                              imageview.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));

                              imageviews.add(imageview);

                          }

                          viewPager.setAdapter(new Mypager());

                          viewPager.setOnPageChangeListener(new myon());

                          int item = Integer.MAX_VALUE / 2 – Integer.MAX_VALUE / 2 % imageviews.size();

                          viewPager.setCurrentItem(item);

                          handler.sendEmptyMessageDelayed(0, 3000);

                      }

                  });

              } catch (Exception e) {

                  e.printStackTrace();

              }

          }

          public static class myon implements ViewPager.OnPageChangeListener {

              @Override

              public void onPageScrollStateChanged(int arg0) {

                  if (arg0 == ViewPager.SCROLL_STATE_DRAGGING) {// 拖拽

                      isdragging = true;

                  } else if (arg0 == ViewPager.SCROLL_STATE_SETTLING) {// 滚动

                  } else if (arg0 == ViewPager.SCROLL_STATE_IDLE && isdragging) {// 静止

                      isdragging = false;

                      handler.removeCallbacksAndMessages(null);

                      handler.sendEmptyMessageDelayed(0, 3000);

                  }

              }

              @Override

              public void onPageScrolled(int arg0, float arg1, int arg2) {

              }

              @Override

              public void onPageSelected(int arg0) {

                  int realpostion = arg0 % imageviews.size();

                  preposition = realpostion;

              }

          }

          public static class Mypager extends PagerAdapter {

              @Override

              public int getCount() {

                  return Integer.MAX_VALUE;// int类型的最大值

              }

              @Override

              public Object instantiateItem(ViewGroup container, int position) {

                  int realPostion = position % imageviews.size();

                  final ImageView imageview = imageviews.get(realPostion);

                  container.addView(imageview);// 添加到Viewpager中

                  imageview.setOnTouchListener(new OnTouchListener() {

                      @Override

                      public boolean onTouch(View v, MotionEvent event) {

                          switch (event.getAction()) {

                              case MotionEvent.ACTION_DOWN:// 手指按下时的操作

                                  handler.removeCallbacksAndMessages(null);

                                  break;

                              case MotionEvent.ACTION_MOVE:// 手指移动时的操作

                                  break;

                              case MotionEvent.ACTION_CANCEL:// 事件取消

                                  handler.removeCallbacksAndMessages(null);

                                  handler.sendEmptyMessageDelayed(0, 3000);

                                  break;

                              case MotionEvent.ACTION_UP:// 手指抬起时的操作

                                  handler.removeCallbacksAndMessages(null);

                                  handler.sendEmptyMessageDelayed(0, 3000);

                                  break;

                          }

                          return false;

                      }

                  });

                  imageview.setTag(realPostion);

                  return imageview;

              }

              @Override

              public boolean isViewFromObject(View arg0, Object arg1) {

                  return arg0 == arg1;

              }

              @Override

              public void destroyItem(ViewGroup container, int position, Object object) {

                  container.removeView((View) object);

              }

          }

      }

       

      需要重点说明的是,为了减少整合的复杂度,插桩的代码尽量放到单独的类里面,入口的调用方法尽量是静态方法,例如本例的入口调用函数是:

      public static void show(final Activity context, int resid)

      该方法有两个参数,一个是当前Activity类入口,另外一个是插入轮播图片的宿主布局的资源id。插桩代码放到单独类的好处是,反编译后将该类所有的生成的smali文件全部拷贝到目标项目中即可,不用考虑彼此之前的关联关系,也不用考虑类和变量的耦合问题,降低整合的复杂度,使整合更简单。

      在Demo的Activity测试页面中调用上述的方法为:

      MarqueeImageControl.show(this, R.id.pic);

      上述的R.id.pic是xml布局中定义的一个类似于LinerLayout这样的布局,作为放置轮播功能的控件宿主。

      代码为:

      <LinearLayout

          android:orientation=”vertical”

          android:id=”@+id/pic”

          android:layout_gravity=”center”

          android:background=”@color/cardview_dark_background”

          android:layout_width=”300dp”

          android:layout_height=”600dp”/>

      在将来插桩整合的时候,目标应用中也应该有这样的控件,用来接纳需要添加进来的轮播功能。

      确保经过测试,该demo实现了相关的功能,然后通过Android Studio的打包功能,将Demo项目打包为apk备用。

       

      第二步:将上述Demo的Apk文件通过安卓修改大师反编译,反编译后获得smali代码,将获得的代码和资源复制到目标项目中进行整合。

      反编译demo项目并打开目录,同样也打开目标项目的项目目录,如下图:

      将上述demo反编译生成的类拖拽到目标项目的smali目录下,demo目录下面的类文件请通过类的包名路径在上述目录中依次展开找到。需要植入的插桩的smali类文件可以放到目标项目的smali目录下面的任何目录,建议直接放到smali根目录或者自定义创建的目录中,方便查看和修改。

      通过上述方法,将核心的类文件已经集成到了目标项目的smali源代码目录中。如果你实现的类文件有第三方引用的类,需要将相关的类也要一并通过上述方法拷贝到目标项目的smali目录中(例如demo类用到了androidx类,需要将androidx类一并拷贝到目标项目中)。

      第三步:通过安卓修改大师的代码布局定位功能,定位要添加和修改的布局控件。

      确保手机和电脑连接成功,安卓修改大师底部处于连接状态,点击修改大师左侧的代码布局定位功能,手机上面浏览到需要添加和修改布局的页面,然后点击上述页面上的抓取界面布局按钮,即可获取当前页面的界面布局和布局层次情况。点击左侧预览图的需要添加插件的区域,右下角会显示该控件的id名称(iv_show),点击右侧的定位布局和代码,将自动进行代码和布局查找工作。

      通过上述的界面抓取功能,也同时获得该界面的类名和包名。见上述截图的上部。类名为com.yntd.jhpj/com.yntd.jhpj.ui.MainActivity,请牢记,后面有用。

      系统自动查找到该图片控件的布局和控件:

      双击查询结果,将进入布局xml界面,下图列出来的是该控件的布局xml(下图下面的红框),一般如果要做界面插入,建议不要动原来的界面元素,因此我们把原来的图片框元素添加 n1:visibility=”gone” 进行隐藏,在该元素的上部添加了单独的布局(用来作为轮播控件的宿主控件)元素用来放置新添加的轮播功能(下图的上面红框),请注意为了保持界面布局一致性,确保新插入的布局控件和原来的控件的布局和大小尺寸的属性一样。

      插入的布局xml:

       <LinearLayout n1:id=”@id/iv_pic” n1:orientation=”vertical” n1:layout_width=”fill_parent” n1:layout_height=”450.0dp” n1:layout_marginLeft=”20.0dip” n1:layout_weight=”5.0″ n1:scaleType=”fitXY” CurrentID=”34″ />

      到此为止已经添加了宿主控件,为将来显示轮播图片打好了基础工作。新增加的这个布局为了方便程序中调用,给定了新的id,目前该id还没有对应的资源id(前面写的注入的类需要宿主的资源id参数),布局中临时定义的id,需要重新编译后才能自动生成资源id。

      点击左侧的打包/签名工作,然后打开的页面中点击项目打包按钮,将自动进行项目打包。

      确保能顺利打包完成,打包成功后,新添加的界面布局控件id才会生成资源id,切记。我们前面为那个布局新定义的id为“iv_pic”,因此点击安卓修改大师面板左侧的“搜索替换”功能,并搜索“iv_pic”,在结果中有一条public.xml文件的搜索结果,该文件里面就是全部的资源对应的资源id,记录下iv_pic对应的资源id(见下面的红框),后续有用。

      第四步:通过插桩方式插入注入的代码。前面已经通过界面抓取获得类名com.yntd.jhpj.ui.MainActivity,在安卓修改大师左侧的代码布局修改功能,点击人代码树状导航,按照上述类路径依次点击找到该类,一般是在oncreate方法里面添加注入方

      法。

      插入的代码行为:

      #集成的代码

          const v0, 0x7f0800e7

          invoke-static {p0, v0}, Lcom/kongyu/project/MarqueeImageControl;->show(Landroid/app/Activity;I)V

       两个参数分别为当前的类的引用p0和上述新创建的宿主控件的资源id,改调用方法为smali语句,如果不熟悉java对应的方法如何用smali调用,可以在前面第一步的demo里面写好调用示例,第二步反编译的时候即可获得对应的smali写法。

      至此,已经完整实现了通过插桩的模式插入自定义的逻辑代码,这种方式适合在任何apk中插入任何逻辑和任何布局,只不过是复杂度的区别罢了。

      一切修改完毕后,注意在编辑器右上角点击保存,然后回到打包签名进行项目打包,手机点击电脑的话,会自动在手机上面安装打包后的成果apk。

       本次教程到此结束,文中提及的资源和代码,以及项目apk在文中已经附带,大家可以跟随学习。

      请登录之后再进行评论

      登录
    • 做任务
    • 帖子间隔 侧栏位置: